Feedback needed: TreeGrid Flattened Hierarchy Support in Vaadin 25

Hello Forum!

Vaadin 25 introduces flattened hierarchy support to HierarchicalDataProvider which provides more predictable scrolling behavior for TreeGrids that use the drag-and-drop functionality or call refreshAll() frequently. It also gives more flexibility for designing optimal data queries, enabling you, for example, to use database techniques like SQL Common Table Expressions to construct the visible range directly in the database.

In addition, TreeGrid has been refactored overall to address various other issues and inconsistencies, which also made it possible to support the scrollToItem feature.

We’d greatly appreciate your feedback and testing. Give it a try with Vaadin 25.0.0 or later and let us know how it works for your project!

More details below:

HierarchyFormat

HierarchicalDataProvider now supports two formats for returning hierarchical data, allowing you to choose the one that best fits your use case: HierarchyFormat.NESTED or HierarchyFormat.FLATTENED.

HierarchyFormat.NESTED (default)

Before version 25, this was the only supported format, and it remains the default in Vaadin 25. This format describes a data provider that, in each request, returns only the direct children of the requested parent:

└── Item 0              <-- 1st request (parent = null, offset = 0, limit = 4)
    └── Item 0-0        <-- 2nd request (parent = Item 0, offset = 0, limit = 4)
        └── Item 0-0-0  <-- 3rd request (parent = Item 0-0, offset = 0, limit = 4)
└── Item 1              <-- Received in the 1st request

The component loads deeper levels by making separate requests based on the viewport and current expanded items. The hierarchy structure is cached in memory incrementally as it’s discovered.

Advantages:

  • Simple and fast data queries. Each request fetches only the direct children, which are then cached hierarchically.

Disadvantages:

  • The full size and structure of the tree remains unknown without recursively fetching the entire hierarchy, which is impractical due to potentially a lot of consecutive requests and heavy memory usage. As a result, the scroll position cannot be restored automatically after using HierarchicalDataProvider.refreshAll() which resets the cached hierarchy state.
  • The scroll container size updates dynamically while scrolling, which can cause users to skip levels if they scroll quickly.

HierarchyFormat.FLATTENED (new)

This is a new format that you can choose to implement starting with Vaadin 25. This format describes a data provider that returns the entire subtree of the requested parent in a single, flattened list. This allows the component to retrieve multiple hierarchy levels in one request, avoiding the need for recursive fetching:

└── Item 0             <-- 1st request (parent = null, offset = 0, limit = 4)
└── Item 0-0           <-- Received in the same request
└── Item 0-0-0         <-- Received in the same request
└── Item 1             <-- Received in the same request

To load the current viewport, the component sends a HierarchicalQuery, where:

  1. HierarchicalQuery#getOffset() specifies the start of the visible range across the entire flattened list
  2. HierarchicalQuery#getExpandedItemIds() provides a set of item IDs that are currently expanded
  3. Other methods work as usual

This format also requires the data provider to implement HierarchicalDataProvider#getDepth(T), which the component uses to display indentation for hierarchical levels.

Here is a simple example to get you started:

class MyDataProvider implements HierarchicalDataProvider<String, Void> {
    private HashMap<String, List<String>> data = new HashMap<>() {
        {
            put(null, List.of("Item 0", "Item 1"));
            put("Item 0", List.of("Item 0-0"));
            put("Item 0-0", List.of("Item 0-0-0"));
        }
    };

    @Override
    public HierarchyFormat getHierarchyFormat() {
        // Indicate that this data provider returns data as a flat list
        return HierarchyFormat.FLATTENED;
    }

    @Override
    public Stream<String> fetchChildren(
            HierarchicalQuery<String, Void> query) {
        // Return a flat list that includes all expanded descendants
        return flatten(query.getParent(), query.getExpandedItemIds())
                .skip(query.getOffset()).limit(query.getLimit());
    }

    @Override
    public int getChildCount(HierarchicalQuery<String, Void> query) {
        // Return the total number of items in the flat list
        return (int) flatten(query.getParent(), query.getExpandedItemIds())
                .count();
    }

    @Override
    public int getDepth(String item) {
        // Implement this method to apply visual indentation based on depth
        return item.split("-").length - 1;
    }

    private Stream<String> flatten(String parent,
            Set<Object> expandedItemIds) {
        // Flatten the subtree into a list, including only expanded branches
        return data.getOrDefault(parent, List.of()).stream()
                .flatMap(child -> expandedItemIds.contains(getId(child))
                        ? Stream.concat(Stream.of(child),
                                flatten(child, expandedItemIds))
                        : Stream.of(child));
    }
}

