RFC: Form binding with signals

We’re making progress towards introducing bind methods to component APIs as described in this RFC. After that is done, we think that the only significant change needed before we can declare signals to be production ready would be to make some adjustments to Binder and HasValue to make them fit better into a signal-enabled world.

The intention here is explicitly not to design a new form binding solution that would be natively based on signals. While that’s something we might want to do in the future, the main goal right now is to make some carefully selected additions to existing APIs.

Cross-field validation

Field-level validator callbacks should be run inside a signal effect so that changes to any signal used by the validator will be detected and trigger running the validator again. Practical cross-field validation furthermore requires that the value of other bindings are available as signals, by adding a signal-aware value getter to Binding.

var passwordBinding = binder.forField(passwordField).bind("password");

binder.forField(confirmPasswordField)
  .withValidator(value -> Objects.equals(value, passwordBinding.value()), 
    "Must match the password")
  .bind("confirmPassword");

Reacting to validation status changes

Binder should provide a readonly signal that contains the latest BinderValidationStatus state. The signal value should be updated whenever a BinderValidationStatusHandler would have been notified. This should work the same regardless of whether a status handler is explicitly configured or if the default that delegates to handleBinderValidationStatus is in use.

var validationStatusSignal = binder.getValidationStatus();
submitButton.bindEnabled(() -> validationStatusSignal.value().isOk());

Setting the bean/record to edit based on a signal

When the selected item is managed as a signal e.g. based on selection events from a Grid or a URL parameter, you want Binder to automatically populate the form based on the selected item whenever the signal value changes.

We would not add any API for this case but instead recommend using ComponentEffect to set up the item to be edited whenever the signal value changes.

ComponentEffect.effect(this, () -> binder.readBean(beanToEditSignal.value());
saveButton.addClickListener(event -> {
  Person person = new Person();
  binder.writeBean(person);
  service.save(person);
  beanToEditSignal.value(person); // In case the save call might have changed some data
});

There are multiple reasons for not introducing any new API for this purpose:

  1. There are three different Binder method to consider: setBean, readBean, readRecord. Preserving the distinction between those cases would lead to a relatively complex binding API.
  2. There are existing methods with a “bind” prefix in the Binder API. Additional “bind” methods related to signals would lead to confusion relative to the existing bind methods that have a different purpose.
  3. Applying changes when a signal changes would require setting up a signal effect. In the case of Binder, there’s no direct component instance to use for the effect which means that there would be no reliable way of avoiding memory leaks in case the signal instance is used in a wider scope than the binder instance.

Updating other parts of the UI based on the bound bean

We don’t add any API to Binder to simplify non-field cases that might now be handled using ReadOnlyHasValue. Instead, we assume there’s already a signal containing the currently edited bean and recommend binding directly to that signal rather than creating a binder binding for that purpose.

add(new Span(() -> "Editing: " + beanToEditSignal.value().getName())); // (missing null check)

Two-way field bindings

Binder is overkill for simple cases such as a standalone search field. Instead, the natural approach would be to bind a signal directly to the value of an input field. This kind of binding would be most natural as a two-way binding so that the developer doesn’t have to add a value change listener to separately update the signal value.

var text = new ValueSignal<>("");
add(new TextField() {{
  bindValue(text);
}});
add(new Span() {{
  bindText(() -> "You typed: " text.value());
}});
add(new Button("Clear", click -> text.value(""));

Note that while typical one-way bindings (e.g. for the text in a Span) accept the readonly Signal base type that might just as well be e.g. a computed signal, this case requires a writable signal. Rather than binding to the ValueSignal implementation, we should add a WritableSignal interface that will also be useful for some other use cases outside the context of this RFC.

Request for comments

We believe that the relatively small additions described above will help combine the existing reactive functionaltiy of Binder with new reactive functionality provided by signals. In particular, we ask for any insights on these two questions:

  1. Do you see ways in which the suggested APIs could be imporovd to better support the described use cases?
  2. Are there use cases that we have not taken into account that you think would be important to consider in combination with signals?
2 Likes

I will go through the questions later. but could you please stop using this syntax:

add(new TextField() {{
  bindValue(text);
}});

This is very uncommon in Java, and even I, as a Java veteran, have to think twice about what happens here. And what’s happening is not good.

  1. It creates a hidden subclass
  2. Allocates a synthetic reference to the outer class (possible memory leak)

And finally, it’s tough to read, not to mention how to debug this.

1 Like

What do you think about

var text = new TextField();
text.setValueSignal("Lorem"); 
text.setValueSignal(new Signal("..."); 
text.getValueSignal(); 

for proper get/set behavior for developers? This would also match the old way with get/set value, allowing people to find this methods pretty easily


My other suggestion would be to create a new Binder. The current Binder is already a quite complex construct… this would allow to focus primarily on Signal handling within the new Binder while simultaneous reducing the breaking changes for people extending the current Binder/Binding.

These are not regular setters and getters. They are 90% side effects and 10% setting the value while there’s not to my knowledge any common use case for ever getting the value. Furthermore, the method would also behave in odd ways as a setter since text.setValueSignal(signal); after a previous text.setValueSignal(signal); throws an exception about a binding already being active.

There were some discussion about different naming strategies in Thoughts about conventions and naming for binding signals to components? and that discussion was leaning towards the “bindXyz” convention. Do you think there was anything we omitted at that time?

I fully agree over a slightly longer time perspective. For now, the focus is on getting the bare necessities covered for releasing with Vaadin 25.1 in March next year. Providing a fully signals-based form binding solution will not be backwards compatible which means that it would be a completely new API while keeping Binder intact.

1 Like

Let’s pull out that discussion to avoid distracting the discussion about form binding. To {{ or not to {{, that's the question

I still try to wrap my head around signals and how my applications would benefit from it.

But IMO signals add complexity and a new mental model. For straightforward business applications with simple state, traditional listeners are easier to understand and maintain.

If all you have is e.g. a Grid to select an item and a form to directly edit that item, then your state management is little more than handling the selection state and validation state. The basic validation state is already handled by Binder even though cross-field validation requires a little bit of non-obvious boilerplate code. There are some interesting cases with the selection state e.g. in relationship with also keeping the selection in the URL for deep linking but that’s mostly outside the scope of this discussion related to form binding.

From that point of view, signals are more about the kind of use case described in Improve support for reactive programming · Issue #3801 · vaadin/flow · GitHub. That’s still in the context of a form but there’s more to the form than just entering all the data into a DTO.

I just noticed that Signals have not been made Serializable. Is this still scheduled for Vaadin 25 or do Signals have their own roadmap? It would prevent us from using them.

To that “effect” (pun intended), will there ever be something like a DistributedSignal that spans several Vaadin instances?

We expect to complete the initial feature set for Vaadin 25.1 in March next year. Proper support for Serializable might not be ready for that deadline but that’s not yet certain. This is complicated by the fact that there are two different scopes involved here. As long as a signal is used only for managing state within a single session, then that signal along with references to all components that depend on it can be serialized relatively easily. This implementation will hopefully be ready for Vaadin 25.1 but I’m not making any promises.

What gets more interesting is when a signal instance is used for sharing state between users since there will then be references between multiple UIs and session and those sessions might even end up deserialized in separate JVMs. This basically means that the values of those signals will have to be synchronized across the whole cluster and the serialized format of the signal instance is just a key used in that synchronization logic. This implementation is further out on the roadmap.

We have prototyped using ValueSignal and related types for both use cases. The only difference is how you get that reference. For a signal that isn’t shared between sessions, you can just do ValueSignal<String> mySignal = new ValueSignal<>(String.class); whereas a signal that is shared across the cluster would be provided from a separate “signal factory”, e.g. ValueSignal<String> mySignal = clusterAwareSignalFactory.value("globalSignalKey", String.class);. So no separate DistributedSignal type would be needed but there’s still a non-trivial amount of plumbing missing behinds the currently available types.

I created an issue for the basic serialization support: Make signals Serializable · Issue #22843 · vaadin/flow · GitHub

1 Like