Browse Source

Initial implementation of ComponentRenderer for Grid (#8743)

Fixes #8622 
Fixes #8623
tags/8.1.0.alpha1
Teemu Suo-Anttila 7 years ago
parent
commit
761c94ab2e

+ 63
- 0
client/src/main/java/com/vaadin/client/connectors/ComponentRendererConnector.java View File

@@ -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();
}
}

+ 41
- 2
documentation/components/components-grid.asciidoc View 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

+ 39
- 0
server/src/main/java/com/vaadin/ui/Grid.java View 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

+ 64
- 0
server/src/main/java/com/vaadin/ui/renderers/ComponentRenderer.java View File

@@ -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();
}
}

+ 90
- 0
server/src/test/java/com/vaadin/tests/components/grid/GridComponentRendererTest.java View File

@@ -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());
}
}
}

+ 26
- 0
shared/src/main/java/com/vaadin/shared/ui/grid/renderers/ComponentRendererState.java View File

@@ -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 {

}

+ 68
- 0
uitest/src/main/java/com/vaadin/tests/components/grid/GridComponents.java View File

@@ -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);

}

}

+ 94
- 0
uitest/src/test/java/com/vaadin/tests/components/grid/GridComponentsTest.java View File

@@ -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));
}
}

Loading…
Cancel
Save