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.
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.
Most helpful comment
Another workaround here