diff options
author | Teemu Suo-Anttila <tsuoanttila@users.noreply.github.com> | 2017-03-09 09:29:51 +0200 |
---|---|---|
committer | Pekka Hyvönen <pekka@vaadin.com> | 2017-03-09 09:29:51 +0200 |
commit | 761c94ab2e1019f7d83d4e8d63254de1ee591d75 (patch) | |
tree | bd167811a86c0c666d321bdbfdb57dea980cb942 | |
parent | 264ee7696568827815604f1e22ce7e330775b3ce (diff) | |
download | vaadin-framework-761c94ab2e1019f7d83d4e8d63254de1ee591d75.tar.gz vaadin-framework-761c94ab2e1019f7d83d4e8d63254de1ee591d75.zip |
Initial implementation of ComponentRenderer for Grid (#8743)
Fixes #8622
Fixes #8623
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)); + } +} |