You can also implement sorting and filtering in the flatten method, if needed. A couple of things to keep in mind:

  1. Sorting should be applied separately within each level to maintain the correct depth-first order.
  2. Sorting should ideally be skipped when flattening the tree to count items, to reduce extra computation.

Here is how sorting and filtering might be implemented:

private Stream<String> flatten(String parent,
        Set<Object> expandedItemIds,
        Optional<Predicate<String>> filter,
        Optional<Comparator<String>> comparator) {
    Stream<String> stream = data.getOrDefault(parent, List.of())
            .stream();

    if (filter.isPresent()) {
        stream = stream.filter((child) -> {
            // Include the item if it satisfies the filter condition
            if (filter.get().test(child)) {
                return true;
            }
            
            // Include the item if any of its descendants satisfy the filter condition
            return expandedItemIds.contains(getId(child))
                    && flatten(child, expandedItemIds, filter,
                            null).findAny().isPresent();
        });
    }

    if (comparator.isPresent()) {
        // Sort items before flattening their children
        stream = stream.sorted(comparator.get());
    }

    return stream.flatMap(
            child -> expandedItemIds.contains(getId(child))
                    ? Stream.concat(Stream.of(child),
                            flatten(child, expandedItemIds,
                                    filter, comparator))
                    : Stream.of(child));
}

Advantages:

  • Fetching the full tree size upfront allows the component to set a fixed scroll container, making scrolling more stable and predictable.
  • By refetching the full tree size, refreshAll() is able to preserve the scroll position, avoiding unexpected jumps.
  • Developers have full control over data queries, allowing for more advanced optimizations and storage strategies at the database level.

Disadvantages:

  • Increased complexity and potentially heavier data queries due to the need for hierarchy reconstruction, which may require, for example, using recursive CTEs (Common Table Expressions) to fetch all descendants of an item in a single SQL query.

TreeDataProvider (and TreeData)

TreeDataProvider now also supports HierarchyFormat.FLATTENED. However, it must currently be enabled manually by passing this format as the second argument to the constructor:

- new TreeDataProvider(treeData);
+ new TreeDataProvider(treeData, HierarchyFormat.FLATTENED);

The same applies when using TreeGrid#setTreeData:

- treeGrid.setTreeData(treeData);
+ treeGrid.setDataProvider(new TreeDataProvider(treeData, HierarchyFormat.FLATTENED));

This is to avoid a breaking change, since enabling it alters how TreeGrid#scrollToIndex(int index) works – the provided index is treated as referring to an item in the flattened structure rather than only at the root level.

Scrolling to items

The overall refactoring of TreeGrid made it possible to add the long-awaited scrollToItem feature. It supports both hierarchy formats mentioned earlier. For more details, please refer to TreeGrid scrollToItem feature in v25

Further reading

Hi, we have an application which are using tree data with an in-memory provider that I believe will benefit a lot with the FLATTENED support. We often have to rely on refreshAll() which to our users frustration makes the scroll position jump and behave very unpredictable.

However we rely on the selection tree grid add-on, selection-grid-flow/selection-grid-flow/src/main/java/com/vaadin/componentfactory/selectiongrid/SelectionTreeGrid.java at main · vaadin-component-factory/selection-grid-flow · GitHub

I know that it is relying on internal APIs but what is your take on how difficult it would be to adapt it to work with the FLATTEND mode?

1 Like

Thanks for the feedback! I’ll be able to take a look over the next few weeks and let you know. Just one thing to clarify, are you using TreeDataProvider or a custom data provider based on TreeData?

We are using a custom data provider extending TreeData (in-memory). Will it support the FLATTENED mode, or what do I need to do to test that?

Will it support the FLATTENED mode, or what do I need to do to test that?

If your data provider class extends TreeDataProvider, you can enable the flattened mode by simply passing HierarchyFormat.FLATTENED as the second argument to the superclass constructor.

If you data provider class extends the raw HierarchicalDataProvider, you will need to refactor it manually to return data in the flattened format. Follow these steps to do so:

  1. Override the getHierarchyFormat() method to return HierarchyFormat.FLATTENED
  2. Modify the fetchChildren method to return a requested portion of the flattened list rather than only direct children, using the expanded items and other parameters from HierarchicalQuery.
  3. Modify the getChildCount method to return the total number of items in the flattened list, also based on the expanded items and other parameters in HierarchicalQuery.
  4. Implement the getDepth method to return depth for each item.

When applying these updates, see the examples earlier in this post for reference.


