feat: add route hierarchy with dynamic titles#24550
Merged
Merged
Conversation
HasDynamicTitle is an instance method, so the only way to get a dynamic title for a route is to create the view. That is a problem for breadcrumbs, menus and similar navigation aids that need the titles of a whole trail of routes without instantiating any of them. Until now the only instance-free title source was the static @PageTitle value. Introduce PageTitleGenerator, a stateless counterpart of HasDynamicTitle that resolves a title purely from the navigation target class and its RouteParameters (passed via a PageTitleContext). A route opts in through the new @PageTitle(generator = ...) attribute. The generator is created through the application Instantiator (so DI works), never the route itself. @PageTitle#value() now defaults to an empty string so a route can declare a generator without a redundant value, and the generator takes precedence over the static value when both are present. Title resolution honours the generator in both code paths: MenuRegistry.getTitle (used by menus, page headers and other instance-free consumers, with a new overload that accepts RouteParameters) and AbstractNavigationStateRenderer when applying the title during navigation. Existing behaviour is unchanged for routes that use a plain @PageTitle or no annotation.
Add @RouteParent as a purely logical navigation hierarchy, independent of the @Route/@RoutePrefix layout chain, so breadcrumbs and hierarchical menus can walk the ancestor trail of a route without instantiating any of them. Mirroring the PageTitleGenerator design, the parent can be declared either statically via @RouteParent(value) or dynamically via @RouteParent(resolver = ...), where a stateless RouteParentResolver computes the parent from the route class and its RouteParameters (handed in through a RouteParentContext). The resolver returns a RouteParentReference carrying the parent class together with the RouteParameters it should be resolved with, since a parent route usually inherits only a subset of the child parameters. Resolvers are created through the application Instantiator (DI), never the route itself; the resolver takes precedence over a static value. RouteUtil.getRouteParent resolves a single level and RouteUtil.getBreadcrumbTrail climbs to the hierarchy root (with a cycle guard), returning the trail ordered root-to-current. Each entry carries the parameters needed to resolve its own title via MenuRegistry.getTitle, so the two features compose: the breadcrumb labels of routes that are never instantiated are produced by their PageTitleGenerator.
Drop the hand-written getters, no-arg-style convenience constructor and null-check compact constructors from PageTitleContext, RouteParentContext and RouteParentReference. They are plain data carriers, so the record components (navigationTarget/routeParameters accessors) are all that is needed. Update call sites and Javadoc examples to use the canonical record accessors.
The previous name described one use case (breadcrumbs) rather than what the method returns. It actually resolves the logical route hierarchy: the chain of the navigation target and all its @RouteParent ancestors, ordered from the root to the target. Breadcrumbs and hierarchical menus are consumers of that chain, not what the method itself produces.
Restore the URL-based parent resolution as the fallback in the route parent chain. RouteUtil.getRouteParent now resolves the logical parent in this order: 1. @RouteParent annotation (its dynamic resolver, else its static value); 2. otherwise the parent is derived from the route URL by walking up to the registered route serving the nearest ancestor path. The URL fallback resolves the target's url with the given parameters, then trims trailing path segments until a different registered navigation target is found, carrying over the route parameters that apply to that ancestor path. It is skipped when no VaadinService is available. This keeps the hierarchy working for the common case where routes are nested by URL and only need @RouteParent for non-URL-aligned hierarchies.
knoobie
reviewed
Jun 9, 2026
knoobie
reviewed
Jun 9, 2026
Address review feedback on PageTitleContext: - Add the query parameters to PageTitleContext so a generator can resolve a title that depends on the query string, not only the route parameters. They are taken from the navigation Location, and default to QueryParameters.empty() for the instance-free menu/breadcrumb paths. - Add the declared PageTitle value to PageTitleContext and stop ignoring it when a generator is set. The value is now handed to the generator, so a single application-wide generator can resolve many routes from their declared value (for example treating it as an i18n message key) instead of needing a dedicated generator class per route.
Following the I18NProvider / MenuAccessControl provider pattern, add Instantiator.getPageTitleGenerator() so an application can define a single default PageTitleGenerator instead of referencing a generator class from every @PageTitle. - DefaultInstantiator resolves it from the new "pageTitle.generator" init parameter (a class name), returning null when none is configured. - SpringInstantiator resolves it from a PageTitleGenerator bean, falling back to the init-parameter default when there is no unique bean, so a Spring aware generator works out of the box. Title resolution order in MenuRegistry.getTitle and AbstractNavigationStateRenderer becomes: per-route @PageTitle generator, then the application-wide default generator, then the static @PageTitle value (or the class simple name for menus). The declared value is handed to the default generator via PageTitleContext, so a generic generator can resolve every route from its value (for example as an i18n key).
A static @RouteParent forwarded the child's full RouteParameters to the
parent unchanged. When the parent route declares fewer (or no) parameters,
building a RouterLink to it failed with NotFoundException, e.g. an
OrderDetailView (order-detail/:orderId) with @RouteParent(OrdersView.class)
handed OrdersView (no parameters) an {orderId=...} it cannot accept.
getRouteParent now narrows a static parent's parameters to the names the
parent's own route template declares, matching what the URL-derived
resolution already does. When no registry is available the parameters are
left unchanged.
tltv
requested changes
Jun 10, 2026
This was referenced Jun 10, 2026
Address review feedback: getRouteParent / getRouteHierarchy no longer hardcode RouteConfiguration.forApplicationScope() nor depend on the ApplicationRouteRegistry class. They now have an overload taking an explicit RouteRegistry, with the existing no-registry overloads resolving the default via VaadinService.getCurrent().getRouter().getRegistry(). The registry is used through RouteConfiguration.forRegistry(...) and RouteRegistry#getNavigationRouteTarget. Expand RouteUtilTest hierarchy coverage to match the scenarios in the sibling RouteHierarchyTest: class without @route, single route, URL-prefix happy path, deep multi-step chain, missing intermediate terminating at the leaf, empty-template root, @RouteParent precedence over the URL prefix, cycle truncation without duplicates, and a resolver returning empty marking a root.
…otation Per review discussion, keep @PageTitle as the static-title annotation it has always been and express instance-free dynamic titles through a dedicated annotation instead of overloading @PageTitle with a generator attribute. - Add @DynamicPageTitle(Class<? extends PageTitleGenerator>) and remove the @PageTitle#generator() attribute; @PageTitle#value() is required again. - Resolution order is unchanged in behavior: per-route @DynamicPageTitle generator, then the application-wide default generator, then the static @PageTitle value (or class simple name for menus). A @PageTitle value declared alongside @DynamicPageTitle is still handed to the generator via PageTitleContext#value() as a key. This follows the Flow convention of a feature-named annotation plus a role-named strategy type (e.g. @theme + AbstractTheme), avoiding the @DomEvent/DomEvent same-name-different-package pattern and the sentinel default on @PageTitle.
Reconcile the review discussion on the getRouteParent / getRouteHierarchy overloads: keep them taking a RouteRegistry (consistent with the rest of RouteUtil, which all use RouteRegistry) and make obtaining a scoped registry ergonomic by adding RouteConfiguration#getRegistry(). Developers can now write RouteConfiguration.forApplicationScope().getRegistry() or RouteConfiguration.forSessionScope().getRegistry() instead of resolving the session/application registry directly.
Per review, make RouteConfiguration the single public entry point for resolving a route's logical parent and hierarchy, since it is the stateless public API over a route registry and lets callers pick the scope (forApplicationScope() / forSessionScope()). - Add RouteConfiguration#getRouteParent and #getRouteHierarchy, delegating to the internal RouteUtil implementation with this configuration's registry. - Drop the RouteConfiguration#getRegistry() accessor and the no-argument RouteUtil.getRouteParent/getRouteHierarchy overloads (and getDefaultRegistry) so there is a single way to do this rather than several. RouteUtil keeps the RouteRegistry-based implementation used by the delegation and internal tests. - Move the public-API tests to RouteConfigurationTest; RouteUtilTest keeps the detailed scenario coverage against the internal registry-based methods.
tltv
requested changes
Jun 10, 2026
MenuRegistry#getTitle and AbstractNavigationStateRenderer#resolveTitleFromTarget implemented the same @DynamicPageTitle / default-generator / @PageTitle value resolution chain. Extract it into a single RouteUtil#resolvePageTitle that returns an Optional, so callers apply their own fallback (empty title for navigation, class simple name for menus). Removes the duplicated generator instantiation and default-generator lookup from MenuRegistry. Also fix a stale comment in RouteUtilTest (RootResolvedView, not OrderDetailView).
tltv
reviewed
Jun 10, 2026
Note in the HasDynamicTitle javadoc that it resolves the title from the live navigation target instance, and point to the instance-free counterpart DynamicPageTitle (with PageTitleGenerator) for resolving titles of routes that are not shown, such as breadcrumb or menu entries.
tltv
reviewed
Jun 10, 2026
tltv
left a comment
Member
There was a problem hiding this comment.
Last findings are about tests. Add ITs covering new annotations and new public API. Add them in flow-test-root-context-npm module.
Add DefaultInstantiatorPageTitleGeneratorTest mirroring DefaultInstantiatorMenuAccessControlTest: no property returns null, a valid class name is instantiated, and an invalid type throws with a descriptive message.
Add integration tests in the test-root-context (flow-test-root-context-npm) module covering the new API: - DynamicPageTitleView + a ViewTitleIT case asserting that the browser title is produced by a @DynamicPageTitle PageTitleGenerator during navigation. - RouteHierarchyParentView / RouteHierarchyView + RouteHierarchyIT, which render a breadcrumb built from RouteConfiguration#getRouteHierarchy (@RouteParent ancestor + current view) with each title resolved without instantiating the ancestor route, and assert both the breadcrumb text and the navigated page title.
Contributor
Author
|
Added ITs in the
The module test-compiles against the new APIs; the browser runs happen in CI. |
|
|
tltv
approved these changes
Jun 11, 2026
Artur-
added a commit
that referenced
this pull request
Jun 11, 2026
…4582) This PR cherry-picks changes from the original PR #24550 to branch 25.2. --- #### Original PR description > Introduces a new, instance-free mechanism for resolving page titles and logical route hierarchies in Vaadin applications. The main focus is on enabling dynamic page titles and hierarchical navigation aids (like breadcrumbs and menus) without requiring instantiation of navigation target components. This is achieved through new annotations, interfaces, and supporting APIs for stateless resolution of titles and parent routes. > > * Introduced the `@DynamicPageTitle` annotation, allowing routes to specify a `PageTitleGenerator` for computing their titles without needing to instantiate the route component. This enables dynamic titles for navigation elements like menus and breadcrumbs. > * Added the `PageTitleGenerator` interface and `PageTitleContext` record, providing a stateless, parameter-driven way to generate page titles for routes. > * Added the `@RouteParent` annotation and supporting APIs, allowing routes to declare their logical parent statically or via a resolver, again without requiring instantiation. > * Extended `RouteConfiguration` with methods to resolve a route's logical parent and hierarchy statelessly, supporting the new navigation aids. > * Updated `Instantiator` and `DefaultInstantiator` to support application-wide default `PageTitleGenerator` resolution, integrating the new mechanism with the dependency injection system. > * Enhanced `MenuRegistry` and related utilities to use the new title and hierarchy resolution APIs, allowing navigation aids to display correct, dynamic titles and hierarchies for all routes. Co-authored-by: totally-not-ai[bot] <290682512+totally-not-ai[bot]@users.noreply.github.com> Co-authored-by: Artur Signell <artur@vaadin.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Introduces a new, instance-free mechanism for resolving page titles and logical route hierarchies in Vaadin applications. The main focus is on enabling dynamic page titles and hierarchical navigation aids (like breadcrumbs and menus) without requiring instantiation of navigation target components. This is achieved through new annotations, interfaces, and supporting APIs for stateless resolution of titles and parent routes.
@DynamicPageTitleannotation, allowing routes to specify aPageTitleGeneratorfor computing their titles without needing to instantiate the route component. This enables dynamic titles for navigation elements like menus and breadcrumbs.PageTitleGeneratorinterface andPageTitleContextrecord, providing a stateless, parameter-driven way to generate page titles for routes.@RouteParentannotation and supporting APIs, allowing routes to declare their logical parent statically or via a resolver, again without requiring instantiation.RouteConfigurationwith methods to resolve a route's logical parent and hierarchy statelessly, supporting the new navigation aids.InstantiatorandDefaultInstantiatorto support application-wide defaultPageTitleGeneratorresolution, integrating the new mechanism with the dependency injection system.MenuRegistryand related utilities to use the new title and hierarchy resolution APIs, allowing navigation aids to display correct, dynamic titles and hierarchies for all routes.