From 7c0aba3190ef9f6e9591c903b4c4bf83b194fb61 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Fri, 12 Jun 2026 11:06:35 +0300 Subject: [PATCH] docs: update signal docs for 25.2 (cached/computed, read-only localeSignal, bulk inserts, component bindings) --- articles/flow/ui-state/building-ui.adoc | 140 +++++++++++++++++-- articles/flow/ui-state/effects-computed.adoc | 29 ++-- articles/flow/ui-state/local-signals.adoc | 23 +++ articles/flow/ui-state/shared-signals.adoc | 26 ++++ 4 files changed, 198 insertions(+), 20 deletions(-) diff --git a/articles/flow/ui-state/building-ui.adoc b/articles/flow/ui-state/building-ui.adoc index 03bbcdc3a9..211b09dedd 100644 --- a/articles/flow/ui-state/building-ui.adoc +++ b/articles/flow/ui-state/building-ui.adoc @@ -77,15 +77,15 @@ firstName.set("Jane"); // Automatically updates to "Jane Doe" ---- -For expensive computations, wrap the lambda in [methodname]`Signal.computed()` to cache the result: +For expensive computations, wrap the computation in [methodname]`Signal.cached()` to cache the result: [source,java] ---- -fullName.bindText(Signal.computed(() -> - firstName.get() + " " + lastName.get())); +fullName.bindText(Signal.cached(Signal.computed(() -> + firstName.get() + " " + lastName.get()))); ---- -Both variants execute the callback when a dependency signal value changes. The difference is that the lambda variant recalculates the result every time someone reads the lambda signal value, while an explicit computed signal (using [methodname]`Signal.computed()`) caches the result until any dependency signal value changes. For simple operations like [methodname]`String::length`, a lambda is enough. However, for complex or expensive computations, use an explicit computed signal to avoid unnecessary recalculations when the value is read multiple times. +Both variants execute the callback when a dependency signal value changes. The difference is that a plain lambda or [methodname]`Signal.computed()` signal recalculates the result every time someone reads the signal value, while a cached signal (using [methodname]`Signal.cached()`) stores the result and reuses it until any dependency signal value changes. For simple operations like [methodname]`String::length`, a lambda is enough. However, for complex or expensive computations, use [methodname]`Signal.cached()` to avoid unnecessary recalculations when the value is read multiple times. See <> for details. [TIP] @@ -567,6 +567,110 @@ This approach works with both [classname]`ListSignal` (for local, single-user sc Use [methodname]`bindChildren()` when you want to create custom components for each item in a layout container (e.g., [classname]`VerticalLayout`). Use [methodname]`Signal.effect()` with [methodname]`setItems()` when you want to populate a data items component (e.g., [classname]`Grid`, [classname]`VirtualList`) that manages its own rendering. +[role="since:com.vaadin:vaadin@V25.2"] +== Component-Specific Bindings + +In addition to the general-purpose bindings, some components provide binding methods for state that is specific to that component. Like the other binding methods, they return a [classname]`SignalBinding` that can be used to register `onChange` callbacks or to unbind. Two-way bindings accept a write callback as the second argument; pass `null` for one-way binding. + + +=== Grid Selection Binding + +Bind the selection of a [classname]`Grid` to a signal through the single-select or multi-select wrapper. Use [methodname]`asSingleSelect().bindValue()` to bind the selected item: + +[source,java] +---- +ValueSignal selectedPerson = new ValueSignal<>(); + +Grid grid = new Grid<>(Person.class); +grid.asSingleSelect().bindValue(selectedPerson, selectedPerson::set); + +// Selecting a row updates the signal; +// setting the signal updates the selection +---- + +Use [methodname]`asMultiSelect().bindValue()` to bind the set of selected items to a `Signal>`: + +[source,java] +---- +ValueSignal> selectedPersons = new ValueSignal<>(Set.of()); + +Grid grid = new Grid<>(Person.class); +grid.setSelectionMode(Grid.SelectionMode.MULTI); +grid.asMultiSelect().bindValue(selectedPersons, selectedPersons::set); +---- + + +=== MessageList Items Binding + +Use [methodname]`bindItems()` to bind the items of a [classname]`MessageList` to a signal holding a list of item signals, such as a [classname]`ListSignal`. The rendered messages update when the list structure or any individual item signal changes: + +[source,java] +---- +ListSignal messages = new ListSignal<>(); + +MessageList messageList = new MessageList(); +messageList.bindItems(messages); + +messages.insertLast(new MessageListItem("Hello!", Instant.now(), "Alice")); +// The new message appears in the list +---- + +As a shorthand, [classname]`MessageList` also has a constructor that binds the items signal directly: + +[source,java] +---- +MessageList messageList = new MessageList(messages); +---- + +While the binding is active, modifying the items manually through [methodname]`setItems()` or [methodname]`addItem()` throws a [classname]`BindingActiveException`. + + +=== AppLayout Drawer Binding + +Use [methodname]`bindDrawerOpened()` to bind the drawer state of an [classname]`AppLayout` to a boolean signal: + +[source,java] +---- +ValueSignal drawerOpened = new ValueSignal<>(true); + +AppLayout appLayout = new AppLayout(); +appLayout.bindDrawerOpened(drawerOpened, drawerOpened::set); + +// Toggling the drawer in the browser updates the signal; +// setting the signal opens or closes the drawer +---- + + +=== Checkbox Indeterminate Binding + +Use [methodname]`bindIndeterminate()` to bind the indeterminate state of a [classname]`Checkbox` to a boolean signal. A typical use case is a "Select All" checkbox indicating that some, but not all, items are selected: + +[source,java] +---- +ValueSignal indeterminate = new ValueSignal<>(true); + +Checkbox selectAll = new Checkbox("Select all"); +selectAll.bindIndeterminate(indeterminate, indeterminate::set); +---- + + +=== Theme Variant Binding + +Use [methodname]`bindThemeVariants()` to bind a list of theme variants to a signal. This method is available on components that implement [interfacename]`HasThemeVariant` and is a typed alternative to [methodname]`bindThemeNames()` that works with theme variant enums instead of raw theme name strings: + +[source,java] +---- +ValueSignal> variants = + new ValueSignal<>(List.of(ButtonVariant.LUMO_PRIMARY)); + +Button saveButton = new Button("Save"); +saveButton.bindThemeVariants(variants); + +// Update all theme variants at once +variants.set(List.of(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SMALL)); +---- + + == Built-In Signal Sources Vaadin provides several built-in signals that expose framework-level state reactively. @@ -595,28 +699,44 @@ The signal is lazily initialized. The actual window dimensions are available fro === UI Locale Signal -Use [methodname]`UI.localeSignal()` to get a writable signal that tracks the UI's locale: +Use [methodname]`UI.localeSignal()` to get a read-only signal that tracks the UI's locale: [source,java] ---- -ValueSignal locale = UI.getCurrent().localeSignal(); +Signal locale = UI.getCurrent().localeSignal(); Span greeting = new Span(); greeting.bindText(locale.map(loc -> loc.getLanguage().equals("fi") ? "Tervetuloa" : "Welcome")); ---- -[WARNING] -Writing directly to this signal does not notify [interfacename]`LocaleChangeObserver` implementations. If you need observers to be notified, use [methodname]`UI.setLocale()` instead. +The signal cannot be written directly. To change the locale, use [methodname]`UI.setLocale()`, which updates the signal and also notifies [interfacename]`LocaleChangeObserver` implementations. For two-way binding with a field component, pass [methodname]`setLocale()` as the write callback: + +[source,java] +---- +UI ui = UI.getCurrent(); + +ComboBox languageSelect = new ComboBox<>("Language"); +languageSelect.setItems(Locale.ENGLISH, Locale.of("fi")); +languageSelect.bindValue(ui.localeSignal(), ui::setLocale); +---- === Session Locale Signal -Use [methodname]`VaadinSession.localeSignal()` to get a shared, writable signal that tracks the session-level locale. UIs that have not had their locale explicitly overridden derive their locale from this signal: +Use [methodname]`VaadinSession.localeSignal()` to get a read-only signal that tracks the session-level locale. UIs that have not had their locale explicitly overridden derive their locale from this signal: + +[source,java] +---- +VaadinSession session = VaadinSession.getCurrent(); +Signal sessionLocale = session.localeSignal(); +---- + +To change the session locale, use [methodname]`VaadinSession.setLocale()`. As with the UI locale signal, the signal can be combined with [methodname]`setLocale()` as the write callback for two-way binding: [source,java] ---- -SharedValueSignal sessionLocale = VaadinSession.getCurrent().localeSignal(); +languageSelect.bindValue(session.localeSignal(), session::setLocale); ---- diff --git a/articles/flow/ui-state/effects-computed.adoc b/articles/flow/ui-state/effects-computed.adoc index 851a4615fe..81da096827 100644 --- a/articles/flow/ui-state/effects-computed.adoc +++ b/articles/flow/ui-state/effects-computed.adoc @@ -112,7 +112,7 @@ Signal fullName = Signal.computed(() -> { }); ---- -Computed signals are read-only; you cannot set their value directly. +Computed signals are read-only; you cannot set their value directly. The computation callback runs every time the signal value is read. For expensive computations, wrap the computed signal in [methodname]`Signal.cached()` to avoid repeating the work on every read (see <<#caching-with-signal-cached,Caching with Signal.cached()>>). === Combining Multiple Signals @@ -136,22 +136,31 @@ quantity.set(3.0); ---- -=== Caching and Lazy Evaluation +[[caching-with-signal-cached]] +[role="since:com.vaadin:vaadin@V25.2"] +=== Caching with Signal.cached() -Computed signals cache their value and only recalculate when dependencies change: +A computed signal created with [methodname]`Signal.computed()` doesn't cache its value: the computation callback runs every time the value is read. This keeps the signal lightweight, but reading the value of an expensive computation from several places repeats the work. + +Use [methodname]`Signal.cached()` to add caching on top of a computed signal. A cached signal stores the most recently computed value and returns it on subsequent reads. The cached value remains valid until the value of any dependency signal changes: [source,java] ---- -Signal expensiveComputation = Signal.computed(() -> { - // This only runs when dependencies change - return performExpensiveCalculation(inputSignal.get()); -}); +Signal expensiveComputation = Signal.cached(Signal.computed(() -> + performExpensiveCalculation(inputSignal.get()))); + +expensiveComputation.get(); // Computes the value +expensiveComputation.get(); // Returns the cached value -// Multiple reads return the cached value -expensiveComputation.get(); // Computes once -expensiveComputation.get(); // Returns cached value +inputSignal.set(newInput); +expensiveComputation.get(); // Recomputes because a dependency changed ---- +Use plain [methodname]`Signal.computed()` for cheap derivations such as string concatenation or simple arithmetic, where the bookkeeping overhead of caching would outweigh the cost of recomputing the value. Use [methodname]`Signal.cached()` when the computation is expensive — for example, filtering or sorting a large list — and the value is read multiple times. + +[NOTE] +An effect or an outer cached signal that uses the value from a cached signal isn't re-run when the inner computation is invalidated but produces the same value as before. This makes [methodname]`Signal.cached()` useful for cutting off update chains when an expensive intermediate result is unchanged. + == Signal Mapping diff --git a/articles/flow/ui-state/local-signals.adoc b/articles/flow/ui-state/local-signals.adoc index 8a017f010e..ee5211a64d 100644 --- a/articles/flow/ui-state/local-signals.adoc +++ b/articles/flow/ui-state/local-signals.adoc @@ -151,6 +151,29 @@ ValueSignal middleItem = items.insertAt(1, "Middle"); Each insert method returns a [classname]`ValueSignal` representing the entry. You can use this signal to update or remove the entry later. +[role="since:com.vaadin:vaadin@V25.2"] +=== Adding Multiple Items + +Use [methodname]`insertAllFirst()`, [methodname]`insertAllLast()`, or [methodname]`insertAllAt()` to insert several items in one operation. All entries are added with a single change notification, which is more efficient than inserting items one by one: + +[source,java] +---- +ListSignal items = new ListSignal<>(); +items.insertLast("Existing"); + +// Insert at the end, preserving the collection order +List> added = items.insertAllLast(List.of("A", "B")); + +// Insert at the beginning +items.insertAllFirst(List.of("First", "Second")); + +// Insert at a specific index (0-indexed) +items.insertAllAt(1, List.of("X", "Y")); +---- + +Each method returns an unmodifiable list of [classname]`ValueSignal` instances for the inserted entries, in the same order as the provided collection. + + === Removing Items Use [methodname]`remove()` to remove a specific entry, or [methodname]`clear()` to remove all entries: diff --git a/articles/flow/ui-state/shared-signals.adoc b/articles/flow/ui-state/shared-signals.adoc index f02aada51c..47f49f85db 100644 --- a/articles/flow/ui-state/shared-signals.adoc +++ b/articles/flow/ui-state/shared-signals.adoc @@ -112,6 +112,32 @@ items.insertAt("New Last", ListPosition.last()); ---- +[role="since:com.vaadin:vaadin@V25.2"] +==== Inserting Multiple Items + +Use [methodname]`insertAllFirst()`, [methodname]`insertAllLast()`, or [methodname]`insertAllAt()` to insert several items in one operation. All inserts are performed within a single transaction, so other users see the entire batch as one atomic change and only one synchronization is needed: + +[source,java] +---- +import com.vaadin.flow.signals.operations.BulkInsertOperation; + +SharedListSignal items = new SharedListSignal<>(String.class); + +// Insert at the end, preserving the collection order +BulkInsertOperation> operation = + items.insertAllLast(List.of("A", "B", "C")); + +// Insert at the beginning +items.insertAllFirst(List.of("First", "Second")); + +// Insert at a specific position +items.insertAllAt(List.of("X", "Y"), + ListPosition.after(operation.signals().get(0))); +---- + +Each method returns a [classname]`BulkInsertOperation` that provides the signals for the inserted entries through [methodname]`signals()` and tracks the entire batch with a single result future through [methodname]`result()`. The signals can be used immediately, even before the result of the operation is confirmed. + + ==== Reordering Items with moveTo Use [methodname]`moveTo()` to change the position of an existing list entry without removing and re-inserting it: