Vaadin-grid: TreeGrid should support expand/collapse trigger on icon click only

Created on 6 Oct 2020  路  4Comments  路  Source: vaadin/vaadin-grid

When using a TreeGrid as a normal tree, it is a bit strange that expand/collapse is triggered when clicking on the expand/collapse icon (v, >) and the cell's content. So when the user clicks an element in the tree which has children, also expand or collapse occurs.

It would be nice if expand/collapse could be triggered only when clicking on the expand/collapse icon (v, >).
Maybe an option to specify this on TreeGrid level would be a good solution.

enhancement flow

Most helpful comment

Another workaround here

All 4 comments

Hi,
Here is some explanation how the expand/collapse is working:
The <vaadin-grid-tree-toggle> (the icon to expand/collapse) toggles the expand state.
By default it contains the content of the column.
--> if you click on this component or it's content, it toggles the expand state.

The workaround moves the content of the <vaadin-grid-tree-toggle> outside.
So you column contains: <vaadin-grid-tree-toggle></vaadin-grid-tree-toggle>your content instead of <vaadin-grid-tree-toggle>your content</vaadin-grid-tree-toggle>.

Unfortunately, the workaround contains a lot of code because you cannot extend easily the HierarchyColumnComponentRenderer. In fact, it's only real 2 changes from the official Vaadin code:

public class GridHelpers {

    public static <T> Grid.Column<T> addNoClickHierarchyColumn(TreeGrid<T> grid, ValueProvider<T, ?> valueProvider) {
        Grid.Column<T> column = grid.addColumn(TemplateRenderer
            .<T> of("<vaadin-grid-tree-toggle "
                + "leaf='[[item.leaf]]' expanded='{{expanded}}' level='[[level]]'>"
                + "</vaadin-grid-tree-toggle>[[item.name]]") // change the original code
            .withProperty("leaf",
                item -> !grid.getDataCommunicator().hasChildren(item))
            .withProperty("name",
                value -> String.valueOf(valueProvider.apply(value))));
        final SerializableComparator<T> comparator =
            (a, b) -> grid.compareMaybeComparables(valueProvider.apply(a),
                valueProvider.apply(b));
        column.setComparator(comparator);

        return column;
    }

    public static  <T, V extends Component> Grid.Column<T> addNoClickComponentHierarchyColumn(
        TreeGrid<T> grid, ValueProvider<T, V> componentProvider) {
        return grid.addColumn(new NoClickHierarchyColumnComponentRenderer<V, T>(
            componentProvider).withProperty("leaf",
            item -> !grid.getDataCommunicator().hasChildren(item)));
    }


    public static class NoClickHierarchyColumnComponentRenderer<COMPONENT extends Component, SOURCE>
        extends ComponentRenderer<COMPONENT, SOURCE> {

        public NoClickHierarchyColumnComponentRenderer(
            ValueProvider<SOURCE, COMPONENT> componentProvider) {
            super(componentProvider);
        }

        public NoClickHierarchyColumnComponentRenderer<COMPONENT, SOURCE> withProperty(
            String property, ValueProvider<SOURCE, ?> provider) {
            setProperty(property, provider);
            return this;
        }

        @Override
        public Rendering<SOURCE> render(Element container,
                                        DataKeyMapper<SOURCE> keyMapper, Element contentTemplate) {

            ComponentRendering rendering = new ComponentRendering(
                keyMapper == null ? null : keyMapper::key);
            rendering.setTemplateElement(contentTemplate);

            container.getNode().runWhenAttached(ui -> setupTemplateWhenAttached(ui,
                container, rendering, keyMapper));
            return rendering;
        }

        private void setupTemplateWhenAttached(UI ui, Element owner,
                                               ComponentRendering rendering, DataKeyMapper<SOURCE> keyMapper) {
            String appId = ui.getInternals().getAppId();
            Element templateElement = rendering.getTemplateElement();
            owner.appendChild(templateElement);

            Element container = new Element("div");
            owner.appendVirtualChild(container);
            rendering.setContainer(container);
            String templateInnerHtml;

            if (keyMapper != null) {
                String nodeIdPropertyName = "_renderer_"
                    + templateElement.getNode().getId();

                templateInnerHtml = String.format(
                    "<flow-component-renderer appid=\"%s\" nodeid=\"[[item.%s]]\"></flow-component-renderer>",
                    appId, nodeIdPropertyName);
                rendering.setNodeIdPropertyName(nodeIdPropertyName);
            } else {
                COMPONENT component = createComponent(null);
                if (component != null) {
                    container.appendChild(component.getElement());

                    templateInnerHtml = String.format(
                        "<flow-component-renderer appid=\"%s\" nodeid=\"%s\"></flow-component-renderer>",
                        appId, component.getElement().getNode().getId());
                } else {
                    templateInnerHtml = "";
                }
            }
            // add the component renderer outside the vaadin-grid-tree-toggle
            templateInnerHtml = "<vaadin-horizontal-layout><vaadin-grid-tree-toggle class$='[[item.cssClassName]]' leaf='[[item.leaf]]' expanded='{{expanded}}' level='[[level]]'></vaadin-grid-tree-toggle>"
            + templateInnerHtml + "</vaadin-horizontal-layout>";  // change the original code
            templateElement.setProperty("innerHTML", templateInnerHtml);
        }

        private class ComponentRendering extends ComponentDataGenerator<SOURCE>
            implements Rendering<SOURCE> {

            private Element templateElement;

            public ComponentRendering(ValueProvider<SOURCE, String> keyMapper) {
                super(NoClickHierarchyColumnComponentRenderer.this, keyMapper);
            }

            public void setTemplateElement(Element templateElement) {
                this.templateElement = templateElement;
            }

            @Override
            public Element getTemplateElement() {
                return templateElement;
            }

            @Override
            public Optional<DataGenerator<SOURCE>> getDataGenerator() {
                return Optional.of(this);
            }

            @Override
            public void generateData(SOURCE item, JsonObject jsonObject) {
                super.generateData(item, jsonObject);
                // in order to add item.leaf property
                getValueProviders().forEach((key, provider) -> jsonObject.put(key,
                    JsonSerializer.toJson(provider.apply(item))));
            }
        }
    }

}

