diff --git a/articles/flow/configuration/properties.adoc b/articles/flow/configuration/properties.adoc index dc3d12d5f4..0e6a522113 100644 --- a/articles/flow/configuration/properties.adoc +++ b/articles/flow/configuration/properties.adoc @@ -277,6 +277,11 @@ Define post-install scripts that need to be executed after npm install completes Default: + Mode: Build +`**pageTitle.generator**`:: +Sets the fully qualified name of an application-wide default `PageTitleGenerator` implementation, used to resolve page titles for routes that don't declare their own `@DynamicPageTitle`. See <<{articles}/flow/routing/page-titles#generating-page-titles, Generating Page Titles>> for details. + +Default: `null` + +Mode: Runtime + `**pnpm.enable**`:: Enables `pnpm`, instead of `npm`, to resolve and download frontend dependencies. It's set by default to `false` since `npm` is used typically. Set it to `true` to enable `pnpm`. See <> for more information. + Default: `false` + diff --git a/articles/flow/routing/navigation.adoc b/articles/flow/routing/navigation.adoc index dffe2f648a..31289a5e99 100644 --- a/articles/flow/routing/navigation.adoc +++ b/articles/flow/routing/navigation.adoc @@ -186,4 +186,42 @@ anchor.getElement().setAttribute("router-ignore", ""); ---- +[[observing-navigation-state]] +== Observing Navigation State [since:com.vaadin:vaadin@V25.2] + +The [methodname]`UI.routerStateSignal()` method returns a read-only signal that holds the current navigation state of the UI as a [classname]`RouterState`. The signal value is updated whenever a navigation completes, immediately before [interfacename]`AfterNavigationListener` implementations are notified, so reactive consumers and listeners observe the same state. + +The signal lets reactive code — breadcrumbs, menus, or anything else that depends on the current location — track navigation without registering an [interfacename]`AfterNavigationListener` and manually fetching the initial state on attach: + +[source,java] +---- +Signal routerState = UI.getCurrent().routerStateSignal(); + +Span currentPath = new Span(); +currentPath.bindText(routerState.map( + state -> "Current path: " + state.location().getPath())); +---- + +[classname]`RouterState` is an immutable record with the following components: + +- [methodname]`location()` — the [classname]`Location` of the current view, including the path, query parameters, and fragment. +- [methodname]`routeParameters()` — the [classname]`RouteParameters` extracted from the URL. +- [methodname]`activeChain()` — an unmodifiable list of the currently active route target and its parent layouts, leaf first. +- [methodname]`navigationTarget()` — the class of the leaf route target. + +The [methodname]`currentView()` method returns the currently shown leaf view as an [classname]`Optional`. + +Before the first navigation completes, the signal value is a [classname]`RouterState` with an empty [classname]`Location`, empty [classname]`RouteParameters`, an empty active chain, and a `null` navigation target. + +Use [methodname]`Signal.get()` to read the value reactively — inside a [methodname]`Signal.effect()` this creates a dependency, so the effect runs again after each navigation — or [methodname]`Signal.peek()` for a non-reactive snapshot. Fine-grained projections can be derived with [methodname]`Signal.map()`, for example a signal holding only the location: + +[source,java] +---- +Signal locationSignal = UI.getCurrent().routerStateSignal() + .map(RouterState::location); +---- + +Combined with the <>, the signal can drive a breadcrumb trail that updates on every navigation. See <<{articles}/flow/ui-state#, Manage UI State with Signals>> for more about signals and effects. + + [discussion-id]`3F7CDDD8-C4FB-44DC-9047-C48EAB57C862` diff --git a/articles/flow/routing/page-titles.adoc b/articles/flow/routing/page-titles.adoc index 31b5e676af..887771c9ba 100644 --- a/articles/flow/routing/page-titles.adoc +++ b/articles/flow/routing/page-titles.adoc @@ -9,9 +9,9 @@ order: 90 = Updating Page Title during Navigation -You can update the page title in two ways during navigation: use the `@PageTitle` annotation; or implement [interfacename]`HasDynamicTitle`. +You can update the page title in three ways during navigation: use the `@PageTitle` annotation; implement [interfacename]`HasDynamicTitle`; or generate the title with a [interfacename]`PageTitleGenerator`. -These approaches are mutually exclusive. Using both in the same class results in a runtime exception at startup. +The `@PageTitle` annotation and [interfacename]`HasDynamicTitle` are mutually exclusive. Using both in the same class results in a runtime exception at startup. == Using the @PageTitle Annotation @@ -64,4 +64,60 @@ class BlogPost extends Component ---- +[[generating-page-titles]] +== Generating Page Titles [since:com.vaadin:vaadin@V25.2] + +A [interfacename]`PageTitleGenerator` resolves the title of a navigation target without requiring an instance of it. While [methodname]`HasDynamicTitle.getPageTitle()` is an instance method and therefore only usable once the view has been created, a generator is resolved purely from the navigation target class and its [classname]`RouteParameters`. This makes it suitable for use cases that need the title of a route that isn't (and shouldn't be) instantiated, such as breadcrumbs, menus, or other navigation aids that render the titles of a whole trail of routes (see <>). + +Reference a generator from a route with the [annotationname]`@DynamicPageTitle` annotation: + +[source,java] +---- +@Route("products/:productId") +@DynamicPageTitle(ProductTitleGenerator.class) +public class ProductView extends Div { + // ... +} + +public class ProductTitleGenerator implements PageTitleGenerator { + @Override + public String generatePageTitle(PageTitleContext context) { + String id = context.routeParameters().get("productId").orElse(""); + return "Product " + id; + } +} +---- + +The [classname]`PageTitleContext` record exposes the navigation target class, the [classname]`RouteParameters` and [classname]`QueryParameters` the target would be navigated to with, and the `@PageTitle` value declared on the route. + +A `@PageTitle` value may be declared alongside [annotationname]`@DynamicPageTitle`. It's then handed to the generator through [methodname]`PageTitleContext.value()` — for example as an i18n message key — rather than used as the title directly. + +Generator implementations must be stateless and cheap to create: they're instantiated through the application [interfacename]`Instantiator` — so dependency injection is available — every time a title is resolved. The navigation target itself is never instantiated. + + +=== Application-Wide Title Generator + +Instead of declaring [annotationname]`@DynamicPageTitle` on every route, a single default generator can be defined for the whole application with the `pageTitle.generator` init parameter, set to the fully qualified class name of a [interfacename]`PageTitleGenerator` implementation: + +.application.properties +[source,properties] +---- +vaadin.pageTitle.generator=com.example.MyTitleGenerator +---- + +The default generator applies to every route that doesn't declare its own [annotationname]`@DynamicPageTitle`. A typical use is a single generator that turns the declared `@PageTitle` value, passed in through [methodname]`PageTitleContext.value()`, into a translated title for every route. + +Custom [interfacename]`Instantiator` implementations can alternatively provide the default generator by overriding [methodname]`Instantiator.getPageTitleGenerator()`. + + +=== Title Resolution Order + +When a navigation completes, the title is resolved in this order: + +. [interfacename]`HasDynamicTitle`, implemented by the navigation target or one of its parent layouts; +. the per-route [annotationname]`@DynamicPageTitle` generator; +. the application-wide default [interfacename]`PageTitleGenerator`, if one is defined; +. the static `@PageTitle` value. + + [discussion-id]`BCB28141-05D7-4DF0-AC9C-C0D73C4FC97D` diff --git a/articles/flow/routing/route-hierarchy.adoc b/articles/flow/routing/route-hierarchy.adoc new file mode 100644 index 0000000000..4952fc9155 --- /dev/null +++ b/articles/flow/routing/route-hierarchy.adoc @@ -0,0 +1,123 @@ +--- +title: Route Hierarchy +page-title: How to define a logical route hierarchy in Vaadin +description: How to declare logical parent routes and resolve the route hierarchy for breadcrumbs and menus. +meta-description: Learn how to declare logical parent routes in Vaadin and resolve the route hierarchy to build breadcrumbs and hierarchical menus. +order: 95 +--- + + += [since:com.vaadin:vaadin@V25.2]#Route Hierarchy# + +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 [annotationname]`@Route` `layout` and [annotationname]`@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 [annotationname]`@RouteParent` annotation to declare the logical parent of a navigation target: + +[source,java] +---- +@Route("dashboard") +public class DashboardView extends Div { +} + +@Route("reports") +@RouteParent(DashboardView.class) +public class ReportsView extends Div { +} +---- + +The parent inherits the [classname]`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 [interfacename]`RouteParentResolver` through the `resolver` attribute of [annotationname]`@RouteParent`. The resolver receives a [classname]`RouteParentContext` describing the navigation target class and its [classname]`RouteParameters`, and returns a [classname]`RouteParentReference` pointing to the parent: + +[source,java] +---- +@Route("orgs/:orgId/projects/:projectId") +@RouteParent(resolver = OrgParentResolver.class) +public class ProjectView extends Div { +} + +public class OrgParentResolver implements RouteParentResolver { + @Override + public Optional 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 [classname]`RouteParentReference` carries both the parent navigation target class and the [classname]`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 [classname]`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 [interfacename]`Instantiator` — so dependency injection is available — every time a parent is resolved. The route itself is never instantiated. + + +== URL-Derived Parents + +Routes without a [annotationname]`@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; [annotationname]`@RouteParent` is only needed when the logical hierarchy differs from the URL paths. + + +== Resolving the Hierarchy + +[classname]`RouteConfiguration` exposes the resolved hierarchy: + +- [methodname]`getRouteParent(Class, RouteParameters)` resolves the logical parent of a navigation target, returning an [classname]`Optional`. +- [methodname]`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 <>, the hierarchy can be used to render a breadcrumb trail that updates on every navigation: + +[source,java] +---- +public class Breadcrumbs extends Div { + + public Breadcrumbs() { + Signal 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(); + for (RouteParentReference entry : configuration.getRouteHierarchy( + state.navigationTarget(), state.routeParameters())) { + add(new RouterLink(resolveTitle(entry), entry.navigationTarget(), + entry.routeParameters())); + } + } +} +---- + +The [methodname]`resolveTitle()` helper in the example resolves the title of each entry. Because none of the ancestor routes are instantiated, instance-based title APIs such as [interfacename]`HasDynamicTitle` can't be used for this; a [interfacename]`PageTitleGenerator` resolves titles from the navigation target class and its route parameters alone: + +[source,java] +---- +private String resolveTitle(RouteParentReference entry) { + PageTitleGenerator generator = new MyTitleGenerator(); + return generator.generatePageTitle(new PageTitleContext( + entry.navigationTarget(), entry.routeParameters(), + QueryParameters.empty(), "")); +} +---- + +See <> for how to declare title generators on routes and application-wide. + + +[discussion-id]`7E1B2F64-9C45-4A1B-BF1F-2D5E8A0C3D91` diff --git a/articles/flow/ui-state/building-ui.adoc b/articles/flow/ui-state/building-ui.adoc index 03bbcdc3a9..9266cd3866 100644 --- a/articles/flow/ui-state/building-ui.adoc +++ b/articles/flow/ui-state/building-ui.adoc @@ -620,6 +620,22 @@ SharedValueSignal sessionLocale = VaadinSession.getCurrent().localeSigna ---- +=== Router State Signal [since:com.vaadin:vaadin@V25.2] + +Use [methodname]`UI.routerStateSignal()` to get a read-only signal that tracks the UI's navigation state. The signal provides a [classname]`RouterState` record with the current location, route parameters, active route chain, and navigation target. Its value updates whenever a navigation completes, making it useful for reactive breadcrumbs and menus: + +[source,java] +---- +Signal routerState = UI.getCurrent().routerStateSignal(); + +Span currentPath = new Span(); +currentPath.bindText(routerState.map( + state -> "Current path: " + state.location().getPath())); +---- + +See <<{articles}/flow/routing/navigation#observing-navigation-state, Observing Navigation State>> for details. + + == Complete Example Here's a complete example combining multiple binding types: