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
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@

== Related Topics

- <<./crud-editor#,Crud Form Editor>> - Using Binder with a signal of a bean to create a CRUD editor for selected item

Check failure on line 171 in articles/flow/ui-state/usage-examples/binder-integration.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.ProductName] Use 'CRUD' instead of 'Crud'. Raw Output: {"message": "[Vaadin.ProductName] Use 'CRUD' instead of 'Crud'.", "location": {"path": "articles/flow/ui-state/usage-examples/binder-integration.adoc", "range": {"start": {"line": 171, "column": 20}}}, "severity": "ERROR"}
- <<../local-signals#,Local Signals>> - Understanding ValueSignal and two-way binding
- <<../effects-computed#,Effects and Computed Signals>> - Creating derived values
- <<../building-ui#,Component Bindings>> - Binding signals to component properties
Expand Down
152 changes: 152 additions & 0 deletions articles/flow/ui-state/usage-examples/crud-editor.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
---
title: Crud Form Editor

Check failure on line 2 in articles/flow/ui-state/usage-examples/crud-editor.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.ProductName] Use 'CRUD' instead of 'Crud'. Raw Output: {"message": "[Vaadin.ProductName] Use 'CRUD' instead of 'Crud'.", "location": {"path": "articles/flow/ui-state/usage-examples/crud-editor.adoc", "range": {"start": {"line": 2, "column": 8}}}, "severity": "ERROR"}
description: Building an form editor CRUD interface for creating and updating data in a grid using signals.
order: 25
---

= Crud Form Editor using Signals

Check failure on line 7 in articles/flow/ui-state/usage-examples/crud-editor.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.ProductName] Use 'CRUD' instead of 'Crud'. Raw Output: {"message": "[Vaadin.ProductName] Use 'CRUD' instead of 'Crud'.", "location": {"path": "articles/flow/ui-state/usage-examples/crud-editor.adoc", "range": {"start": {"line": 7, "column": 3}}}, "severity": "ERROR"}

This guide demonstrates how to create a reactive CRUD (Create, Read, Update, Delete) form editor that synchronizes with a grid using signals. The approach eliminates complex event handling chains while maintaining robust functionality.

== The Use Case

We want to create an interface where users can:

Check warning on line 13 in articles/flow/ui-state/usage-examples/crud-editor.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.We] Try to avoid using first-person plural like 'We'. Raw Output: {"message": "[Vaadin.We] Try to avoid using first-person plural like 'We'.", "location": {"path": "articles/flow/ui-state/usage-examples/crud-editor.adoc", "range": {"start": {"line": 13, "column": 1}}}, "severity": "WARNING"}

- View items in a grid
- Select an item for editing
- See immediate form updates based on selection
- Create new or update existing items
- Save changes with proper validation feedback

The key challenge is managing the selected item state reactively, ensuring UI components stay perfectly synchronized without manual coordination.

== Architecture Overview

Our solution leverages three core concepts with clean separation of concerns:

Check warning on line 25 in articles/flow/ui-state/usage-examples/crud-editor.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.We] Try to avoid using first-person plural like 'Our'. Raw Output: {"message": "[Vaadin.We] Try to avoid using first-person plural like 'Our'.", "location": {"path": "articles/flow/ui-state/usage-examples/crud-editor.adoc", "range": {"start": {"line": 25, "column": 1}}}, "severity": "WARNING"}

- *Signals* for reactive state management (especially for the selected item)
- *Binder* for form-to-data binding and validation
- *Effects* to synchronize data between grid selection and form

== Implementation Steps

=== 1. Create the Item Grid and Form

First, we need a data model class that represents our items:

Check warning on line 35 in articles/flow/ui-state/usage-examples/crud-editor.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.We] Try to avoid using first-person plural like 'our'. Raw Output: {"message": "[Vaadin.We] Try to avoid using first-person plural like 'our'.", "location": {"path": "articles/flow/ui-state/usage-examples/crud-editor.adoc", "range": {"start": {"line": 35, "column": 51}}}, "severity": "WARNING"}