The SelectionGrid addon hasn’t been upgraded to Vaadin 25 yet, so it doesn’t support these new features yet. However, once upgraded, the same steps will apply to data providers to enable flattened mode with it as well.

2 Likes

I’ve tested this now in 25.0.0-beta1, and it is an improvement.

I use a TreeGrid for our main application menu.
It has up to 525 entries and can be 3 levels deep.
We have a TextField to filter it, and when we filter, we keep and open all parent entries with visible children.

Especially the filtering was slightly sluggish, with 4 requests and jumping scrollbar
Now, I enter something into the filter and I see 2 requests and no jumping scrollbar.
Nice.

One comment: Shouldn’t HierarchicalQuery.getExpandedItemIds be getExpandedItems?
On the java-side we get the actual items.

One strange thing; When I click on a parent entry to expand it, I now consistently see 3 requests, while the the old one sent only 2.

Looks like it is the last one that is “new”? :

-- 1st

{
	"clientId": 211,
	"csrfToken": "7a7e4dfa-890d-4ec9-8a35-a75e30a076d1",
	"rpc": [
		{
			"data": {
				"event.detail.internalColumnId": "col0",
				"event.detail.itemKey": "3113",
				"event.detail.section": "body"
			},
			"event": "grid-cell-focus",
			"node": 45,
			"type": "event"
		}
	],
	"syncId": 211
}

-- 2nd

{
	"clientId": 212,
	"csrfToken": "7a7e4dfa-890d-4ec9-8a35-a75e30a076d1",
	"rpc": [
		{
			"args": [
				"onClick",
				"3113",
				[]
			],
			"channel": 0,
			"node": 44,
			"type": "channel"
		}
	],
	"syncId": 212
}

-- 3rd

{
	"clientId": 213,
	"csrfToken": "7a7e4dfa-890d-4ec9-8a35-a75e30a076d1",
	"rpc": [
		{
			"node": 45,
			"promise": 98,
			"templateEventMethodArgs": [
				77
			],
			"templateEventMethodName": "confirmUpdate",
			"type": "publishedEventHandler"
		}
	],
	"syncId": 213
}

1 Like

Thanks for checking it out!

The number of RPC calls might be unrelated to that feature. There were some other significant changes to the components (Lit migration) which might have caused some timing changes so that events are not relayed in the same call. Hard to say without taking a closer look. The focus and click event not being in the same call looks suspicous.

1 Like

We initially planned to add getExpandedItems, but then those items would need to be refreshed when calling refreshAll, which isn’t possible until they appear in the viewport. To avoid inconsistencies, such as some items being refreshed while others aren’t, we decided to go with getExpandedItemIds for now. However, this isn’t set in stone and can be revisited later if we find how to avoid these inconsistencies.

Don’t understand.
My point is that it is items I get when I call getExpandedItemIds.

In your examples you just add some text. Then that text is the item, and that is what you get in getExpandedItemIds.

If you add pojos instead with a dataprovider, then getExpandedItemIds returns those pojos

That’s because, by default, the ID of an item is the item object itself. However, if you customize DataProvider#getId, the ID can be something else.

That being said, I understand your confusion. How do you think expanded items should behave on refresh? That would probably need an ExpandedItemsPreservationHandler, similar to SelectionPreservationHandler.

Learned something new :slight_smile:

I only use “refresh” to update after filtering, and then the actual items are the same.
You are talking about how to handle it when the items themselves are recreated?

Well, then I understand the problem at least.

Since I don’t do it, I have no opinion.
I’m always for making similar things similar, but I didn’t even know about SelectionPreservationHandler…

The SelectionGrid addon has been now updated to support Vaadin 25, including TreeGrid improvements. The new version is available here: Selection Grid - Vaadin Add-on Directory

I have now been able to test our application with Vaadin 25 beta8 with the FLATTENED tree data and the updated SelectionGrid addon, and everything seems to work fine.
This is a HUGE improvement :clap:

The only thing I wish for is that scrollToItem could check if the item is already in the view, see TreeGrid scrollToItem feature in v25 - Vaadin Forum

Maybe there is a way to check it myself?

1 Like

Thanks, your feedback was noted. We’ve updated the scrollToItem method so it won’t scroll when the item is already fully visible. This change will apply to both Grid and TreeGrid starting from Vaadin 25.0.0-rc1, which is already out for flow-components: Release Vaadin Flow Components V25.0.0-rc1 · vaadin/flow-components · GitHub

We’ve also added documentation explaining how to use hierarchical data providers with TreeGrid and how to implement different hierarchy formats. Feedback is welcome!

That’s great news and have verified that it works in rc1 :clap:

1 Like