]> source.dussan.org Git - vaadin-framework.git/commitdiff
Add CustomField component in Vaadin core (#3718)
authorHenri Sara <hesara@vaadin.com>
Thu, 8 Dec 2011 08:38:53 +0000 (10:38 +0200)
committerHenri Sara <hesara@vaadin.com>
Thu, 8 Dec 2011 08:38:53 +0000 (10:38 +0200)
CustomField component, related changes in AbstractField and some
tests/examples added to Vaadin core.

14 files changed:
src/com/vaadin/ui/AbstractField.java
src/com/vaadin/ui/CustomField.java [new file with mode: 0644]
tests/testbench/com/vaadin/tests/components/customfield/AbstractNestedFormExample.java [new file with mode: 0644]
tests/testbench/com/vaadin/tests/components/customfield/AddressField.java [new file with mode: 0644]
tests/testbench/com/vaadin/tests/components/customfield/AddressFormExample.java [new file with mode: 0644]
tests/testbench/com/vaadin/tests/components/customfield/BooleanField.java [new file with mode: 0644]
tests/testbench/com/vaadin/tests/components/customfield/BooleanFieldExample.html [new file with mode: 0644]
tests/testbench/com/vaadin/tests/components/customfield/BooleanFieldExample.java [new file with mode: 0644]
tests/testbench/com/vaadin/tests/components/customfield/EmbeddedForm.java [new file with mode: 0644]
tests/testbench/com/vaadin/tests/components/customfield/EmbeddedFormExample.html [new file with mode: 0644]
tests/testbench/com/vaadin/tests/components/customfield/EmbeddedFormExample.java [new file with mode: 0644]
tests/testbench/com/vaadin/tests/components/customfield/NestedFormExample.html [new file with mode: 0644]
tests/testbench/com/vaadin/tests/components/customfield/NestedFormExample.java [new file with mode: 0644]
tests/testbench/com/vaadin/tests/components/customfield/NestedPersonForm.java [new file with mode: 0644]

index 73760d9247fe9bbbe53773552892ac89f0fcbaf6..44fb71ac84e7ff8809201969634df2953133f603 100644 (file)
@@ -328,7 +328,7 @@ public abstract class AbstractField<T> extends AbstractComponent implements
             setModified(false);
 
             // If the new value differs from the previous one
-            if (!equals(newFieldValue, value)) {
+            if (!equals(newFieldValue, getInternalValue())) {
                 setInternalValue(newFieldValue);
                 fireValueChange(false);
             } else if (wasModified) {
@@ -355,7 +355,7 @@ public abstract class AbstractField<T> extends AbstractComponent implements
     private T getFieldValue() {
         // Give the value from abstract buffers if the field if possible
         if (dataSource == null || !isReadThrough() || isModified()) {
-            return value;
+            return getInternalValue();
         }
 
         // There is no buffered value so use whatever the data model provides
@@ -521,7 +521,7 @@ public abstract class AbstractField<T> extends AbstractComponent implements
     protected void setValue(T newFieldValue, boolean repaintIsNotNeeded)
             throws Property.ReadOnlyException, Property.ConversionException {
 
-        if (!equals(newFieldValue, value)) {
+        if (!equals(newFieldValue, getInternalValue())) {
 
             // Read only fields can not be changed
             if (isReadOnly()) {
@@ -646,7 +646,7 @@ public abstract class AbstractField<T> extends AbstractComponent implements
     public void setPropertyDataSource(Property newDataSource) {
 
         // Saves the old value
-        final Object oldValue = value;
+        final Object oldValue = getInternalValue();
 
         // Stops listening the old data source changes
         if (dataSource != null
@@ -705,6 +705,7 @@ public abstract class AbstractField<T> extends AbstractComponent implements
         }
 
         // Fires value change if the value has changed
+        T value = getInternalValue();
         if ((value != oldValue)
                 && ((value != null && !value.equals(oldValue)) || value == null)) {
             fireValueChange(false);
@@ -1143,7 +1144,7 @@ public abstract class AbstractField<T> extends AbstractComponent implements
         if (isReadThrough()) {
             if (committingValueToDataSource) {
                 boolean propertyNotifiesOfTheBufferedValue = equals(event
-                        .getProperty().getValue(), value);
+                        .getProperty().getValue(), getInternalValue());
                 if (!propertyNotifiesOfTheBufferedValue) {
                     /*
                      * Property (or chained property like PropertyFormatter) now
@@ -1201,6 +1202,14 @@ public abstract class AbstractField<T> extends AbstractComponent implements
         requestRepaint();
     }
 
+    /**
+     * 
+     * @return
+     */
+    protected T getInternalValue() {
+        return value;
+    }
+
     /**
      * Sets the internal field value. This is purely used by AbstractField to
      * change the internal Field value. It does not trigger valuechange events.
diff --git a/src/com/vaadin/ui/CustomField.java b/src/com/vaadin/ui/CustomField.java
new file mode 100644 (file)
index 0000000..eb4cdc3
--- /dev/null
@@ -0,0 +1,242 @@
+package com.vaadin.ui;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.Iterator;
+
+import com.vaadin.data.Property;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.gwt.client.ui.VCustomComponent;
+
+/**
+ * A {@link Field} whose UI content can be constructed by the user, enabling the
+ * creation of e.g. form fields by composing Vaadin components. Customization of
+ * both the visual presentation and the logic of the field is possible.
+ * 
+ * Subclasses must implement {@link #getType()} and {@link #createContent()}.
+ * 
+ * Most custom fields can simply compose a user interface that calls the methods
+ * {@link #setInternalValue(Object)} and {@link #getInternalValue()} when
+ * necessary.
+ * 
+ * It is also possible to override {@link #commit()},
+ * {@link #setPropertyDataSource(Property)} and other logic of the field.
+ * 
+ * @since 7.0
+ */
+@ClientWidget(VCustomComponent.class)
+public abstract class CustomField<T> extends AbstractField<T> implements
+        ComponentContainer {
+
+    /**
+     * The root component implementing the custom component.
+     */
+    private Component root = null;
+
+    /**
+     * Constructs a new custom field.
+     * 
+     * <p>
+     * The component is implemented by wrapping the methods of the composition
+     * root component given as parameter. The composition root must be set
+     * before the component can be used.
+     * </p>
+     */
+    public CustomField() {
+        // expand horizontally by default
+        setWidth(100, UNITS_PERCENTAGE);
+    }
+
+    /**
+     * Constructs the content and notifies it that the {@link CustomField} is
+     * attached to a window.
+     * 
+     * @see com.vaadin.ui.Component#attach()
+     */
+    @Override
+    public void attach() {
+        root = getContent();
+        super.attach();
+        getContent().setParent(this);
+        getContent().attach();
+
+        fireComponentAttachEvent(getContent());
+    }
+
+    /**
+     * Notifies the content that the {@link CustomField} is detached from a
+     * window.
+     * 
+     * @see com.vaadin.ui.Component#detach()
+     */
+    @Override
+    public void detach() {
+        super.detach();
+        getContent().detach();
+    }
+
+    @Override
+    public void paintContent(PaintTarget target) throws PaintException {
+        if (getContent() == null) {
+            throw new IllegalStateException(
+                    "Content component or layout of the field must be set before the "
+                            + getClass().getName() + " can be painted");
+        }
+
+        getContent().paint(target);
+    }
+
+    /**
+     * Returns the content of the
+     * 
+     * @return
+     */
+    protected Component getContent() {
+        if (null == root) {
+            root = createContent();
+        }
+        return root;
+    }
+
+    /**
+     * Create the content component or layout for the field. Subclasses of
+     * {@link CustomField} should implement this method.
+     * 
+     * Note that this method is called when the CustomField is attached to a
+     * layout or when {@link #getContent()} is called explicitly for the first
+     * time. It is only called once for a {@link CustomField}.
+     * 
+     * @return
+     */
+    protected abstract Component createContent();
+
+    private void requestContentRepaint() {
+        if (getParent() == null) {
+            // skip repaint - not yet attached
+            return;
+        }
+        if (getContent() instanceof ComponentContainer) {
+            ((ComponentContainer) getContent()).requestRepaintAll();
+        } else {
+            getContent().requestRepaint();
+        }
+    }
+
+    // Size related methods
+    // TODO might not be necessary to override but following the pattern from
+    // AbstractComponentContainer
+
+    @Override
+    public void setHeight(float height, int unit) {
+        super.setHeight(height, unit);
+        requestContentRepaint();
+    }
+
+    @Override
+    public void setWidth(float height, int unit) {
+        super.setWidth(height, unit);
+        requestContentRepaint();
+    }
+
+    // ComponentContainer methods
+
+    private class ComponentIterator implements Iterator<Component>,
+            Serializable {
+        boolean first = getContent() != null;
+
+        public boolean hasNext() {
+            return first;
+        }
+
+        public Component next() {
+            first = false;
+            return getContent();
+        }
+
+        public void remove() {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    public Iterator<Component> getComponentIterator() {
+        return new ComponentIterator();
+    }
+
+    public int getComponentCount() {
+        return (null != getContent()) ? 1 : 0;
+    }
+
+    public void requestRepaintAll() {
+        requestRepaint();
+
+        requestContentRepaint();
+    }
+
+    /**
+     * Fires the component attached event. This should be called by the
+     * addComponent methods after the component have been added to this
+     * container.
+     * 
+     * @param component
+     *            the component that has been added to this container.
+     */
+    protected void fireComponentAttachEvent(Component component) {
+        fireEvent(new ComponentAttachEvent(this, component));
+    }
+
+    // TODO remove these methods when ComponentContainer interface is cleaned up
+
+    public void addComponent(Component c) {
+        throw new UnsupportedOperationException();
+    }
+
+    public void removeComponent(Component c) {
+        throw new UnsupportedOperationException();
+    }
+
+    public void removeAllComponents() {
+        throw new UnsupportedOperationException();
+    }
+
+    public void replaceComponent(Component oldComponent, Component newComponent) {
+        throw new UnsupportedOperationException();
+    }
+
+    public void moveComponentsFrom(ComponentContainer source) {
+        throw new UnsupportedOperationException();
+    }
+
+    private static final Method COMPONENT_ATTACHED_METHOD;
+
+    static {
+        try {
+            COMPONENT_ATTACHED_METHOD = ComponentAttachListener.class
+                    .getDeclaredMethod("componentAttachedToContainer",
+                            new Class[] { ComponentAttachEvent.class });
+        } catch (final java.lang.NoSuchMethodException e) {
+            // This should never happen
+            throw new java.lang.RuntimeException(
+                    "Internal error finding methods in CustomField");
+        }
+    }
+
+    public void addListener(ComponentAttachListener listener) {
+        addListener(ComponentContainer.ComponentAttachEvent.class, listener,
+                COMPONENT_ATTACHED_METHOD);
+    }
+
+    public void removeListener(ComponentAttachListener listener) {
+        removeListener(ComponentContainer.ComponentAttachEvent.class, listener,
+                COMPONENT_ATTACHED_METHOD);
+    }
+
+    public void addListener(ComponentDetachListener listener) {
+        // content never detached
+    }
+
+    public void removeListener(ComponentDetachListener listener) {
+        // content never detached
+    }
+
+}
diff --git a/tests/testbench/com/vaadin/tests/components/customfield/AbstractNestedFormExample.java b/tests/testbench/com/vaadin/tests/components/customfield/AbstractNestedFormExample.java
new file mode 100644 (file)
index 0000000..d8c962f
--- /dev/null
@@ -0,0 +1,75 @@
+package com.vaadin.tests.components.customfield;\r
+\r
+import com.vaadin.data.Item;\r
+import com.vaadin.data.Property;\r
+import com.vaadin.data.Property.ValueChangeEvent;\r
+import com.vaadin.tests.components.TestBase;\r
+import com.vaadin.tests.util.Person;\r
+import com.vaadin.ui.Table;\r
+\r
+/**\r
+ * Demonstrate the use of a form as a custom field within another form.\r
+ */\r
+public abstract class AbstractNestedFormExample extends TestBase {\r
+    private NestedPersonForm personForm;\r
+    private boolean embeddedAddress;\r
+\r
+    public void setup(boolean embeddedAddress) {\r
+        this.embeddedAddress = embeddedAddress;\r
+\r
+        addComponent(getPersonTable());\r
+    }\r
+\r
+    /**\r
+     * Creates a table with two person objects\r
+     */\r
+    public Table getPersonTable() {\r
+        Table table = new Table();\r
+        table.setPageLength(5);\r
+        table.setSelectable(true);\r
+        table.setImmediate(true);\r
+        table.setNullSelectionAllowed(true);\r
+        table.addContainerProperty("Name", String.class, null);\r
+        table.addListener(getTableValueChangeListener());\r
+        Person person = new Person("Teppo", "Testaaja",\r
+                "teppo.testaaja@example.com", "", "Ruukinkatu 2–4", 20540,\r
+                "Turku");\r
+        Person person2 = new Person("Taina", "Testaaja",\r
+                "taina.testaaja@example.com", "", "Ruukinkatu 2–4", 20540,\r
+                "Turku");\r
+        Item item = table.addItem(person);\r
+        item.getItemProperty("Name").setValue(\r
+                person.getFirstName() + " " + person.getLastName());\r
+        item = table.addItem(person2);\r
+        item.getItemProperty("Name").setValue(\r
+                person2.getFirstName() + " " + person2.getLastName());\r
+        return table;\r
+    }\r
+\r
+    /**\r
+     * Creates value change listener for the table\r
+     */\r
+    private Property.ValueChangeListener getTableValueChangeListener() {\r
+        return new Property.ValueChangeListener() {\r
+\r
+            public void valueChange(ValueChangeEvent event) {\r
+                if (personForm != null) {\r
+                    removeComponent(personForm);\r
+                }\r
+                if (event.getProperty().getValue() != null) {\r
+                    personForm = new NestedPersonForm((Person) event\r
+                            .getProperty().getValue(), embeddedAddress);\r
+                    personForm.setWidth("350px");\r
+                    addComponent(personForm);\r
+                }\r
+            }\r
+\r
+        };\r
+    }\r
+\r
+    @Override\r
+    protected Integer getTicketNumber() {\r
+        return null;\r
+    }\r
+\r
+}\r
diff --git a/tests/testbench/com/vaadin/tests/components/customfield/AddressField.java b/tests/testbench/com/vaadin/tests/components/customfield/AddressField.java
new file mode 100644 (file)
index 0000000..a02b323
--- /dev/null
@@ -0,0 +1,98 @@
+package com.vaadin.tests.components.customfield;\r
+\r
+import java.util.Arrays;\r
+import java.util.List;\r
+\r
+import com.vaadin.data.Buffered;\r
+import com.vaadin.data.Validator.InvalidValueException;\r
+import com.vaadin.data.util.BeanItem;\r
+import com.vaadin.tests.util.Address;\r
+import com.vaadin.ui.Component;\r
+import com.vaadin.ui.CustomField;\r
+import com.vaadin.ui.Form;\r
+\r
+/**\r
+ * Nested form for the Address object of the Person object\r
+ */\r
+public class AddressField extends CustomField<Address> {\r
+    private Form addressForm;\r
+    private final Form parentForm;\r
+\r
+    public AddressField() {\r
+        this(null);\r
+    }\r
+\r
+    public AddressField(Form parentForm) {\r
+        this.parentForm = parentForm;\r
+    }\r
+\r
+    @Override\r
+    protected Component createContent() {\r
+        if (parentForm != null) {\r
+            addressForm = new EmbeddedForm(parentForm);\r
+        } else {\r
+            addressForm = new Form();\r
+        }\r
+        addressForm.setCaption("Address");\r
+        addressForm.setWriteThrough(false);\r
+\r
+        // make sure field changes are sent early\r
+        addressForm.setImmediate(true);\r
+\r
+        return addressForm;\r
+    }\r
+\r
+    @Override\r
+    protected Form getContent() {\r
+        return (Form) super.getContent();\r
+    }\r
+\r
+    @Override\r
+    public void setInternalValue(Address address) throws ReadOnlyException,\r
+            ConversionException {\r
+        // create the address if not given\r
+        if (null == address) {\r
+            address = new Address();\r
+        }\r
+\r
+        super.setInternalValue(address);\r
+\r
+        // set item data source and visible properties in a single operation to\r
+        // avoid creating fields multiple times\r
+        List<String> visibleProperties = Arrays.asList("streetAddress",\r
+                "postalCode", "city");\r
+        getContent().setItemDataSource(new BeanItem<Address>(address),\r
+                visibleProperties);\r
+    }\r
+\r
+    /**\r
+     * commit changes of the address form\r
+     */\r
+    @Override\r
+    public void commit() throws Buffered.SourceException, InvalidValueException {\r
+        addressForm.commit();\r
+        super.commit();\r
+    }\r
+\r
+    /**\r
+     * discard changes of the address form\r
+     */\r
+    @Override\r
+    public void discard() throws Buffered.SourceException {\r
+        // Do not discard the top-level value\r
+        // super.discard();\r
+        addressForm.discard();\r
+    }\r
+\r
+    @Override\r
+    public boolean isReadOnly() {\r
+        // In this application, the address is modified implicitly by\r
+        // addressForm.commit(), not by setting the Address object for a Person.\r
+        return false;\r
+    }\r
+\r
+    @Override\r
+    public Class<Address> getType() {\r
+        return Address.class;\r
+    }\r
+}
\ No newline at end of file
diff --git a/tests/testbench/com/vaadin/tests/components/customfield/AddressFormExample.java b/tests/testbench/com/vaadin/tests/components/customfield/AddressFormExample.java
new file mode 100644 (file)
index 0000000..5386a40
--- /dev/null
@@ -0,0 +1,45 @@
+package com.vaadin.tests.components.customfield;\r
+\r
+import com.vaadin.tests.components.TestBase;\r
+import com.vaadin.tests.util.Address;\r
+import com.vaadin.ui.Button;\r
+import com.vaadin.ui.Button.ClickEvent;\r
+\r
+/**\r
+ * Demonstrate a custom field which is a form, and contains another custom field\r
+ * for the selection of a city.\r
+ */\r
+public class AddressFormExample extends TestBase {\r
+\r
+    @Override\r
+    protected void setup() {\r
+        Address address = new Address("Ruukinkatu 2-4", 20540, "Turku");\r
+        final AddressField field = new AddressField();\r
+        field.setValue(address);\r
+\r
+        addComponent(field);\r
+\r
+        Button commitButton = new Button("Save", new Button.ClickListener() {\r
+            public void buttonClick(ClickEvent event) {\r
+                field.commit();\r
+                Address address = field.getValue();\r
+                field.getWindow().showNotification(\r
+                        "Address saved: " + address.getStreetAddress() + ", "\r
+                                + address.getPostalCode() + ", "\r
+                                + address.getCity());\r
+            }\r
+        });\r
+        addComponent(commitButton);\r
+    }\r
+\r
+    @Override\r
+    protected String getDescription() {\r
+        return "Custom field for editing an Address";\r
+    }\r
+\r
+    @Override\r
+    protected Integer getTicketNumber() {\r
+        return null;\r
+    }\r
+\r
+}\r
diff --git a/tests/testbench/com/vaadin/tests/components/customfield/BooleanField.java b/tests/testbench/com/vaadin/tests/components/customfield/BooleanField.java
new file mode 100644 (file)
index 0000000..196a04e
--- /dev/null
@@ -0,0 +1,45 @@
+package com.vaadin.tests.components.customfield;\r
+\r
+import com.vaadin.ui.Button;\r
+import com.vaadin.ui.Button.ClickEvent;\r
+import com.vaadin.ui.Button.ClickListener;\r
+import com.vaadin.ui.Component;\r
+import com.vaadin.ui.CustomField;\r
+import com.vaadin.ui.Label;\r
+import com.vaadin.ui.VerticalLayout;\r
+\r
+/**\r
+ * An example of a custom field for editing a boolean value. The field is\r
+ * composed of multiple components, and could also edit a more complex data\r
+ * structures. Here, the commit etc. logic is not overridden.\r
+ */\r
+public class BooleanField extends CustomField {\r
+\r
+    @Override\r
+    protected Component createContent() {\r
+        VerticalLayout layout = new VerticalLayout();\r
+\r
+        layout.addComponent(new Label("Please click the button"));\r
+\r
+        final Button button = new Button("Click me");\r
+        button.addListener(new ClickListener() {\r
+            public void buttonClick(ClickEvent event) {\r
+                Object value = getValue();\r
+                boolean newValue = true;\r
+                if ((value instanceof Boolean) && ((Boolean) value)) {\r
+                    newValue = false;\r
+                }\r
+                setValue(newValue);\r
+                button.setCaption(newValue ? "On" : "Off");\r
+            }\r
+        });\r
+        layout.addComponent(button);\r
+\r
+        return layout;\r
+    }\r
+\r
+    @Override\r
+    public Class<?> getType() {\r
+        return Boolean.class;\r
+    }\r
+}
\ No newline at end of file
diff --git a/tests/testbench/com/vaadin/tests/components/customfield/BooleanFieldExample.html b/tests/testbench/com/vaadin/tests/components/customfield/BooleanFieldExample.html
new file mode 100644 (file)
index 0000000..6b1b3e8
--- /dev/null
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<link rel="selenium.base" href="" />
+<title>BooleanFieldExample</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr><td rowspan="1" colspan="3">BooleanFieldExample</td></tr>
+</thead><tbody>
+<tr>
+       <td>open</td>
+       <td>/run/com.vaadin.tests.components.customfield.BooleanFieldExample?restartApplication</td>
+       <td></td>
+</tr>
+<tr>
+       <td>screenCapture</td>
+       <td></td>
+       <td>initial</td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>vaadin=runcomvaadintestscomponentscustomfieldBooleanFieldExample::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VVerticalLayout[0]/ChildComponentContainer[0]/VForm[0]/VFormLayout[0]/VFormLayout$VFormLayoutTable[0]/VCustomComponent[0]/VVerticalLayout[0]/ChildComponentContainer[1]/VButton[0]/domChild[0]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>screenCapture</td>
+       <td></td>
+       <td>on</td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>vaadin=runcomvaadintestscomponentscustomfieldBooleanFieldExample::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VVerticalLayout[0]/ChildComponentContainer[0]/VForm[0]/VFormLayout[0]/VFormLayout$VFormLayoutTable[0]/VCustomComponent[0]/VVerticalLayout[0]/ChildComponentContainer[1]/VButton[0]/domChild[0]/domChild[0]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>screenCapture</td>
+       <td></td>
+       <td>off</td>
+</tr>
+
+</tbody></table>
+</body>
+</html>
diff --git a/tests/testbench/com/vaadin/tests/components/customfield/BooleanFieldExample.java b/tests/testbench/com/vaadin/tests/components/customfield/BooleanFieldExample.java
new file mode 100644 (file)
index 0000000..d53a38e
--- /dev/null
@@ -0,0 +1,89 @@
+package com.vaadin.tests.components.customfield;\r
+\r
+import com.vaadin.data.Item;\r
+import com.vaadin.data.util.BeanItem;\r
+import com.vaadin.tests.components.TestBase;\r
+import com.vaadin.ui.Button;\r
+import com.vaadin.ui.Button.ClickEvent;\r
+import com.vaadin.ui.Button.ClickListener;\r
+import com.vaadin.ui.Component;\r
+import com.vaadin.ui.DefaultFieldFactory;\r
+import com.vaadin.ui.Field;\r
+import com.vaadin.ui.Form;\r
+import com.vaadin.ui.VerticalLayout;\r
+\r
+public class BooleanFieldExample extends TestBase {\r
+\r
+    /**\r
+     * Data model class with two boolean fields.\r
+     */\r
+    public static class TwoBooleans {\r
+        private boolean normal;\r
+        private boolean custom;\r
+\r
+        public void setNormal(boolean normal) {\r
+            this.normal = normal;\r
+        }\r
+\r
+        public boolean isNormal() {\r
+            return normal;\r
+        }\r
+\r
+        public void setCustom(boolean custom) {\r
+            this.custom = custom;\r
+        }\r
+\r
+        public boolean isCustom() {\r
+            return custom;\r
+        }\r
+    }\r
+\r
+    @Override\r
+    protected void setup() {\r
+        final VerticalLayout layout = new VerticalLayout();\r
+        layout.setMargin(true);\r
+\r
+        final Form form = new Form();\r
+        form.setFormFieldFactory(new DefaultFieldFactory() {\r
+            @Override\r
+            public Field createField(Item item, Object propertyId,\r
+                    Component uiContext) {\r
+                if ("custom".equals(propertyId)) {\r
+                    return new BooleanField();\r
+                }\r
+                return super.createField(item, propertyId, uiContext);\r
+            }\r
+        });\r
+        final TwoBooleans data = new TwoBooleans();\r
+        form.setItemDataSource(new BeanItem<TwoBooleans>(data));\r
+\r
+        layout.addComponent(form);\r
+\r
+        Button submit = new Button("Submit", new ClickListener() {\r
+            public void buttonClick(ClickEvent event) {\r
+                form.commit();\r
+                layout.getWindow()\r
+                        .showNotification(\r
+                                "The custom boolean field value is "\r
+                                        + data.isCustom()\r
+                                        + ".<br>"\r
+                                        + "The checkbox (default boolean field) value is "\r
+                                        + data.isNormal() + ".");\r
+            }\r
+        });\r
+        layout.addComponent(submit);\r
+\r
+        addComponent(layout);\r
+    }\r
+\r
+    @Override\r
+    protected String getDescription() {\r
+        return "A customized field (a two-state button) for editing a boolean value.";\r
+    }\r
+\r
+    @Override\r
+    protected Integer getTicketNumber() {\r
+        return null;\r
+    }\r
+\r
+}\r
diff --git a/tests/testbench/com/vaadin/tests/components/customfield/EmbeddedForm.java b/tests/testbench/com/vaadin/tests/components/customfield/EmbeddedForm.java
new file mode 100644 (file)
index 0000000..ceaea1f
--- /dev/null
@@ -0,0 +1,67 @@
+package com.vaadin.tests.components.customfield;\r
+\r
+import java.util.HashMap;\r
+import java.util.Map;\r
+\r
+import com.vaadin.ui.CustomLayout;\r
+import com.vaadin.ui.Field;\r
+import com.vaadin.ui.Form;\r
+import com.vaadin.ui.Layout;\r
+\r
+/**\r
+ * Form that displays its fields in the layout of another form.\r
+ * \r
+ * The fields are still logically part of this form even though they are in the\r
+ * layout of the parent form. The embedded form itself is automatically hidden.\r
+ * \r
+ * TODO Known issue: any field factory creating an {@link EmbeddedForm}\r
+ * (directly or indirectly) should re-use the field once it has been created to\r
+ * avoid the creation of duplicate fields when e.g. setting the visible item\r
+ * properties.\r
+ */\r
+public class EmbeddedForm extends Form {\r
+    private Form parentForm;\r
+    private Map<Object, Field> fields = new HashMap<Object, Field>();\r
+\r
+    /**\r
+     * Create a form that places its fields in another {@link Form}.\r
+     * \r
+     * @param parentForm\r
+     *            form to which to embed the fields, not null\r
+     */\r
+    public EmbeddedForm(Form parentForm) {\r
+        this.parentForm = parentForm;\r
+        setVisible(false);\r
+    }\r
+\r
+    @Override\r
+    protected void attachField(Object propertyId, Field field) {\r
+        if (propertyId == null || field == null) {\r
+            return;\r
+        }\r
+\r
+        Layout layout = parentForm.getLayout();\r
+\r
+        Field oldField = fields.get(propertyId);\r
+        if (oldField != null) {\r
+            layout.removeComponent(oldField);\r
+        }\r
+\r
+        fields.put(propertyId, field);\r
+\r
+        if (layout instanceof CustomLayout) {\r
+            ((CustomLayout) layout).addComponent(field, propertyId.toString());\r
+        } else {\r
+            layout.addComponent(field);\r
+        }\r
+    }\r
+\r
+    @Override\r
+    public boolean removeItemProperty(Object id) {\r
+        // remove the field from the parent layout if already added there\r
+        parentForm.getLayout().removeComponent(fields.get(id));\r
+        fields.remove(id);\r
+\r
+        return super.removeItemProperty(id);\r
+    }\r
+}
\ No newline at end of file
diff --git a/tests/testbench/com/vaadin/tests/components/customfield/EmbeddedFormExample.html b/tests/testbench/com/vaadin/tests/components/customfield/EmbeddedFormExample.html
new file mode 100644 (file)
index 0000000..2af441b
--- /dev/null
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<link rel="selenium.base" href="http://localhost:8888/" />
+<title>EmbeddedFormExample</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr><td rowspan="1" colspan="3">EmbeddedFormExample</td></tr>
+</thead><tbody>
+<tr>
+       <td>open</td>
+       <td>/run/com.vaadin.tests.components.customfield.EmbeddedFormExample?restartApplication</td>
+       <td></td>
+</tr>
+<tr>
+       <td>mouseClick</td>
+       <td>vaadin=runcomvaadintestscomponentscustomfieldEmbeddedFormExample::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VScrollTable[0]/domChild[1]/domChild[0]/domChild[1]/domChild[0]/domChild[0]/domChild[0]/domChild[0]</td>
+       <td>66,10</td>
+</tr>
+<tr>
+       <td>screenCapture</td>
+       <td></td>
+       <td>initial</td>
+</tr>
+<tr>
+       <td>enterCharacter</td>
+       <td>vaadin=runcomvaadintestscomponentscustomfieldEmbeddedFormExample::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[1]/VForm[0]/VFormLayout[0]/VFormLayout$VFormLayoutTable[0]/VTextField[4]</td>
+       <td>Turkuaa</td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>vaadin=runcomvaadintestscomponentscustomfieldEmbeddedFormExample::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[1]/VForm[0]/VHorizontalLayout[0]/ChildComponentContainer[0]/VHorizontalLayout[0]/ChildComponentContainer[0]/VButton[0]/domChild[0]/domChild[0]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>verifyValue</td>
+       <td>vaadin=runcomvaadintestscomponentscustomfieldEmbeddedFormExample::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[1]/VForm[0]/VFormLayout[0]/VFormLayout$VFormLayoutTable[0]/VTextField[4]</td>
+       <td>Turku</td>
+</tr>
+<tr>
+       <td>enterCharacter</td>
+       <td>vaadin=runcomvaadintestscomponentscustomfieldEmbeddedFormExample::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[1]/VForm[0]/VFormLayout[0]/VFormLayout$VFormLayoutTable[0]/VTextField[4]</td>
+       <td>Helsinki</td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>vaadin=runcomvaadintestscomponentscustomfieldEmbeddedFormExample::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[1]/VForm[0]/VHorizontalLayout[0]/ChildComponentContainer[0]/VHorizontalLayout[0]/ChildComponentContainer[1]/VButton[0]/domChild[0]/domChild[0]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>verifyValue</td>
+       <td>vaadin=runcomvaadintestscomponentscustomfieldEmbeddedFormExample::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[1]/VForm[0]/VFormLayout[0]/VFormLayout$VFormLayoutTable[0]/VTextField[4]</td>
+       <td>Helsinki</td>
+</tr>
+<tr>
+       <td>mouseClick</td>
+       <td>vaadin=runcomvaadintestscomponentscustomfieldEmbeddedFormExample::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VScrollTable[0]/domChild[1]/domChild[0]/domChild[1]/domChild[0]/domChild[1]/domChild[0]/domChild[0]</td>
+       <td>52,15</td>
+</tr>
+<tr>
+       <td>verifyValue</td>
+       <td>vaadin=runcomvaadintestscomponentscustomfieldEmbeddedFormExample::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[1]/VForm[0]/VFormLayout[0]/VFormLayout$VFormLayoutTable[0]/VTextField[4]</td>
+       <td>Turku</td>
+</tr>
+<tr>
+       <td>mouseClick</td>
+       <td>vaadin=runcomvaadintestscomponentscustomfieldEmbeddedFormExample::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VScrollTable[0]/domChild[1]/domChild[0]/domChild[1]/domChild[0]/domChild[0]/domChild[0]/domChild[0]</td>
+       <td>56,10</td>
+</tr>
+<tr>
+       <td>verifyValue</td>
+       <td>vaadin=runcomvaadintestscomponentscustomfieldEmbeddedFormExample::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[1]/VForm[0]/VFormLayout[0]/VFormLayout$VFormLayoutTable[0]/VTextField[4]</td>
+       <td>Helsinki</td>
+</tr>
+
+</tbody></table>
+</body>
+</html>
diff --git a/tests/testbench/com/vaadin/tests/components/customfield/EmbeddedFormExample.java b/tests/testbench/com/vaadin/tests/components/customfield/EmbeddedFormExample.java
new file mode 100644 (file)
index 0000000..aeb1984
--- /dev/null
@@ -0,0 +1,17 @@
+package com.vaadin.tests.components.customfield;
+
+public class EmbeddedFormExample extends AbstractNestedFormExample {
+
+    @Override
+    protected void setup() {
+        super.setup(true);
+    }
+
+    @Override
+    protected String getDescription() {
+        return "An address form embedded in a person form.\n"
+                + "The address fields are placed in the layout of the parent (person) form.\n"
+                + "Note that in many cases the same result can be achieved with a property that maps subfields to the top level.";
+    }
+
+}
diff --git a/tests/testbench/com/vaadin/tests/components/customfield/NestedFormExample.html b/tests/testbench/com/vaadin/tests/components/customfield/NestedFormExample.html
new file mode 100644 (file)
index 0000000..f9f5783
--- /dev/null
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<link rel="selenium.base" href="" />
+<title>NestedFormExample</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr><td rowspan="1" colspan="3">NestedFormExample</td></tr>
+</thead><tbody>
+<tr>
+       <td>open</td>
+       <td>/run/com.vaadin.tests.components.customfield.NestedFormExample?restartApplication</td>
+       <td></td>
+</tr>
+<tr>
+       <td>mouseClick</td>
+       <td>vaadin=runcomvaadintestscomponentscustomfieldNestedFormExample::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VScrollTable[0]/domChild[1]/domChild[0]/domChild[1]/domChild[0]/domChild[0]/domChild[0]/domChild[0]</td>
+       <td>33,9</td>
+</tr>
+<tr>
+       <td>screenCapture</td>
+       <td></td>
+       <td></td>
+</tr>
+<tr>
+       <td>enterCharacter</td>
+       <td>vaadin=runcomvaadintestscomponentscustomfieldNestedFormExample::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[1]/VForm[0]/VFormLayout[0]/VFormLayout$VFormLayoutTable[0]/VCustomComponent[0]/VForm[0]/VFormLayout[0]/VFormLayout$VFormLayoutTable[0]/VTextField[2]</td>
+       <td>Turkuaa</td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>vaadin=runcomvaadintestscomponentscustomfieldNestedFormExample::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[1]/VForm[0]/VHorizontalLayout[0]/ChildComponentContainer[0]/VHorizontalLayout[0]/ChildComponentContainer[0]/VButton[0]/domChild[0]/domChild[0]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>verifyValue</td>
+       <td>vaadin=runcomvaadintestscomponentscustomfieldNestedFormExample::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[1]/VForm[0]/VFormLayout[0]/VFormLayout$VFormLayoutTable[0]/VCustomComponent[0]/VForm[0]/VFormLayout[0]/VFormLayout$VFormLayoutTable[0]/VTextField[2]</td>
+       <td>Turku</td>
+</tr>
+<tr>
+       <td>enterCharacter</td>
+       <td>vaadin=runcomvaadintestscomponentscustomfieldNestedFormExample::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[1]/VForm[0]/VFormLayout[0]/VFormLayout$VFormLayoutTable[0]/VCustomComponent[0]/VForm[0]/VFormLayout[0]/VFormLayout$VFormLayoutTable[0]/VTextField[2]</td>
+       <td>Helsinki</td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>vaadin=runcomvaadintestscomponentscustomfieldNestedFormExample::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[1]/VForm[0]/VHorizontalLayout[0]/ChildComponentContainer[0]/VHorizontalLayout[0]/ChildComponentContainer[1]/VButton[0]/domChild[0]/domChild[0]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>verifyValue</td>
+       <td>vaadin=runcomvaadintestscomponentscustomfieldNestedFormExample::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[1]/VForm[0]/VFormLayout[0]/VFormLayout$VFormLayoutTable[0]/VCustomComponent[0]/VForm[0]/VFormLayout[0]/VFormLayout$VFormLayoutTable[0]/VTextField[2]</td>
+       <td>Helsinki</td>
+</tr>
+<tr>
+       <td>mouseClick</td>
+       <td>vaadin=runcomvaadintestscomponentscustomfieldNestedFormExample::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VScrollTable[0]/domChild[1]/domChild[0]/domChild[1]/domChild[0]/domChild[1]/domChild[0]/domChild[0]</td>
+       <td>56,14</td>
+</tr>
+<tr>
+       <td>verifyValue</td>
+       <td>vaadin=runcomvaadintestscomponentscustomfieldNestedFormExample::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[1]/VForm[0]/VFormLayout[0]/VFormLayout$VFormLayoutTable[0]/VCustomComponent[0]/VForm[0]/VFormLayout[0]/VFormLayout$VFormLayoutTable[0]/VTextField[2]</td>
+       <td>Turku</td>
+</tr>
+<tr>
+       <td>mouseClick</td>
+       <td>vaadin=runcomvaadintestscomponentscustomfieldNestedFormExample::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VScrollTable[0]/domChild[1]/domChild[0]/domChild[1]/domChild[0]/domChild[0]/domChild[0]/domChild[0]</td>
+       <td>30,9</td>
+</tr>
+<tr>
+       <td>verifyValue</td>
+       <td>vaadin=runcomvaadintestscomponentscustomfieldNestedFormExample::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[1]/VForm[0]/VFormLayout[0]/VFormLayout$VFormLayoutTable[0]/VCustomComponent[0]/VForm[0]/VFormLayout[0]/VFormLayout$VFormLayoutTable[0]/VTextField[2]</td>
+       <td>Helsinki</td>
+</tr>
+
+</tbody></table>
+</body>
+</html>
diff --git a/tests/testbench/com/vaadin/tests/components/customfield/NestedFormExample.java b/tests/testbench/com/vaadin/tests/components/customfield/NestedFormExample.java
new file mode 100644 (file)
index 0000000..91fb43f
--- /dev/null
@@ -0,0 +1,15 @@
+package com.vaadin.tests.components.customfield;
+
+public class NestedFormExample extends AbstractNestedFormExample {
+
+    @Override
+    protected void setup() {
+        super.setup(false);
+    }
+
+    @Override
+    protected String getDescription() {
+        return "An address form nested in a person form.";
+    }
+
+}
diff --git a/tests/testbench/com/vaadin/tests/components/customfield/NestedPersonForm.java b/tests/testbench/com/vaadin/tests/components/customfield/NestedPersonForm.java
new file mode 100644 (file)
index 0000000..e0a3b08
--- /dev/null
@@ -0,0 +1,94 @@
+package com.vaadin.tests.components.customfield;
+
+import java.util.Arrays;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.util.BeanItem;
+import com.vaadin.tests.util.Person;
+import com.vaadin.ui.Alignment;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.DefaultFieldFactory;
+import com.vaadin.ui.Field;
+import com.vaadin.ui.Form;
+import com.vaadin.ui.HorizontalLayout;
+
+/**
+ * Example of nested forms
+ */
+public class NestedPersonForm extends Form {
+    private BeanItem<Person> beanItem;
+    private final boolean embeddedAddress;
+
+    /**
+     * Creates a person form which contains nested form for the persons address
+     */
+    public NestedPersonForm(Person person, boolean embeddedAddress) {
+        this.embeddedAddress = embeddedAddress;
+
+        beanItem = new BeanItem<Person>(person);
+        setCaption("Update person details");
+        setWriteThrough(false);
+        setFormFieldFactory(new PersonFieldFactory());
+        // set the data source and the visible fields
+        // Note that if the nested form is the first or last field in the parent
+        // form, styles from the parent (padding, ...) may leak to its contents.
+        setItemDataSource(beanItem, Arrays.asList("firstName", "lastName",
+                "address", "email", "phoneNumber"));
+        getFooter().addComponent(getButtonsLayout());
+        getFooter().setMargin(false, false, true, true);
+    }
+
+    /**
+     * Get apply and discard button in the layout
+     */
+    private Component getButtonsLayout() {
+        HorizontalLayout buttons = new HorizontalLayout();
+        buttons.setSpacing(true);
+        Button discardChanges = new Button("Discard changes",
+                new Button.ClickListener() {
+                    public void buttonClick(ClickEvent event) {
+                        NestedPersonForm.this.discard();
+                    }
+                });
+        buttons.addComponent(discardChanges);
+        buttons.setComponentAlignment(discardChanges, Alignment.MIDDLE_LEFT);
+
+        Button apply = new Button("Apply", new Button.ClickListener() {
+            public void buttonClick(ClickEvent event) {
+                try {
+                    NestedPersonForm.this.commit();
+                } catch (Exception e) {
+                    // Ignored, we'll let the Form handle the errors
+                }
+            }
+        });
+        buttons.addComponent(apply);
+        return buttons;
+    }
+
+    /**
+     * Field factory for person form
+     */
+    private class PersonFieldFactory extends DefaultFieldFactory {
+        // reuse the address field - required by EmbeddedForm
+        private AddressField addressField;
+
+        @Override
+        public Field createField(Item item, Object propertyId,
+                Component uiContext) {
+            Field f = super.createField(item, propertyId, uiContext);
+            if ("address".equals(propertyId)) {
+                // create a custom field for the Address object
+                if (addressField == null) {
+                    Form form = (embeddedAddress && uiContext instanceof Form) ? (Form) uiContext
+                            : null;
+                    addressField = new AddressField(form);
+                }
+                f = addressField;
+            }
+            return f;
+        }
+    }
+}