summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTeemu Suo-Anttila <tsuoanttila@users.noreply.github.com>2017-03-09 09:29:51 +0200
committerPekka Hyvönen <pekka@vaadin.com>2017-03-09 09:29:51 +0200
commit761c94ab2e1019f7d83d4e8d63254de1ee591d75 (patch)
treebd167811a86c0c666d321bdbfdb57dea980cb942
parent264ee7696568827815604f1e22ce7e330775b3ce (diff)
downloadvaadin-framework-761c94ab2e1019f7d83d4e8d63254de1ee591d75.tar.gz
vaadin-framework-761c94ab2e1019f7d83d4e8d63254de1ee591d75.zip
Initial implementation of ComponentRenderer for Grid (#8743)
Fixes #8622 Fixes #8623
-rw-r--r--client/src/main/java/com/vaadin/client/connectors/ComponentRendererConnector.java63
-rw-r--r--documentation/components/components-grid.asciidoc43
-rw-r--r--server/src/main/java/com/vaadin/ui/Grid.java39
-rw-r--r--server/src/main/java/com/vaadin/ui/renderers/ComponentRenderer.java64
-rw-r--r--server/src/test/java/com/vaadin/tests/components/grid/GridComponentRendererTest.java90
-rw-r--r--shared/src/main/java/com/vaadin/shared/ui/grid/renderers/ComponentRendererState.java26
-rw-r--r--uitest/src/main/java/com/vaadin/tests/components/grid/GridComponents.java68
-rw-r--r--uitest/src/test/java/com/vaadin/tests/components/grid/GridComponentsTest.java94
8 files changed, 485 insertions, 2 deletions
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
index 0000000000..ef14c5024e
--- /dev/null
+++ b/client/src/main/java/com/vaadin/client/connectors/ComponentRendererConnector.java
@@ -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();
+ }
+}
diff --git a/documentation/components/components-grid.asciidoc b/documentation/components/components-grid.asciidoc
index 957a9a7fb4..7fd6332dd6 100644
--- a/documentation/components/components-grid.asciidoc
+++ b/documentation/components/components-grid.asciidoc
@@ -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
diff --git a/server/src/main/java/com/vaadin/ui/Grid.java b/server/src/main/java/com/vaadin/ui/Grid.java
index 5d03825c6f..d0b908a956 100644
--- a/server/src/main/java/com/vaadin/ui/Grid.java
+++ b/server/src/main/java/com/vaadin/ui/Grid.java
@@ -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
index 0000000000..1053907723
--- /dev/null
+++ b/server/src/main/java/com/vaadin/ui/renderers/ComponentRenderer.java
@@ -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
index 0000000000..9e6eb33704
--- /dev/null
+++ b/server/src/test/java/com/vaadin/tests/components/grid/GridComponentRendererTest.java
@@ -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
index 0000000000..fd1e7db66a
--- /dev/null
+++ b/shared/src/main/java/com/vaadin/shared/ui/grid/renderers/ComponentRendererState.java
@@ -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
index 0000000000..2eb48f7697
--- /dev/null
+++ b/uitest/src/main/java/com/vaadin/tests/components/grid/GridComponents.java
@@ -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
index 0000000000..89226c147e
--- /dev/null
+++ b/uitest/src/test/java/com/vaadin/tests/components/grid/GridComponentsTest.java
@@ -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));
+ }
+}