VAADIN

Vaadin TreeGrid with lazy loading, filtering and sorting

13 May, 2021

I’m giving my first steps into Vaadin 14 after a long, long time of Vaadin 8 projects and I want to share my experience when it was time to work with the TreeGrid component to display hierarchical data. I ran into this issue where I needed to add filtering and sorting to a Vaadin TreeGrid implementing lazy loading. I had this article for Grid as base but some things are different so I thought it would be a good idea to share all the steps needed and all things to keep in mind for this kind of implementation.

For the following explanation I will assume that you have some knowledge on Vaadin components and lazy loading.

To start, let’s define an entity class to represent the data we want to display hierarchically. In this case, Department entity:

public class Department {

    private int id;
    private String name;
    private String manager;
    private Department parent;

    public Department(int id, String name, Department parent, String manager) {
        this.id = id;
        this.name = name;
        this.manager = manager;
        this.parent = parent;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getManager() {
        return manager;
    }

    public void setManager(String manager) {
        this.manager = manager;
    }

    public Department getParent() {
        return parent;
    }

    public void setParent(Department parent) {
        this.parent = parent;
    }

}

After that, we can define the TreeGrid and it’s data provider with lazy loading. Unlike Grid, the fetch and count callbacks are based on HierarchicalQuery. So now, to implement hierarchical lazy data provider we need to define the following three methods:

protected Stream<Department> fetchChildrenFromBackEnd(HierarchicalQuery<Department, String> query) {...}

public int getChildCount(HierarchicalQuery<Department, String> query) {...}

public boolean hasChildren(Department item) {...}

As a result, the TreeGrid with lazy loading implementation will will look like this:

public MainView() {

    TreeGrid<Department> treeGrid = new TreeGrid<>();
    treeGrid.addHierarchyColumn(Department::getName)
    .setHeader("Department Name").setKey("name");
    treeGrid.addColumn(Department::getManager)
    .setHeader("Manager").setKey("manager");
	 
    DepartmentService departmentService = new DepartmentService();	
	
    HierarchicalDataProvider<Department, Void> dataProvider =
	new AbstractBackEndHierarchicalDataProvider<Department, Void>() {

	// returns the number of immediate child items
	@Override
	public int getChildCount(HierarchicalQuery<Department, Void> query) {
	    return (int) departmentService.getChildCount(query.getParent());
	}

	// checks if a given item should be expandable
	@Override
	public boolean hasChildren(Department item) {
	    return departmentService.hasChildren(item);
	}
		
	// returns the immediate child items based on offset and limit
	@Override
	protected Stream<Department> fetchChildrenFromBackEnd(
		HierarchicalQuery<Department, Void> query) {
		return departmentService.fetchChildren(query.getParent(), 
                  query.getLimit(), query.getOffset()).stream();
	}
    };

    treeGrid.setDataProvider(dataProvider);	
    add(treeGrid);
}

Of course, we have to define a service class that will be in charge of retrieving the data from the backend. This service will have to implement the following methods in order to be able to set the data provider of the Tree Grid:

public List<Department> fetchChildren(Department parent) {...}

public int getChildCount(Department parent) {...}

public boolean hasChildren(Department parent, int limit, int offset) {...}

But, what if we want to add filtering and sorting to this implementation? Let’s see…

Filtering

Define a filter class that can be applied to the collection of items, to filter them out:

public class DepartmentFilter {

	private String nameFilter = null;	
	private String managerFilter = null;
	
	public DepartmentFilter() {	}

	public DepartmentFilter(String nameFilter, String managerFilter) {
		this.nameFilter = nameFilter;
		this.managerFilter = managerFilter;
	}

	public String getNameFilter() {
		return nameFilter;
	}

	public void setNameFilter(String nameFilter) {
		this.nameFilter = nameFilter;
	}

	public String getManagerFilter() {
		return managerFilter;
	}

