Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions articles/flow/configuration/properties.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<development-mode/npm-pnpm-bun#, Switching Between npm, pnpm and bun>> for more information. +
Default: `false` +
Expand Down
38 changes: 38 additions & 0 deletions articles/flow/routing/navigation.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,42 @@
----


[[observing-navigation-state]]
== Observing Navigation State [since:com.vaadin:vaadin@V25.2]

Check failure on line 190 in articles/flow/routing/navigation.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vale.Terms] Use '(?-i)Vaadin' instead of 'vaadin'. Raw Output: {"message": "[Vale.Terms] Use '(?-i)Vaadin' instead of 'vaadin'.", "location": {"path": "articles/flow/routing/navigation.adoc", "range": {"start": {"line": 190, "column": 49}}}, "severity": "ERROR"}

Check failure on line 190 in articles/flow/routing/navigation.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vale.Terms] Use '(?-i)Vaadin' instead of 'vaadin'. Raw Output: {"message": "[Vale.Terms] Use '(?-i)Vaadin' instead of 'vaadin'.", "location": {"path": "articles/flow/routing/navigation.adoc", "range": {"start": {"line": 190, "column": 42}}}, "severity": "ERROR"}

Check warning on line 190 in articles/flow/routing/navigation.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.HeadingCase] 'Observing Navigation State [since:com.vaadin:vaadin@V25.2]' should be in title case. Raw Output: {"message": "[Vaadin.HeadingCase] 'Observing Navigation State [since:com.vaadin:vaadin@V25.2]' should be in title case.", "location": {"path": "articles/flow/routing/navigation.adoc", "range": {"start": {"line": 190, "column": 4}}}, "severity": "WARNING"}

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> 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<Location> locationSignal = UI.getCurrent().routerStateSignal()
.map(RouterState::location);
----

Combined with the <<route-hierarchy#, route hierarchy>>, 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`
60 changes: 58 additions & 2 deletions articles/flow/routing/page-titles.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@

= 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
Expand Down Expand Up @@ -64,4 +64,60 @@
----


[[generating-page-titles]]
== Generating Page Titles [since:com.vaadin:vaadin@V25.2]

Check failure on line 68 in articles/flow/routing/page-titles.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vale.Terms] Use '(?-i)Vaadin' instead of 'vaadin'. Raw Output: {"message": "[Vale.Terms] Use '(?-i)Vaadin' instead of 'vaadin'.", "location": {"path": "articles/flow/routing/page-titles.adoc", "range": {"start": {"line": 68, "column": 45}}}, "severity": "ERROR"}

Check failure on line 68 in articles/flow/routing/page-titles.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vale.Terms] Use '(?-i)Vaadin' instead of 'vaadin'. Raw Output: {"message": "[Vale.Terms] Use '(?-i)Vaadin' instead of 'vaadin'.", "location": {"path": "articles/flow/routing/page-titles.adoc", "range": {"start": {"line": 68, "column": 38}}}, "severity": "ERROR"}

Check warning on line 68 in articles/flow/routing/page-titles.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.HeadingCase] 'Generating Page Titles [since:com.vaadin:vaadin@V25.2]' should be in title case. Raw Output: {"message": "[Vaadin.HeadingCase] 'Generating Page Titles [since:com.vaadin:vaadin@V25.2]' should be in title case.", "location": {"path": "articles/flow/routing/page-titles.adoc", "range": {"start": {"line": 68, "column": 4}}}, "severity": "WARNING"}

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 <<route-hierarchy#, Route Hierarchy>>).

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`
123 changes: 123 additions & 0 deletions articles/flow/routing/route-hierarchy.adoc
Original file line number Diff line number Diff line change
@@ -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<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 [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<RouteParentReference>`.
- [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 <<navigation#observing-navigation-state, router state signal>>, 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> 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 <<page-titles#generating-page-titles, Generating Page Titles>> for how to declare title generators on routes and application-wide.


[discussion-id]`7E1B2F64-9C45-4A1B-BF1F-2D5E8A0C3D91`
16 changes: 16 additions & 0 deletions articles/flow/ui-state/building-ui.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,22 @@
----


=== Router State Signal [since:com.vaadin:vaadin@V25.2]

Check failure on line 623 in articles/flow/ui-state/building-ui.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vale.Terms] Use '(?-i)Vaadin' instead of 'vaadin'. Raw Output: {"message": "[Vale.Terms] Use '(?-i)Vaadin' instead of 'vaadin'.", "location": {"path": "articles/flow/ui-state/building-ui.adoc", "range": {"start": {"line": 623, "column": 43}}}, "severity": "ERROR"}

Check failure on line 623 in articles/flow/ui-state/building-ui.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vale.Terms] Use '(?-i)Vaadin' instead of 'vaadin'. Raw Output: {"message": "[Vale.Terms] Use '(?-i)Vaadin' instead of 'vaadin'.", "location": {"path": "articles/flow/ui-state/building-ui.adoc", "range": {"start": {"line": 623, "column": 36}}}, "severity": "ERROR"}

Check warning on line 623 in articles/flow/ui-state/building-ui.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.HeadingCase] 'Router State Signal [since:com.vaadin:vaadin@V25.2]' should be in title case. Raw Output: {"message": "[Vaadin.HeadingCase] 'Router State Signal [since:com.vaadin:vaadin@V25.2]' should be in title case.", "location": {"path": "articles/flow/ui-state/building-ui.adoc", "range": {"start": {"line": 623, "column": 5}}}, "severity": "WARNING"}

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> 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:
Expand Down
Loading