]> source.dussan.org Git - vaadin-framework.git/commitdiff
Initial implementation of ComponentRenderer for Grid (#8743)
authorTeemu Suo-Anttila <tsuoanttila@users.noreply.github.com>
Thu, 9 Mar 2017 07:29:51 +0000 (09:29 +0200)
committerPekka Hyvönen <pekka@vaadin.com>
Thu, 9 Mar 2017 07:29:51 +0000 (09:29 +0200)
Fixes #8622
Fixes #8623

client/src/main/java/com/vaadin/client/connectors/ComponentRendererConnector.java [new file with mode: 0644]
documentation/components/components-grid.asciidoc
server/src/main/java/com/vaadin/ui/Grid.java
server/src/main/java/com/vaadin/ui/renderers/ComponentRenderer.java [new file with mode: 0644]
server/src/test/java/com/vaadin/tests/components/grid/GridComponentRendererTest.java [new file with mode: 0644]
shared/src/main/java/com/vaadin/shared/ui/grid/renderers/ComponentRendererState.java [new file with mode: 0644]
uitest/src/main/java/com/vaadin/tests/components/grid/GridComponents.java [new file with mode: 0644]
uitest/src/test/java/com/vaadin/tests/components/grid/GridComponentsTest.java [new file with mode: 0644]

diff --git a/client/src/main/java/com/vaadin/client/connectors/ComponentRendererConnector.java b/client/src/main/java/com/vaadin/client/connectors/ComponentRendererConnector.java
new file mode 100644 (file)
index 0000000..ef14c50
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.connectors;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.vaadin.client.ComponentConnector;
+import com.vaadin.client.ConnectorMap;
+import com.vaadin.client.renderers.Renderer;
+import com.vaadin.client.renderers.WidgetRenderer;
+import com.vaadin.client.widget.grid.RendererCellReference;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.grid.renderers.ComponentRendererState;
+import com.vaadin.ui.renderers.ComponentRenderer;
+
+/**
+ * Connector for {@link ComponentRenderer}. The renderer wraps the component
+ * into a {@link SimplePanel} to allow handling events correctly.
+ *
+ * @author Vaadin Ltd
+ * @since 8.1
+ */
+@Connect(ComponentRenderer.class)
+public class ComponentRendererConnector
+        extends AbstractRendererConnector<String> {
+
+    @Override
+    protected Renderer<String> createRenderer() {
+        return new WidgetRenderer<String, SimplePanel>() {
+
+            @Override
+            public SimplePanel createWidget() {
+                return GWT.create(SimplePanel.class);
+            }
+
+            @Override
+            public void render(RendererCellReference cell, String connectorId,
+                    SimplePanel widget) {
+                ComponentConnector connector = (ComponentConnector) ConnectorMap
+                        .get(getConnection()).getConnector(connectorId);
+                widget.setWidget(connector.getWidget());
+            }
+        };
+    }
+
+    @Override
+    public ComponentRendererState getState() {
+        return (ComponentRendererState) super.getState();
+    }
+}
index 957a9a7fb4d64f8f591fdbdcdb53ec48afe8c58a..7fd6332dd6ca15d888cfd9e4c30ee5540866bdd9 100644 (file)
@@ -448,7 +448,7 @@ The column type must be a [interfacename]#Resource#, as described in
 +
 [source, java]
 ----
-Column<ThemeResource> imageColumn = grid.addColumn(
+Column<Person, ThemeResource> imageColumn = grid.addColumn(
     p -> new ThemeResource("img/"+p.getLastname()+".jpg"),
     new ImageRenderer());
 ----
@@ -478,7 +478,7 @@ Set the renderer in the [classname]#Grid.Column# object:
 +
 [source, java]
 ----
-Column<String> htmlColumn grid.addColumn(person ->
+Column<Person, String> htmlColumn grid.addColumn(person ->
       "<a href='" + person.getDetailsUrl() + "' target='_top'>info</a>",
       new HtmlRenderer());
 ----
@@ -502,6 +502,45 @@ Column<Integer> birthYear = grid.addColumn(Person::getBirthYear,
 [classname]#ProgressBarRenderer#:: Renders a progress bar in a column with a [classname]#Double# type. The value
 must be between 0.0 and 1.0.
 
+[classname]#ComponentRenderer#:: Renders a Vaadin [classname]#Component# in a column. Since components
+are quite complex, the [classname]#ComponentRenderer# comes with possible performance issues.
+To use it efficiently you should use as few nested components as possible.
+
++
+Use [classname]#Button# in [classname]#Grid#:
++
+----
+grid.addColumn(person -> {
+      Button button = new Button("Click me!");
+      button.addClickListener(click -> 
+            Notification.show("Clicked: " + person.toString()));
+      return button;
+}, new ComponentRenderer());
+----
+
+Components will occasionally be generated again during runtime. If you have a state in your 
+component and not in the data object, you need to handle storing it yourself. Below is a simple
+example on how to achieve this.
+
++
+Store a [classname]#TextField# with changed value.
++
+----
+Map<Person, TextField> textFields = new HashMap<>();
+grid.addColumn(person -> {
+      // Check for existing text field
+      if (textFields.containsKey(person)) {
+            return textFields.get(person);
+      }
+      // Create a new one
+      TextField textField = new TextField();
+      textField.setValue(person.getLastname());
+      // Store the text field when user updates the value
+      textField.addValueChangeListener(change -> 
+            textFields.put(person, textField));
+      return textField;
+      }, new ComponentRenderer());
+----
 
 [[components.grid.renderer.custom]]
 === Custom Renderers
index 5d03825c6f6014de2523d0d18d51831b37a30c01..d0b908a956e16b1f1fc7425b5274dc0fca4331cd 100644 (file)
@@ -115,6 +115,7 @@ import com.vaadin.ui.declarative.DesignContext;
 import com.vaadin.ui.declarative.DesignException;
 import com.vaadin.ui.declarative.DesignFormatter;
 import com.vaadin.ui.renderers.AbstractRenderer;
+import com.vaadin.ui.renderers.ComponentRenderer;
 import com.vaadin.ui.renderers.HtmlRenderer;
 import com.vaadin.ui.renderers.Renderer;
 import com.vaadin.ui.renderers.TextRenderer;
@@ -831,6 +832,7 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents,
         private DescriptionGenerator<T> descriptionGenerator;
 
         private Binding<T, ?> editorBinding;
+        private Map<T, Component> activeComponents = new HashMap<>();
 
         private String userId;
 
@@ -961,6 +963,11 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents,
 
             V providerValue = valueProvider.apply(data);
 
+            // Make Grid track components.
+            if (renderer instanceof ComponentRenderer
+                    && providerValue instanceof Component) {
+                addComponent(data, (Component) providerValue);
+            }
             JsonValue rendererValue = renderer.encode(providerValue);
 
             obj.put(communicationId, rendererValue);
@@ -981,6 +988,38 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents,
             }
         }
 
+        private void addComponent(T data, Component component) {
+            if (activeComponents.containsKey(data)) {
+                if (activeComponents.get(data).equals(component)) {
+                    // Reusing old component
+                    return;
+                }
+                removeComponent(data);
+            }
+            activeComponents.put(data, component);
+            addComponentToGrid(component);
+        }
+
+        @Override
+        public void destroyData(T item) {
+            removeComponent(item);
+        }
+
+        private void removeComponent(T item) {
+            Component component = activeComponents.remove(item);
+            if (component != null) {
+                removeComponentFromGrid(component);
+            }
+        }
+
+        @Override
+        public void destroyAllData() {
+            // Make a defensive copy of keys, as the map gets cleared when
+            // removing components.
+            new HashSet<>(activeComponents.keySet())
+                    .forEach(this::removeComponent);
+        }
+
         /**
          * Gets a data object with the given key from the given JsonObject. If
          * there is no object with the key, this method creates a new
diff --git a/server/src/main/java/com/vaadin/ui/renderers/ComponentRenderer.java b/server/src/main/java/com/vaadin/ui/renderers/ComponentRenderer.java
new file mode 100644 (file)
index 0000000..1053907
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.renderers;
+
+import com.vaadin.shared.ui.grid.renderers.ComponentRendererState;
+import com.vaadin.ui.Component;
+
+import elemental.json.Json;
+import elemental.json.JsonValue;
+
+/**
+ * A renderer for presenting Components.
+ * <p>
+ * <strong>Note:</strong> The use of ComponentRenderer causes the Grid to
+ * generate components for all items currently available in the client-side.
+ * This means that a number of components is always generated and sent to the
+ * client. Using complex structures of many nested components might be heavy to
+ * generate and store, which will lead to performance problems.
+ * <p>
+ * <strong>Note:</strong> Components will occasionally be generated again during
+ * runtime e.g. when selection changes. If your component has an internal state
+ * that is not stored into the object, you should reuse the same component
+ * instances.
+ *
+ * @author Vaadin Ltd
+ * @since 8.1
+ */
+public class ComponentRenderer extends AbstractRenderer<Object, Component> {
+
+    /**
+     * Constructor for ComponentRenderer.
+     */
+    public ComponentRenderer() {
+        super(Component.class);
+    }
+
+    @Override
+    public JsonValue encode(Component value) {
+        return Json.create(value.getConnectorId());
+    }
+
+    @Override
+    protected ComponentRendererState getState(boolean markAsDirty) {
+        return (ComponentRendererState) super.getState(markAsDirty);
+    }
+
+    @Override
+    protected ComponentRendererState getState() {
+        return (ComponentRendererState) super.getState();
+    }
+}
diff --git a/server/src/test/java/com/vaadin/tests/components/grid/GridComponentRendererTest.java b/server/src/test/java/com/vaadin/tests/components/grid/GridComponentRendererTest.java
new file mode 100644 (file)
index 0000000..9e6eb33
--- /dev/null
@@ -0,0 +1,90 @@
+package com.vaadin.tests.components.grid;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Future;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.provider.DataProvider;
+import com.vaadin.server.VaadinSession;
+import com.vaadin.tests.data.bean.Person;
+import com.vaadin.tests.util.AlwaysLockedVaadinSession;
+import com.vaadin.tests.util.MockUI;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.renderers.ComponentRenderer;
+
+/**
+ * Test to validate clean detaching in Grid with ComponentRenderer.
+ */
+public class GridComponentRendererTest {
+
+    private static final Person PERSON = Person.createTestPerson1();
+    private Grid<Person> grid;
+    private List<Person> backend;
+    private DataProvider<Person, ?> dataProvider;
+    private Label testComponent;
+    private Label oldComponent;
+
+    @Before
+    public void setUp() {
+        VaadinSession.setCurrent(new AlwaysLockedVaadinSession(null));
+        backend = new ArrayList<>();
+        backend.add(PERSON);
+        dataProvider = DataProvider.ofCollection(backend);
+        grid = new Grid<>();
+        grid.setDataProvider(dataProvider);
+        grid.addColumn(p -> {
+            oldComponent = testComponent;
+            testComponent = new Label();
+            return testComponent;
+        }, new ComponentRenderer());
+        new MockUI() {
+            @Override
+            public Future<Void> access(Runnable runnable) {
+                runnable.run();
+                return null;
+            };
+        }.setContent(grid);
+    }
+
+    @Test
+    public void testComponentChangeOnRefresh() {
+        generateDataForClient(true);
+        dataProvider.refreshItem(PERSON);
+        generateDataForClient(false);
+        Assert.assertNotNull("Old component should exist.", oldComponent);
+    }
+
+    @Test
+    public void testComponentChangeOnSelection() {
+        generateDataForClient(true);
+        grid.select(PERSON);
+        generateDataForClient(false);
+        Assert.assertNotNull("Old component should exist.", oldComponent);
+    }
+
+    @Test
+    public void testComponentChangeOnDataProviderChange() {
+        generateDataForClient(true);
+        grid.setItems(PERSON);
+        Assert.assertEquals(
+                "Test component was not detached on DataProvider change.", null,
+                testComponent.getParent());
+    }
+
+    private void generateDataForClient(boolean initial) {
+        grid.getDataCommunicator().beforeClientResponse(initial);
+        if (testComponent != null) {
+            Assert.assertEquals("New component was not attached.", grid,
+                    testComponent.getParent());
+        }
+        if (oldComponent != null) {
+            Assert.assertEquals("Old component was not detached.", null,
+                    oldComponent.getParent());
+        }
+    }
+}
diff --git a/shared/src/main/java/com/vaadin/shared/ui/grid/renderers/ComponentRendererState.java b/shared/src/main/java/com/vaadin/shared/ui/grid/renderers/ComponentRendererState.java
new file mode 100644 (file)
index 0000000..fd1e7db
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.shared.ui.grid.renderers;
+
+/**
+ * Shared state for ComponentRenderer.
+ *
+ * @author Vaadin
+ * @since 8.1
+ */
+public class ComponentRendererState extends AbstractRendererState {
+
+}
diff --git a/uitest/src/main/java/com/vaadin/tests/components/grid/GridComponents.java b/uitest/src/main/java/com/vaadin/tests/components/grid/GridComponents.java
new file mode 100644 (file)
index 0000000..2eb48f7
--- /dev/null
@@ -0,0 +1,68 @@
+package com.vaadin.tests.components.grid;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.IntStream;
+
+import com.vaadin.annotations.Widgetset;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.shared.ui.ValueChangeMode;
+import com.vaadin.tests.components.AbstractTestUIWithLog;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.Notification;
+import com.vaadin.ui.Notification.Type;
+import com.vaadin.ui.TextField;
+import com.vaadin.ui.renderers.ComponentRenderer;
+
+@Widgetset("com.vaadin.DefaultWidgetSet")
+public class GridComponents extends AbstractTestUIWithLog {
+
+    private Map<String, TextField> textFields = new HashMap<>();
+    private int counter = 0;
+
+    @Override
+    protected void setup(VaadinRequest request) {
+        Grid<String> grid = new Grid<String>();
+        grid.addColumn(string -> new Label(string), new ComponentRenderer());
+        grid.addColumn(string -> {
+            if (textFields.containsKey(string)) {
+                log("Reusing old text field for: " + string);
+                return textFields.get(string);
+            }
+
+            TextField textField = new TextField();
+            textField.setValue(string);
+            // Make sure all changes are sent immediately
+            textField.setValueChangeMode(ValueChangeMode.EAGER);
+            textField.addValueChangeListener(e -> {
+                // Value of text field edited by user, store
+                textFields.put(string, textField);
+            });
+            return textField;
+        }, new ComponentRenderer());
+        grid.addColumn(string -> {
+            Button button = new Button("Click Me!",
+                    e -> Notification.show(
+                            "Clicked button on row for: " + string,
+                            Type.WARNING_MESSAGE));
+            button.setId(string.replace(' ', '_').toLowerCase());
+            return button;
+        }, new ComponentRenderer());
+
+        addComponent(grid);
+        grid.setSizeFull();
+
+        Button resetData = new Button("Reset data", e -> {
+            grid.setItems(IntStream.range(0, 1000).boxed()
+                    .map(i -> "Row " + (i + (counter * 1000))));
+            textFields.clear();
+            ++counter;
+        });
+        resetData.click();
+        addComponent(resetData);
+
+    }
+
+}
diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/GridComponentsTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/GridComponentsTest.java
new file mode 100644 (file)
index 0000000..89226c1
--- /dev/null
@@ -0,0 +1,94 @@
+package com.vaadin.tests.components.grid;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.openqa.selenium.WebElement;
+
+import com.vaadin.testbench.By;
+import com.vaadin.testbench.elements.ButtonElement;
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.testbench.elements.GridElement.GridRowElement;
+import com.vaadin.testbench.elements.NotificationElement;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+public class GridComponentsTest extends MultiBrowserTest {
+
+    @Test
+    public void testReuseTextFieldOnScroll() {
+        openTestURL();
+        GridElement grid = $(GridElement.class).first();
+        editTextFieldInCell(grid, 0, 1);
+        // Scroll out of view port
+        grid.getRow(900);
+        // Scroll back
+        grid.getRow(0);
+
+        WebElement textField = grid.getCell(0, 1)
+                .findElement(By.tagName("input"));
+        Assert.assertEquals("TextField value was reset", "Foo",
+                textField.getAttribute("value"));
+        Assert.assertTrue("No mention in the log",
+                logContainsText("1. Reusing old text field for: Row 0"));
+    }
+
+    @Test
+    public void testReuseTextFieldOnSelect() {
+        openTestURL();
+        GridElement grid = $(GridElement.class).first();
+        editTextFieldInCell(grid, 1, 1);
+        // Select row
+        grid.getCell(1, 1).click(1, 1);
+
+        WebElement textField = grid.getCell(1, 1)
+                .findElement(By.tagName("input"));
+        Assert.assertEquals("TextField value was reset", "Foo",
+                textField.getAttribute("value"));
+        Assert.assertTrue("No mention in the log",
+                logContainsText("1. Reusing old text field for: Row 1"));
+    }
+
+    @Test
+    public void testReplaceData() {
+        openTestURL();
+        assertRowExists(5, "Row 5");
+        $(ButtonElement.class).caption("Reset data").first().click();
+        assertRowExists(5, "Row 1005");
+    }
+
+    private void editTextFieldInCell(GridElement grid, int row, int col) {
+        WebElement textField = grid.getCell(row, col)
+                .findElement(By.tagName("input"));
+        textField.clear();
+        textField.sendKeys("Foo");
+    }
+
+    @Test
+    public void testRow5() {
+        openTestURL();
+        assertRowExists(5, "Row 5");
+    }
+
+    @Test
+    public void testRow0() {
+        openTestURL();
+        assertRowExists(0, "Row 0");
+    }
+
+    @Test
+    public void testRow999() {
+        openTestURL();
+        assertRowExists(999, "Row 999");
+    }
+
+    private void assertRowExists(int i, String string) {
+        GridRowElement row = $(GridElement.class).first().getRow(i);
+        Assert.assertEquals("Label text did not match", string,
+                row.getCell(0).getText());
+        row.findElement(By.id(string.replace(' ', '_').toLowerCase())).click();
+        // IE 11 is slow, need to wait for the notification.
+        waitUntil(driver -> isElementPresent(NotificationElement.class), 10);
+        Assert.assertTrue("Notification should contain given text",
+                $(NotificationElement.class).first().getText()
+                        .contains(string));
+    }
+}