Search code examples
vaadinvaadin8

Replacement for collapseItemsRecursively and expandItemsRecursively in Vaadin 8.1 TreeGrid


Vaadin 8.1 introduced the TreeGrid component. It does not have the collapseItemsRecursively and expandItemsRecursively methods anymore (as available in the now legacy Tree component). Do i miss something or do you need to develop your own implementation? If so, what is a recommended way of doing this?


Solution

  • As I'm sure you've noticed, the TreeGrid is a rather new component, currently being developed and available starting with v8.1.alphaX (current stable version is v8.0.6). As such, it probably has only some basic functionalities for the time being, with the rest to follow sometime in the future, although there are no guarantee. For example this similar feature request for the older TreeTable component has been in open state since 2011.

    Vaadin docs warning

    Either way, even if they're probably not the optimum solutions, there are a couple of work-arounds that you can use to achieve this behavior. I'm shamelessly using as a base sample, a slightly modified version of the code currently available in the vaadin-sampler for TreeGrid.

    public class RecursiveExpansionTreeGrid extends VerticalLayout {
    
        private Random random = new Random();
    
        public RecursiveExpansionTreeGrid() {
            // common setup with some dummy data
            TreeGrid<Project> treeGrid = new TreeGrid<>();
            treeGrid.setItems(generateProjectsForYears(2010, 2016), Project::getSubProjects);
            treeGrid.addColumn(Project::getName).setCaption("Project Name").setId("name-column");
            treeGrid.addColumn(Project::getHoursDone).setCaption("Hours Done");
            treeGrid.addColumn(Project::getLastModified).setCaption("Last Modified");
            addComponent(treeGrid);
        }
    
        // generate some dummy data to display in the tree grid
        private List<Project> generateProjectsForYears(int startYear, int endYear) {
            List<Project> projects = new ArrayList<>();
    
            for (int year = startYear; year <= endYear; year++) {
                Project yearProject = new Project("Year " + year);
    
                for (int i = 1; i < 2 + random.nextInt(5); i++) {
                    Project customerProject = new Project("Customer Project " + i);
                    customerProject.setSubProjects(Arrays.asList(
                            new LeafProject("Implementation", random.nextInt(100), year),
                            new LeafProject("Planning", random.nextInt(10), year),
                            new LeafProject("Prototyping", random.nextInt(20), year)));
                    yearProject.addSubProject(customerProject);
                }
                projects.add(yearProject);
            }
            return projects;
        }
    
        // POJO for easy binding
        public class Project {
            private List<Project> subProjects = new ArrayList<>();
            private String name;
    
            public Project(String name) {
                this.name = name;
            }
    
            public String getName() {
                return name;
            }
    
            public List<Project> getSubProjects() {
                return subProjects;
            }
    
            public void setSubProjects(List<Project> subProjects) {
                this.subProjects = subProjects;
            }
    
            public void addSubProject(Project subProject) {
                subProjects.add(subProject);
            }
    
            public int getHoursDone() {
                return getSubProjects().stream().map(project -> project.getHoursDone()).reduce(0, Integer::sum);
            }
    
            public Date getLastModified() {
                return getSubProjects().stream().map(project -> project.getLastModified()).max(Date::compareTo).orElse(null);
            }
        }
    
        // Second POJO for easy binding
        public class LeafProject extends Project {
            private int hoursDone;
            private Date lastModified;
    
            public LeafProject(String name, int hoursDone, int year) {
                super(name);
                this.hoursDone = hoursDone;
                lastModified = new Date(year - 1900, random.nextInt(12), random.nextInt(10));
            }
    
            @Override
            public int getHoursDone() {
                return hoursDone;
            }
    
            @Override
            public Date getLastModified() {
                return lastModified;
            }
        }
    }
    

    Next, recursively expanding or collapsing the nodes depends a bit on your scenario, but basically it breaks down to the same thing: making sure each node from the root to the deepest leaf is expanded/collapsed.The simplest way of doing it is to flatten your hierarchy into a list of nodes, and call the appropriate method, expand(List<T> items) or expand(T ... items) (the second delegates to the first and is probably a convenience method eg expand(myItem)).

    For simplicity, I've added a flatten method in our Project implementation. If you can't do that for some reason, then create a recursive method that creates a list starting with the selected node and includes all the children, of the children, of the children.... well, you get the idea.

    public Stream<Project> flatten() {
        return Stream.concat(Stream.of(this), getSubProjects().stream().flatMap(Project::flatten));
    }
    

    Possible scenarios:

    1. Automatically expand the entire hierarchy when expanding the root - add listeners, and expand/collapse the whole flattened hierarchy:
    treeGrid.addCollapseListener(event -> {
        if (event.isUserOriginated()) {
            // event is triggered by all collapse calls, so only do it the first time, when the user clicks in the UI
            // and ignore the programmatic calls
            treeGrid.collapse(event.getCollapsedItem().flatten().collect(Collectors.toList()));
        }
    });
    treeGrid.addExpandListener(event -> {
        if (event.isUserOriginated()) {
            // event is triggered by all expand calls, so only do it the first time, when the user clicks in the UI
            // and ignore the programmatic calls
            treeGrid.expand(event.getExpandedItem().flatten().collect(Collectors.toList()));
        }
    });
    
    1. Expanding the hierarchy or part of it with a custom action, such as a context menu
    GridContextMenu<Project> contextMenu = new GridContextMenu<>(treeGrid);
    contextMenu.addGridBodyContextMenuListener(contextEvent -> {
        contextMenu.removeItems();
        if (contextEvent.getItem() != null) {
            Project project = (Project) contextEvent.getItem();
            // update selection
            treeGrid.select(project);
    
            // show option for expanding
            contextMenu.addItem("Expand all", VaadinIcons.PLUS, event -> treeGrid.expand((project).flatten().collect(Collectors.toList())));
    
            // show option for collapsing
            contextMenu.addItem("Collapse all", VaadinIcons.MINUS, event -> treeGrid.collapse((project).flatten().collect(Collectors.toList())));
        }
    });
    

    In the end, you should be getting this effect:

    TreeGrid - expand or collapse recursively