flow icon indicating copy to clipboard operation
flow copied to clipboard

LitTemplate + <vaadin-checkbox>: Checked state not reliably updated

Open Frettman opened this issue 2 years ago • 4 comments

Description of the bug

For a Grid column I'm using a LitTemplate containing a . (I need the regular selection for a details area, so the intention is to use this new column as a separate way to select multiple items). My problem basically is, when the checkbox has been checked by clicking on it, I can't remove the check from the server-side by calling 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

Frettman avatar Feb 21 '23 14:02 Frettman

The behaviour described above looks similar to https://github.com/vaadin/platform/issues/3640, but the opposite - refreshAll preserves the value of the checkbox.

mshabarov avatar Feb 28 '23 11:02 mshabarov

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

  1. tick "Prevent checked"
  2. check some items
  3. 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).

mshabarov avatar Sep 13 '23 07:09 mshabarov

@yuriy-fix FYI, this might be an issue in Grid or LitRenderer.

mshabarov avatar Sep 13 '23 07:09 mshabarov

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!

ivan-slavchev avatar Oct 15 '24 06:10 ivan-slavchev