	public void setManagerFilter(String managerFilter) {
		this.managerFilter = managerFilter;
	}
	
}

Instead of using a HierarchicalDataProvider, we need to configure a HierarchicalConfigurableFilterDataProvider that will allow us to set a filter to apply to the queries. We need to wrap the data provider into a ConfigurableDataProvider by calling withConfigurableFilter() method in order to tell the data provider that is going to be filterable.

HierarchicalConfigurableFilterDataProvider<Department, Void, DepartmentFilter> dataProvider =
     new AbstractBackEndHierarchicalDataProvider<Department, DepartmentFilter>() {

   // returns the number of immediate child items based on query filter
   @Override
   public int getChildCount(HierarchicalQuery<Department, DepartmentFilter> query) {
      return (int) departmentService.getChildCount(query.getParent(), query.getFilter().orElse(null));
   }

   // checks if a given item should be expandable
   @Override
   public boolean hasChildren(Department item) {
      return departmentService.hasChildren(item);
   }

   // returns the immediate child items based on offset, limit and filter
   @Override
   protected Stream<Department> fetchChildrenFromBackEnd(
	    HierarchicalQuery<Department, DepartmentFilter> query) {
      return departmentService.fetchChildren(query.getParent(), 
            query.getLimit(), query.getOffset(), 
            query.getFilter().orElse(null)).stream();
   }

}.withConfigurableFilter();

// set data provider to tree
treeGrid.setDataProvider(dataProvider);
	
// define filter
DepartmentFilter treeFilter = new DepartmentFilter();
	
// set filter to data provider
dataProvider.setFilter(treeFilter);

Update the UI adding new filter fields for the columns we want to be filterable. Add this fields to a header row. That will display the filter fields on top of each column:

HeaderRow filterRow = treeGrid.prependHeaderRow();

TextField nameFilterTF = new TextField();
nameFilterTF.setClearButtonVisible(true);
nameFilterTF.addValueChangeListener(e -> {
	treeFilter.setNameFilter(e.getValue());
	dataProvider.refreshAll(); 
});
filterRow.getCell(treeGrid.getColumnByKey("name")).setComponent(nameFilterTF);

TextField managerFilterTF = new TextField();
managerFilterTF.setClearButtonVisible(true);
managerFilterTF.addValueChangeListener(e -> {
	treeFilter.setManagerFilter(e.getValue());
	dataProvider.refreshAll();
});
filterRow.getCell(treeGrid.getColumnByKey("manager")).setComponent(managerFilterTF);  

Finally, don’t forget to update the service class to fetch data using DepartmentFilter in case of filtering. The methods to update in the current example are:

public List<Department> fetchChildren(Department parent, int limit, int offset, DepartmentFilter filter) {...}

public int getChildCount(Department parent, DepartmentFilter filter) {...}

Sorting

Similarly to filtering, we need to define a class to store the sorting information:

public class DepartmentSort {

        public static final String NAME = "name";	
        public static final String MANAGER = "manager";

        private String propertyName;	
        private boolean descending;
	
	public DepartmentSort(String propertyName, boolean descending) {
		this.propertyName = propertyName;
		this.descending = descending;
	}
	public String getPropertyName() {
		return propertyName;
	}
	public void setPropertyName(String propertyName) {
		this.propertyName = propertyName;
	}
	public boolean isDescending() {
		return descending;
	}
	public void setDescending(boolean descending) {
		this.descending = descending;
	}
	
}

Then, update the definition of the data provider to send the sorting properties and order to the backend where sorting happens:

HierarchicalConfigurableFilterDataProvider<Department, Void, DepartmentFilter> dataProvider =
	new AbstractBackEndHierarchicalDataProvider<Department, DepartmentFilter>() {

	// returns the number of immediate child items based on query filter
	@Override
	public int getChildCount(HierarchicalQuery<Department, DepartmentFilter> query) {
		return (int) departmentService.getChildCount(query.getParent(), 
                  query.getFilter().orElse(null));
	}

	// checks if a given item should be expandable
	@Override
	public boolean hasChildren(Department item) {
		return departmentService.hasChildren(item);
	}

	// returns the immediate child items based on offset, limit, filter and sorting
	@Override
	protected Stream<Department> fetchChildrenFromBackEnd(
		HierarchicalQuery<Department, DepartmentFilter> query) {
		List<DepartmentSort> sortOrders = query.getSortOrders().stream()
			.map(sortOrder -> new DepartmentSort(sortOrder.getSorted(), 
                         sortOrder.getDirection().equals(SortDirection.ASCENDING)))
			.collect(Collectors.toList());
		return departmentService.fetchChildren(query.getParent(), query.getLimit(), 
                   query.getOffset(), query.getFilter().orElse(null), sortOrders).stream();
	}

}.withConfigurableFilter();

As a final step, one more time, we need to update the service class to get data sorted by the properties and order selected. In this part, we only need to update fetchChildren method:

public Collection<Department> fetchChildren(Department parent, int limit, 
         int offset, DepartmentFilter filter, List<DepartmentSort> sortOrders) { ... }	

And like that, we have a TreeGrid with lazy loading, filtering and sorting.

Check out the following animation to see some TreeGrid action:

In addition, a few things to keep in mind:

#1: Don’t forget to specify which columns are sortable by using setSortable(true) or setSortProperty(…)

treeGrid.addHierarchyColumn(Department::getName)
    .setHeader("Department Name").setKey("name")
    .setSortProperty(DepartmentSort.NAME);	  
treeGrid.addColumn(Department::getManager)
    .setHeader("Manager").setKey("manager")
    .setSortProperty(DepartmentSort.MANAGER);

If the column is sortable and a sort property is not specified, the column key will be used by default.

#2: You can use setMultisort(true) to enable multiple column sorting on the client-side

treeGrid.setMultiSort(true);

#3: When adding the fields for filtering in the header row, don’t forget to call refreshAll() method, so the data provider will trigger the loading of the data based on the new specified filter values.

TextField managerFilterTF = new TextField();
	managerFilterTF.setClearButtonVisible(true);
	managerFilterTF.addValueChangeListener(e -> {
		treeFilter.setManagerFilter(e.getValue());
		dataProvider.refreshAll();    // -> don't forget about this!
	});    

You can find the complete example in our  GitHub organization. Click here if you want to checkout the source code and test the implementation yourself.

Hope you find this useful and feel free to write if any concern arise.

Thanks for reading and keep the code flowing!

Paola De Bartolo
By Paola De Bartolo

Systems Engineer. Java Developer. Vaadin enthusiast since the moment I heard "you can implement all UI with Java". Proud member of the #FlowingCodeTeam since 2017.

Join the conversation!
Profile Picture

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

  • Frank says:

    Hi Paola, great example! The code is very well written and instructive. Thank you very much for sharing! Just one question: The TreeGrid is scrollable (which is great) but just uses about half of my screen (notebook with 1920 x 1080). The lower part is empty. Where could I adapt the height of the TreeGrid?

    • Paola De Bartolo says:

      Hi Frank, thanks for the feedback, it’s much appreciated! Regarding your question, you can set height of TreeGrid by using setHeight method (for e.g. treeGrid.setHeight("500px");). But you also need to take in consideration the parent layout. In my example, the parent layout is a VerticalLayout, then by setting the height to full to this layout will make the tree take all the possible space. Regards.

  • Artur says:

    Hi Paola, thank you very much for your example!! I was just trying to figure out how to make lazy loading with tree grid, now I also have to make a filter but I don’t really get what flattenElement() method does. I tried to write my own filter just like you did but came across one problem: as my dataProvider has like 1000000 elements and I have to filter the data really quick I deleted flattenElement() method as it is too time consuming and heavy but now I get error each time I reload the page, like that:
    Assertion error: No child node found with id 27
    Can you please tell me what is the flattenElement() method needed for or maybe some other way to make filter with dataProvider work, I don’t really have an idea why my page crushes on reload, thank ypu in advance, looking forward to hearing from you,
    Best regards,
    Artur.

    • Paola De Bartolo says:

      Hello Artur. Please keep in mind that the DepartmentService class is just a mock of a service. The flattenElement method is only a method that helps to simulate a database or rest API call searching for the results. What you need to do is write your own implementation of the filtering having in consideration your backed service.

  • Berk Ott says:

    Thank you for this Paola! Can you provide what the actual query string looks like? Is there where clause or is it just select Dept , Manager. I have yet to find a complete example for reading data from a database. Much appreciated!

  • Berk Ott says:

    Oops I just saw the link to GitHub!
    Thank you!