Skip to content
Draft
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
4 changes: 2 additions & 2 deletions articles/building-apps/views/add-master-detail.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,9 @@ The [methodname]`addBackdropClickListener()` and [methodname]`addDetailEscapePre

The signal keeps the selection in memory, which is enough for many views. It isn't preserved across a page reload, though, and it can't be shared as a link. If you need the selected item to be bookmarkable or to survive a refresh, represent it in the URL.

To do this, keep the selection signal as the source of truth and synchronize it with a <<pass-data/query-parameters#,query parameter>> or <<pass-data/route-parameters#,route parameter>>: update the URL when the signal changes, and initialize the signal from the URL on navigation. The detail area still follows the signal reactively; only the source that drives the signal changes.
To do this, keep the selection signal as the source of truth and synchronize it with a <<pass-data/route-parameters#,route parameter>> or <<pass-data/query-parameters#,query parameter>>: initialize the signal from the URL when the view is entered, and navigate to a new URL when the signal changes. The detail area still follows the signal reactively; only the source that drives the signal changes.

[NOTE]
Reading and writing query and route parameters directly as signals is planned for a future release, which will make synchronizing the selection with the URL even more straightforward.
The current route and query parameters are available as a read-only signal through [methodname]`UI.getCurrentOrThrow().routerStateSignal()`, so the URL can drive the selection reactively; you write changes back by navigating. The <<pass-data/route-parameters#,Route Parameters>>, <<pass-data/route-templates#,Route Templates>>, and <<pass-data/query-parameters#,Query Parameters>> guides show the patterns for each.

With the master, the selection signal, and the reactive detail in place, the master-detail view is complete. The next step is to integrate it into your application and enhance it with real data and more complex UI components as needed.
82 changes: 26 additions & 56 deletions articles/building-apps/views/pass-data/query-parameters.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
= Query Parameters
:toclevels: 2

In this guide, you'll learn how to access and set query parameters in a Vaadin view.
In this guide, you'll learn how to read and set query parameters in a Vaadin view, and how to bind a field to a query parameter with a <</flow/ui-state#,signal>> so the two stay in sync.


== Copy-Paste into Your Project
Expand All @@ -22,7 +22,7 @@
include::{root}/src/main/java/com/vaadin/demo/buildingapps/passdata/QueryParameterView.java[tags=snippet,indent=0]
----

For more detailed instructions on how to use route parameters, continue reading below.
This view reads the `filter` query parameter from the router state signal and two-way binds it to a text field: the field shows the current value, and editing it updates the URL. For more detailed instructions on how to use query parameters, continue reading below.


== What Are Query Parameters?
Expand All @@ -42,45 +42,28 @@
If you need required parameters, consider using <<route-parameters#,route parameters>> instead.


== Accessing Query Parameter Values
== Reading Query Parameters Reactively

Check failure on line 45 in articles/building-apps/views/pass-data/query-parameters.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vale.Terms] Use 'reactively' instead of 'Reactively'. Raw Output: {"message": "[Vale.Terms] Use 'reactively' instead of 'Reactively'.", "location": {"path": "articles/building-apps/views/pass-data/query-parameters.adoc", "range": {"start": {"line": 45, "column": 29}}}, "severity": "ERROR"}

Check warning on line 45 in articles/building-apps/views/pass-data/query-parameters.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.HeadingCase] 'Reading Query Parameters Reactively' should be in title case. Raw Output: {"message": "[Vaadin.HeadingCase] 'Reading Query Parameters Reactively' should be in title case.", "location": {"path": "articles/building-apps/views/pass-data/query-parameters.adoc", "range": {"start": {"line": 45, "column": 4}}}, "severity": "WARNING"}

You access query parameters similarly to <<route-templates#accessing-route-parameter-values,multiple route parameters>>. Your view must implement the [interfacename]`BeforeEnterObserver` interface, which defines the [methodname]`beforeEnter()` method. Inside this method, you can retrieve query parameters:
Query parameters are part of the current location, which is available as a <</flow/ui-state#,signal>>. Calling [methodname]`UI.getCurrentOrThrow().routerStateSignal()` returns a read-only signal whose value is a [classname]`RouterState` record; its [methodname]`location()` method gives you the [classname]`Location`, and [methodname]`getQueryParameters()` the [classname]`QueryParameters` for the active navigation.

As with <<route-templates#accessing-route-parameter-values,route templates>>, a convenient pattern is to derive a separate signal for each parameter. Because [interfacename]`Signal` is a functional interface, you can define one with a lambda that reads the value from the location:

[source,java]
----
@Route
public class OrdersView extends Main implements BeforeEnterObserver {
private static final String QUERY_PARAM_FILTER = "filter"; // <1>

private static final String QUERY_PARAM_SORT = "sort"; // <1>
private static final String QUERY_PARAM_FILTER = "filter";

@Override
public void beforeEnter(BeforeEnterEvent event) {
event.getLocation().getQueryParameters().getSingleParameter(QUERY_PARAM_SORT)
.ifPresent(sort -> {
// Process the sort parameter
});
event.getLocation().getQueryParameters().getSingleParameter(QUERY_PARAM_FILTER)
.ifPresent(filter -> {
// Process the filter parameter
});
}
...
}
private final Signal<String> filterParam = () -> UI.getCurrentOrThrow()
.routerStateSignal().get().location().getQueryParameters()
.getSingleParameter(QUERY_PARAM_FILTER).orElse("");
----
<1> *Tip:* To improve readability and maintainability, declare query parameter names as constants.

Now, if you navigate to `/orders?sort=date&filter=shipped`, the query parameter values will be:

* `sort` -> `"date"`
* `filter` -> `"shipped"`
Bind components to these parameter signals as you would to any other signal. Reading the value inside a binding's reactive context makes the component update automatically whenever the parameter changes.

The [classname]`QueryParameters` class provides methods for accessing query parameter values. Familiarize yourself with its API if you frequently use query parameters in your application.
The [classname]`QueryParameters` class provides methods for accessing query parameter values; familiarize yourself with its API if you use query parameters often. Since query parameters are always strings and always optional, they may be missing or contain invalid data, so always supply a default with [methodname]`orElse()` and validate as needed.

[NOTE]
You can also retrieve query parameters using [methodname]`UI.getCurrent().getActiveViewLocation().getQueryParameters()`.

Since query parameters are always strings and optional, they may be empty or contain invalid data. To handle this, you can provide default values, redirect users to a different view, or display an error message.
If you need to inspect query parameters imperatively — for example, to redirect before the view renders — implement [interfacename]`BeforeEnterObserver` and read [methodname]`event.getLocation().getQueryParameters()` inside [methodname]`beforeEnter()`.


== Setting Query Parameters
Expand Down Expand Up @@ -112,37 +95,24 @@
You _cannot_ set query parameters to `null`. To clear a query parameter, exclude it from the [classname]`QueryParameters` object.


=== Updating Query Parameters Dynamically
== Binding a Field to a Query Parameter

Query parameters are often set dynamically, such as when users apply filters or change sort options. In such cases, the view must navigate to itself while updating query parameters.

When navigating to the _same_ view, the existing view instance is reused. The browser URL updates, and [methodname]`beforeEnter()` is invoked again.

The following example updates the `filter` query parameter when a user types in a text field:
Query parameters often back a form field directly — a filter, a search term, or a sort option. With the parameter exposed as a signal, you can two-way bind a field to it using [methodname]`bindValue()`: the field shows the current value, and edits write back to the URL.

[source,java]
----
@Route
public class OrdersView extends Main implements BeforeEnterObserver {

private static final String QUERY_PARAM_SORT = "sort";
private static final String QUERY_PARAM_FILTER = "filter";
filterField.bindValue(filterParam, this::setFilter); // <1>

// tag::snippet[]
public OrdersView() {
var filterField = new TextField();
add(filterField);

filterField.addValueChangeListener(event -> {
var queryParameters = UI.getCurrent().getActiveViewLocation()
.getQueryParameters()
.merging(QUERY_PARAM_FILTER, event.getValue());
UI.getCurrent().navigate(OrdersView.class, queryParameters);
});
}
// end::snippet[]
...
private void setFilter(String filter) {
var queryParameters = UI.getCurrent().getActiveViewLocation()
.getQueryParameters()
.merging(QUERY_PARAM_FILTER, filter); // <2>
UI.getCurrent().navigate(QueryParameterView.class, queryParameters);
}
----
<1> `filterParam` is the parameter signal from <<#reading-query-parameters-reactively,Reading Query Parameters Reactively>>; [methodname]`setFilter()` is called whenever the field changes.

Check failure on line 113 in articles/building-apps/views/pass-data/query-parameters.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vale.Terms] Use 'reactively' instead of 'Reactively'. Raw Output: {"message": "[Vale.Terms] Use 'reactively' instead of 'Reactively'.", "location": {"path": "articles/building-apps/views/pass-data/query-parameters.adoc", "range": {"start": {"line": 113, "column": 112}}}, "severity": "ERROR"}
<2> [methodname]`merging()` replaces only the `filter` parameter and leaves any others in the URL intact.

When the user edits the field, [methodname]`setFilter()` navigates the view to itself with the updated query parameters. The existing view instance is reused, the browser URL updates, and the router state signal — and therefore the bound field — reflects the new value. The binding works in both directions, so the field also updates if the URL changes by other means, such as the browser's back button.

If the original URL was `/orders?sort=date&filter=shipped`, and the user enters "foobar" in the text field, the URL updates to `/orders?sort=date&filter=foobar`.
[methodname]`merging()` preserves the other parameters: if the URL was `/orders?sort=date&filter=shipped` and the user enters "foobar", it becomes `/orders?sort=date&filter=foobar`.
48 changes: 46 additions & 2 deletions articles/building-apps/views/pass-data/route-parameters.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ order: 10
= Route Parameters
:toclevels: 2

In this guide, you'll learn how to create a view that accepts a single route parameter. You'll also explore the differences between optional and wildcard route parameters.
In this guide, you'll learn how to create a view that accepts a single route parameter, holds it in a <</flow/ui-state#,signal>> so the UI updates reactively, and keeps the browser URL in sync. You'll also explore the differences between optional and wildcard route parameters.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the order on this page could be improved as it reads a bit like:

  1. Accessing the Route Parameter Value
  2. Optional Parameters
  3. Wildcard Parameters
  4. Naah, don't actually do as shown above, do it like this (Holding the Parameter in a Signal)

It could be restructured so you start from "This is how you get a parameter into the UI through a signal", then "optional parameters" -> "wildcard parameters"

I'm not sure you even need to explicitly spell out that you don't have to use a signal, or maybe spell it out like that in the first part.



== Copy-Paste into Your Project
Expand All @@ -22,7 +22,7 @@ If you want to quickly try out route parameters in your Vaadin application, copy
include::{root}/src/main/java/com/vaadin/demo/buildingapps/passdata/RouteParameterView.java[tags=snippet,indent=0]
----

For more detailed instructions on how to use route parameters, continue reading below.
This view stores the route parameter in a signal, binds its text to a value derived from that signal, and updates the URL whenever the signal changes. For more detailed instructions on how to use route parameters, continue reading below.


== What Are Route Parameters?
Expand Down Expand Up @@ -138,3 +138,47 @@ Now, navigating to `/customers/cu1234/edit` passes `"cu1234/edit"` as the parame
An empty wildcard parameter is an empty string (`""`), while an empty optional parameter is `null`. So, navigating to `/customers` calls [methodname]`setParameter()` with `""` instead of `null`.

If you’re considering wildcard parameters because you need multiple route parameters, <<route-templates#,Route Templates>> may be a better solution.


== Holding the Parameter in a Signal

The [classname]`CustomerView` examples above set text directly inside [methodname]`setParameter()`. That works for trivial views, but as soon as several parts of the UI depend on the parameter, you have to remember to update each of them by hand every time the value changes.

A cleaner approach — the one used by the <<#copy-paste-into-your-project,copy-paste example>> at the top of this guide — is to store the parameter in a <</flow/ui-state#,signal>> and let the UI derive from it. The signal becomes the single source of truth: [methodname]`setParameter()` only writes the incoming value to the signal, and every component that depends on the parameter binds to it and updates itself automatically.

[source,java]
----
private final ValueSignal<String> parameter = new ValueSignal<>(null);

@Override
public void setParameter(BeforeEvent event, @OptionalParameter String parameterValue) {
parameter.set(parameterValue);
}
----

The parameter is held in a [classname]`ValueSignal<String>`, initialized to `null` for the "no value" case. When the router calls [methodname]`setParameter()`, it writes the incoming value — which may be `null` for an <<#optional-parameters,optional parameter>> — straight to the signal.

Components don't read the parameter directly. Instead, they bind to the signal and re-render whenever it changes. The copy-paste example binds a [classname]`Paragraph` with [methodname]`bindText()`:

[source,java]
----
parameterValue.bindText(() -> Optional.ofNullable(parameter.get())
.map(value -> "Parameter value: " + value)
.orElse("No parameter value provided"));
----

Anything else that depends on the parameter — an enabled state, a visible flag, a nested component — can bind to the same signal in the same way. See <</flow/ui-state/building-ui#,Component Bindings>> for the full set of binding methods.


== Keeping the URL in Sync

Because the signal is the source of truth, the URL should follow it: when the value changes, the browser's address bar should update to match. A <</flow/ui-state/effects-computed#,signal effect>> handles this. Reading `parameter.get()` inside the effect makes it re-run whenever the value changes, navigating the view to itself with the current parameter:

[source,java]
----
Signal.effect(this,
() -> UI.getCurrent().navigate(RouteParameterView.class,
parameter.get()));
----

The effect is bound to the view's lifecycle, so it's active only while the view is attached. Navigating the view to itself reuses the existing instance and updates the URL; because the target location matches the current one whenever [methodname]`setParameter()` was the source of the change, this doesn't cause a navigation loop.
Loading
Loading