Check warning on line 35 in articles/flow/ui-state/usage-examples/crud-editor.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.We] Try to avoid using first-person plural like 'we'. Raw Output: {"message": "[Vaadin.We] Try to avoid using first-person plural like 'we'.", "location": {"path": "articles/flow/ui-state/usage-examples/crud-editor.adoc", "range": {"start": {"line": 35, "column": 8}}}, "severity": "WARNING"}

[source,java]
----
include::{root}/src/main/java/com/vaadin/demo/flow/signals/usecase/CrudEditorExample.java[tags=data-model,indent=0]
// Constructor, getters, and setters are omitted for brevity
----

Set up a grid to display items and manage selection:

[source,java]
----
include::{root}/src/main/java/com/vaadin/demo/flow/signals/usecase/CrudEditorExample.java[tags=grid-setup,indent=0]
----

Create and bind the form fields using the binder:

[source,java]
----
include::{root}/src/main/java/com/vaadin/demo/flow/signals/usecase/CrudEditorExample.java[tags=form,indent=0]
----

=== 2. Synchronize Grid Selection and the Form using a Signal

Use [classname]`ValueSignal` to track the currently selected item:

[source,java]
----
include::{root}/src/main/java/com/vaadin/demo/flow/signals/usecase/CrudEditorExample.java[tags=item-signal,indent=0]
----

This signal will hold either a real selected item (for editing) or a special `NEW_ITEM` placeholder for creating new items.

Check warning on line 66 in articles/flow/ui-state/usage-examples/crud-editor.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.Will] Avoid using 'will'. Raw Output: {"message": "[Vaadin.Will] Avoid using 'will'.", "location": {"path": "articles/flow/ui-state/usage-examples/crud-editor.adoc", "range": {"start": {"line": 66, "column": 13}}}, "severity": "WARNING"}
The new item is the initial value, as nothing is selected by default.

Now create bidirectional synchronization between grid selection and the signal:

[source,java]
----
include::{root}/src/main/java/com/vaadin/demo/flow/signals/usecase/CrudEditorExample.java[tags=selection,indent=0]
----

Next, add an effect to update the binder with the selected item data:

[source,java]
----
include::{root}/src/main/java/com/vaadin/demo/flow/signals/usecase/CrudEditorExample.java[tags=binder-signal,indent=0]
----

[NOTE]
====
We use [methodname]`Binder::readBean(bean)` to update the form data using

Check warning on line 85 in articles/flow/ui-state/usage-examples/crud-editor.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.We] Try to avoid using first-person plural like 'We'. Raw Output: {"message": "[Vaadin.We] Try to avoid using first-person plural like 'We'.", "location": {"path": "articles/flow/ui-state/usage-examples/crud-editor.adoc", "range": {"start": {"line": 85, "column": 1}}}, "severity": "WARNING"}
a signal. This is the preferred approach.

Avoid using [methodname]`Binder::setBean(beanInstance)` with signals, as this
enables the binder to change the bean properties directly using setter methods.
Such changes are not detected by signals.

To update the signal with the changes from the binder,
use [methodname]`Binder::writeBean(bean)` combined with the `signal.set(bean)`,
see the example below.
====

Now create and bind individual form fields:

=== 3. Create a Dynamic Save Button

Add a derived boolean signal to distinguish between creating a new and exiting
an existing item selected in the grid.

[source,java]
----
include::{root}/src/main/java/com/vaadin/demo/flow/signals/usecase/CrudEditorExample.java[tags=derived-signal,indent=0]
----

Add the save button with reactive label and behavior depending on whether a new
or existing item is being edited:

[source,java]
----
include::{root}/src/main/java/com/vaadin/demo/flow/signals/usecase/CrudEditorExample.java[tags=save-snippet,indent=0]
----

Combine all components and add them to your view:

