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:
- There are three different
Bindermethod to consider:setBean,readBean,readRecord. Preserving the distinction between those cases would lead to a relatively complex binding API. - There are existing methods with a “bind” prefix in the
BinderAPI. Additional “bind” methods related to signals would lead to confusion relative to the existing bind methods that have a different purpose. - 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:
- Do you see ways in which the suggested APIs could be imporovd to better support the described use cases?
- Are there use cases that we have not taken into account that you think would be important to consider in combination with signals?