Fixes #8622 Fixes #8623tags/8.1.0.alpha1
@@ -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(); | |||
} | |||
} |
@@ -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 |
@@ -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 |
@@ -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(); | |||
} | |||
} |
@@ -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()); | |||
} | |||
} | |||
} |
@@ -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 { | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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)); | |||
} | |||
} |