[source,java]
----
include::{root}/src/main/java/com/vaadin/demo/flow/signals/usecase/CrudEditorExample.java[tags=layout,indent=0]
----

== Key Takeaways

**Using signals for UI state**::
The `selectedItemSignal` becomes the single source of truth for "which item
is being edited". All components that need to display/edit this item can
react to changes in this signal. The grid and form stay perfectly
synchronized without manual coordination.
**Adding derived signals for computed logic**::
The `creatingItemSignal` tells us whether we're editing a new (unsaved)

Check warning on line 132 in articles/flow/ui-state/usage-examples/crud-editor.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.We] Try to avoid using first-person plural like 'we'. Raw Output: {"message": "[Vaadin.We] Try to avoid using first-person plural like 'we'.", "location": {"path": "articles/flow/ui-state/usage-examples/crud-editor.adoc", "range": {"start": {"line": 132, "column": 47}}}, "severity": "WARNING"}

Check warning on line 132 in articles/flow/ui-state/usage-examples/crud-editor.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.We] Try to avoid using first-person plural like 'us'. Raw Output: {"message": "[Vaadin.We] Try to avoid using first-person plural like 'us'.", "location": {"path": "articles/flow/ui-state/usage-examples/crud-editor.adoc", "range": {"start": {"line": 132, "column": 36}}}, "severity": "WARNING"}
item or an existing one. This drives dynamic behavior like button labels
and data persistence logic.
**Using effects for UI reactivity**::
[methodname]`Signal.effect()` performs reactive UI updates based on signal
dependencies.
[methodname]`Binder::readBean(bean)` and [methodname]`Biner::writeBean(bean)`::
Used to synchronize the form state between the binder and the bean state
signal.
**When to use `Signal::peek()`**::
When you need the current value but don't want to create a dependency on
the signal. For example, in event handlers where you just need the current

Check warning on line 143 in articles/flow/ui-state/usage-examples/crud-editor.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.JustSimply] Avoid using 'just'. It may be insensitive. Raw Output: {"message": "[Vaadin.JustSimply] Avoid using 'just'. It may be insensitive.", "location": {"path": "articles/flow/ui-state/usage-examples/crud-editor.adoc", "range": {"start": {"line": 143, "column": 58}}}, "severity": "WARNING"}
state without subscribing to changes.

== Related Topics

- <<./binder-integration#,Form Binding with Dynamic Validation>> - Combining Binder with signals for forms with dynamic validation logic
- <<../local-signals#,Local Signals>> - Understanding ValueSignal and two-way binding
- <<../effects-computed#,Effects and Computed Signals>> - Creating derived values
- <<../building-ui#,Component Bindings>> - Binding signals to component properties
- <<../../components/grid#,Grid>> - Reference documentation for Grid component
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package com.vaadin.demo.flow.signals.usecase;

import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.binder.ValidationException;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.signals.Signal;
import com.vaadin.flow.signals.local.ValueSignal;
import org.jspecify.annotations.NonNull;

import java.util.List;

