LitTemplate + <vaadin-checkbox>: Checked state not reliably updated
Description of the bug
For a Grid column I'm using a LitTemplate containing a refreshItem/refreshAll. If the checkbox has been checked from the server-side via refreshItem/refreshAll (even if it already appears checked), I can then uncheck it in the same way.
I'm not sure where the problem exactly lies, but I couldn't reproduce something similar with plain Lit, so I guess it might be some interaction with Flow (LitTemplate, Grid, ...) ...
I created a standalone view to experiment and demonstrate my issue (see below).
Expected behavior
Refreshing an item in a Grid should reliably update the LitTemplate.
Minimal reproducible example
In the following view, check any item(s) by clicking on the checkbox. Then click on "Uncheck all items" to see that this has no effect. One other observation: If you check all items via "Check all items" you can check/uncheck by clicking on the checkbox as much as you want and "Uncheck all items" still works. But after that we're back to step 1.
package com.example.application.views;
import com.vaadin.flow.component.Composite;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.data.renderer.LitRenderer;
import com.vaadin.flow.router.Route;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Route("grid-lit")
public class GridLitView
extends
Composite<VerticalLayout>
{
private List<String> items = List.of("Item 1", "Item 2", "Item 3", "Item 4", "Item 5");
private Set<String> checkedItems = new HashSet<>();
public GridLitView()
{
VerticalLayout content = getContent();
Grid<String> grid = new Grid<>();
content.add(grid);
Checkbox preventCheck = new Checkbox("Prevent checking");
content.add(preventCheck);
Button checkButton = new Button("Check all items");
content.add(checkButton);
Button uncheckButton = new Button("Uncheck all items");
content.add(uncheckButton);
Button refreshButton = new Button("Refresh all items");
content.add(refreshButton);
grid.addColumn(LitRenderer.<String> of(
"<vaadin-checkbox ?checked=${item.checked} @change=${(e) => handleChecked(e.target.checked)}></vaadin-checkbox>")
.withProperty("checked", item ->
{
boolean checked = checkedItems.contains(item);
if (checked)
System.out.println(item + " is checked");
else
System.out.println(item + " is NOT checked");
return checked;
}) //
.withFunction("handleChecked", (item, args) ->
{
if (args.getBoolean(0))
{
if (preventCheck.getValue())
// doesn't work if checkbox has been checked by clicking on it
grid.getListDataView().refreshItem(item);
else
checkedItems.add(item);
}
else
{
checkedItems.remove(item);
}
})) //
.setKey("checked") //
.setAutoWidth(true).setFlexGrow(0);
grid.addColumn(item -> item) //
.setHeader("Name") //
.setFlexGrow(1);
checkButton.addClickListener(event ->
{
checkedItems.addAll(items);
grid.getListDataView().refreshAll();
});
uncheckButton.addClickListener(event ->
{
checkedItems.clear();
grid.getListDataView().refreshAll();
});
refreshButton.addClickListener(event ->
{
grid.getListDataView().refreshAll();
});
grid.setItems(items);
}
}
Versions
Vaadin: 23.3.5 and 24.0.0.beta2 Flow: 23.3.3 and 24.0.0.beta2 Java: Eclipse Adoptium 11.0.16 OS: amd64 Windows 10 10.0 Browser: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/110.0
The behaviour described above looks similar to https://github.com/vaadin/platform/issues/3640, but the opposite - refreshAll preserves the value of the checkbox.
@Frettman first of all sorry for answering too late for your request.
I tried your example locally and I didn't get why do you use extra collection checkedItems?
This doesn't needed IMO and probably lead to a bugs in the app.
DataProvider operates with the items assigned to it, it doesn't know about your internal cache checkedItems, so I believe it's better if you maintain the checked items directly with the collection assigned to the DataProvider.
For doing it, I added Item class with a value and checked flag:
public static class Item {
String value;
boolean checked;
public Item(String value) {
this.value = value;
this.checked = false;
}
public Item(String value, boolean checked) {
this.value = value;
this.checked = checked;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public boolean isChecked() {
return checked;
}
public void setChecked(boolean checked) {
this.checked = checked;
}
@Override
public String toString() {
return value;
}
}
Then I reimplemented the logic for checking/unchecking items, like this:
public GridLitView()
{
VerticalLayout content = getContent();
Grid<Item> grid = new Grid<>();
content.add(grid);
Checkbox preventCheck = new Checkbox("Prevent checking");
content.add(preventCheck);
Button checkButton = new Button("Check all items");
content.add(checkButton);
Button uncheckButton = new Button("Uncheck all items");
content.add(uncheckButton);
Button refreshButton = new Button("Refresh all items");
content.add(refreshButton);
grid.addColumn(LitRenderer.<Item> of(
"<vaadin-checkbox ?checked=${item.checked} @change=${(e) => handleChecked(e.target.checked)}></vaadin-checkbox>")
.withProperty("checked", Item::isChecked) //
.withFunction("handleChecked", (item, args) ->
{
if (!preventCheck.getValue()) {
item.setChecked(args.getBoolean(0));
grid.getDataProvider().refreshItem(item);
}
})) //
.setKey("checked") //
.setAutoWidth(true).setFlexGrow(0);
grid.addColumn(item -> item) //
.setHeader("Name") //
.setFlexGrow(1);
checkButton.addClickListener(event ->
{
grid.getListDataView().getItems().forEach(item -> item.setChecked(true));
grid.getListDataView().refreshAll();
});
uncheckButton.addClickListener(event ->
{
grid.getListDataView().getItems().forEach(item -> item.setChecked(false));
grid.getListDataView().refreshAll();
});
refreshButton.addClickListener(event ->
{
grid.getListDataView().refreshAll();
});
List<Item> items = List.of(new Item("Item 1", true), new Item("Item 2", true), new Item("Item 3"), new Item("Item 4"), new Item("Item 5"));
grid.setItems(items);
}
Now I get the check all and uncheck all buttons working as expected. However, refresh all button does nothing if I:
- tick "Prevent checked"
- check some items
- click on "Refresh All Items"
I can see that the items on the server are not updated, because I prevent it, also I can see that the data provider fetched the items from collection and send them in the UIDL as expected.
But for some reason the checkboxes state are not reverted back to the state before I ticked "Prevent checked". This I treat as a bug here.
In the same time, refreshAll works fine when I do change the items on the server (e.g. check all and uncheck all both work).
@yuriy-fix FYI, this might be an issue in Grid or LitRenderer.
Is there some workaround regarding this bug? I'm experiencing the same issue, even with a native checkbox input (so I would also assume it's something related to the LitRenderer itself) which means that at the moment there is no performant way to put checkboxes in grid. Adding Checkbox components with a component renderer becomes a performance issue in case of multiple columns or many rows. Any alternative solution would be appreciated, thank you!