Docs

Route Hierarchy

How to declare logical parent routes and resolve the route hierarchy for breadcrumbs and menus.

Routes can declare a logical parent route, forming a route hierarchy. The hierarchy is used to build navigation aids such as breadcrumb trails and hierarchical menus.

The route hierarchy is independent of the layout chain declared through @Route layout and @RoutePrefix: a route may be rendered inside one layout while logically belonging under a completely different route. The hierarchy is also resolved without creating an instance of the route or its parent, which makes it usable for routes that aren’t currently shown, such as the ancestors of a breadcrumb trail.

Declaring a Static Parent

Use the @RouteParent annotation to declare the logical parent of a navigation target:

Source code
Java
@Route("dashboard")
public class DashboardView extends Div {
}

@Route("reports")
@RouteParent(DashboardView.class)
public class ReportsView extends Div {
}

The parent inherits the RouteParameters of the annotated route, narrowed to the names that the parent’s own route template declares. A parent with fewer or no parameters therefore still resolves to a working link.

Resolving the Parent Dynamically

When the parent — or the parameters it should be resolved with — needs to be computed, set a RouteParentResolver through the resolver attribute of @RouteParent. The resolver receives a RouteParentContext describing the navigation target class and its RouteParameters, and returns a RouteParentReference pointing to the parent:

Source code
Java
@Route("orgs/:orgId/projects/:projectId")
@RouteParent(resolver = OrgParentResolver.class)
public class ProjectView extends Div {
}

public class OrgParentResolver implements RouteParentResolver {
    @Override
    public Optional<RouteParentReference> resolveParent(
            RouteParentContext context) {
        // carry over only the parameters the parent route needs
        RouteParameters parentParameters = new RouteParameters("orgId",
                context.routeParameters().get("orgId").orElseThrow());
        return Optional.of(
                new RouteParentReference(OrgView.class, parentParameters));
    }
}

A RouteParentReference carries both the parent navigation target class and the RouteParameters it should be resolved with. The parameters are part of the reference because a parent route typically declares only a subset of the child parameters, and that subset is needed both to build a link to the parent and to resolve its parent in turn.

Returning an empty Optional marks the top of the hierarchy. When a resolver is set, the static value of the annotation is ignored.

Resolver implementations must be stateless and cheap to create: they’re instantiated through the application Instantiator — so dependency injection is available — every time a parent is resolved. The route itself is never instantiated.

URL-Derived Parents

Routes without a @RouteParent annotation get their logical parent derived from the route URL: the path is walked upwards until a registered route serving the nearest ancestor path is found. For example, the parent of a route serving users/:userId/orders is the route serving users/:userId, if one is registered.

This means that route hierarchies that follow the URL structure work without any annotations; @RouteParent is only needed when the logical hierarchy differs from the URL paths.

Resolving the Hierarchy

RouteConfiguration exposes the resolved hierarchy:

  • getRouteParent(Class, RouteParameters) resolves the logical parent of a navigation target, returning an Optional<RouteParentReference>.

  • getRouteHierarchy(Class, RouteParameters) resolves the whole chain of the target and its logical ancestors, ordered from the hierarchy root to the given target, which is the last element.

Neither method instantiates any of the routes. Combined with the router state signal, the hierarchy can be used to render a breadcrumb trail that updates on every navigation:

Source code
Java
public class Breadcrumbs extends Div {

    public Breadcrumbs() {
        Signal<RouterState> routerState = UI.getCurrent().routerStateSignal();
        Signal.effect(this, () -> update(routerState.get()));
    }

    private void update(RouterState state) {
        removeAll();
        if (state.navigationTarget() == null) {
            return;
        }
        RouteConfiguration configuration = RouteConfiguration
                .forSessionScope();
        Router router = VaadinService.getCurrent().getRouter();
        for (RouteParentReference entry : configuration.getRouteHierarchy(
                state.navigationTarget(), state.routeParameters())) {
            String title = router.resolvePageTitle(entry.navigationTarget(),
                    entry.routeParameters()).orElse("");
            add(new RouterLink(title, entry.navigationTarget(),
                    entry.routeParameters()));
        }
    }
}

Router.resolvePageTitle(Class, RouteParameters) resolves the title of each entry without instantiating the route, applying the same @DynamicPageTitle generator and application-wide default PageTitleGenerator chain used during navigation. Because the ancestor routes are never instantiated, instance-based title APIs such as HasDynamicTitle aren’t consulted; titles are resolved from the navigation target class and its route parameters alone.

See Generating Page Titles for how to declare title generators on routes and application-wide.

7E1B2F64-9C45-4A1B-BF1F-2D5E8A0C3D91

Updated