@Route("crud-editor-with-signals")
public class CrudEditorExample extends VerticalLayout {

// tag::item-signal[]
static private final Item NEW_ITEM = new Item();
// end::item-signal[]

public CrudEditorExample() {
// tag::grid-setup[]
Grid<Item> itemGrid = new Grid<>(Item.class);
itemGrid.setColumns("product", "category");
itemGrid.setItems(listItems());
// end::grid-setup[]

// tag::item-signal[]
ValueSignal<Item> selectedItemSignal = new ValueSignal<>(NEW_ITEM);
// end::item-signal[]
// tag::selection[]
// Update signal when user selects an item in the grid
itemGrid.addSelectionListener((event) -> selectedItemSignal
.set(event.getFirstSelectedItem().orElse(NEW_ITEM)));

// Keep grid selection in sync with the signal (if signal changes
// programmatically)
Signal.effect(this, () -> itemGrid.select(selectedItemSignal.get()));
// end::selection[]

add(itemGrid);

// tag::form[]
Binder<Item> itemBinder = new Binder<>();

TextField productNameField = new TextField("Product");
itemBinder.forField(productNameField).bind(Item::getProduct,
Item::setProduct);

ComboBox<String> categoryField = new ComboBox<>("Category");
categoryField.setItems(List.of("Office", "Tech", "Stationery"));
itemBinder.forField(categoryField).bind(Item::getCategory,
Item::setCategory);
// end::form[]

// tag::binder-signal[]
Signal.effect(this,
() -> itemBinder.readBean(selectedItemSignal.get()));
// end::binder-signal[]

// tag::derived-signal[]
// Derived signal, true when no item is selected (creating a new item)
final Signal<Boolean> creatingItemSignal = selectedItemSignal
.map(NEW_ITEM::equals);
// end::derived-signal[]

// tag::save-snippet[]
Button saveButton = new Button(
() -> creatingItemSignal.get() ? "Create" : "Update");

saveButton.addClickListener((event) -> {
try {
// Note: the method uses `peek()` to read signals, as the click
// listener should not subscribe to the state changes.
boolean creatingItem = creatingItemSignal.peek();
// Create a new item instance or reuse existing for editing
Item item = creatingItem ? new Item()
: selectedItemSignal.peek();

// Save changes from the binder
itemBinder.writeBean(item);
item = saveItem(item);

// Update the selected item signal to use the saved item
selectedItemSignal.set(item);

// Refresh the data in the grid
itemGrid.getDataProvider().refreshAll();

// Show success notification
final String successMessage = creatingItem ? "Item added"
: "Item updated";
Notification
.show(successMessage, 3000,
Notification.Position.BOTTOM_END)
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
} catch (ValidationException e) {
// Show validation error
Notification.show("Invalid item", 3000,
Notification.Position.BOTTOM_END);
}
});
// end::save-snippet[]

// tag::layout[]
FormLayout formLayout = new FormLayout();
formLayout.setAutoResponsive(true);
formLayout.addFormRow(productNameField, categoryField, saveButton);

add(formLayout);
// end::layout[]
}

static private List<Item> listItems() {
return List.of(new Item(1, "Laptop", "Tech"),
new Item(2, "Desk Chair", "Office"),
new Item(3, "Monitor", "Tech"), new Item(4, "Keyboard", "Tech"),
new Item(5, "Mouse Pad", "Office"),
new Item(6, "Printer Paper", "Office"),
new Item(7, "Stapler", "Office"),
new Item(8, "Desk", "Stationary"),
new Item(9, "Notebook", "Office"),
new Item(10, "Pen Set", "Office"),
new Item(11, "Cable Ties", "Tech"),
new Item(12, "Extension Cord", "Tech"));
}

static private @NonNull Item saveItem(@NonNull Item item) {
// Make the item change persistent, for example, using a database
final Item savedItem = new Item();
savedItem.setId(item.getId());
savedItem.setProduct(item.getProduct());
savedItem.setCategory(item.getCategory());
return savedItem;
}

/**
* Item bean class for storing supply information
*/
// tag::data-model[]
static public class Item {
private long id;
@NonNull
private String product = "";
@NonNull
private String category = "";
// end::data-model[]

// Constructor
public Item() {
}

// Parameterized constructor with all fields
public Item(long id, @NonNull String product,
@NonNull String category) {
this.id = id;
this.product = product;
this.category = category;
}

// Getters and Setters
public long getId() {
return id;
}

public void setId(long id) {
this.id = id;
}

public @NonNull String getProduct() {
return product;
}

public void setProduct(@NonNull String product) {
this.product = product;
}

public @NonNull String getCategory() {
return category;
}

public void setCategory(@NonNull String category) {
this.category = category;
}

@Override
public String toString() {
return String.format("Item[id=%d, product='%s', category='%s']", id,
product, category);
}
// tag::data-model[]
}
// end::data-model[]
}
Loading