This class has to be in this package: com.vaadin.flow.component.grid since it uses a protected method of the TreeGrid.
You can also extend the TreeGrid and put the code in the subclass.

Here you have an example of the usage:

TreeGrid<DummyFile> grid = new TreeGrid<>();
        // text column to expand
        GridHelpers.addNoClickHierarchyColumn(grid, DummyFile::getFilename).setHeader("File Name");
       // or component column to expand
        GridHelpers.addNoClickComponentHierarchyColumn(grid, o -> {
            Checkbox checkbox = new Checkbox();
            checkbox.setIndeterminate(true);

            HorizontalLayout horizontalLayout = new HorizontalLayout(checkbox, new Span(o.getFilename()));
            return horizontalLayout;
        }).setHeader("File Name");

Another workaround here

Another workaround here

Imho this should be the default behavior. Thanks for linking this.

I had a similar issue this week, and got some assistance from the Vaadin channel on Polymer Slack. I am writing a folder tree component with vaadin-checkbox for node selection. Expanding a branch did not change the vaadin-checkbox, nor would closing a branch check a checkbox. However closing a branch would uncheck a checkbox. Putting the toggle and the checkbox in separate columns removed the indentation.

Jean-Christophe suggested I append the checkbox after the toggle but in the same column. That worked. Here's my code:

  render() {
    return html`
      <vaadin-grid theme="compact" aria-label="items to checkout">
        <vaadin-grid-column
          width="18em"
          resizable
          .renderer="${this._boundItemRenderer}"
        ></vaadin-grid-column>
      </vaadin-grid>
    `;
  }

  constructor() {
    super();
    this.items = null;
    this._boundItemRenderer = this._itemRenderer.bind(this);
  }

  _itemRenderer(root, column, rowData) {
    // Test to avoid re-creating toggler during subsequent calls.
    let toggler = root.firstElementChild;
    if (!toggler) {
      toggler = window.document.createElement('vaadin-grid-tree-toggle');

      toggler.addEventListener('expanded-changed', () => {
        const grid = this.shadowRoot.querySelector('vaadin-grid');
        toggler.addEventListener('expanded-changed', evt => {
          evt.preventDefault();
          grid[toggler.expanded ? 'expandItem' : 'collapseItem'](toggler.item);
        });
      });
      root.appendChild(toggler);

      const checkbox = document.createElement('vaadin-checkbox');
      toggler.appendChild(checkbox);
    }

    toggler.item = rowData.item;
    toggler.leaf = !rowData.item.objectID.includes('fldr');
    toggler.expanded = rowData.expanded;
    toggler.level = rowData.level;

    const checkbox = toggler.querySelector('vaadin-checkbox');
    checkbox.id = rowData.item.objectID;
    checkbox.dataset.parent = rowData.item.parentObjID;
    checkbox.textContent = rowData.item.itemName;
    checkbox.addEventListener('change', evt => {
      this._onCheckboxClick(evt);
    });
  }

I hope this is helpful.

Was this page helpful?
0 / 5 - 0 ratings