Skip to content

Commit f29cca8

Browse files
tltvmshabarov
andauthored
docs: document Signal binding in Element (#4881)
Fixes: #4790 Co-authored-by: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com>
1 parent 0b66d54 commit f29cca8

File tree

1 file changed

+255
-0
lines changed

1 file changed

+255
-0
lines changed

articles/flow/advanced/signals.adoc

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,3 +405,258 @@ Signal.effect(() -> {
405405
String name = nameSignal.peek(); // The effect will not depend on nameSignal
406406
});
407407
----
408+
409+
== Signal Binding in Element
410+
411+
Various [classname]`com.vaadin.flow.dom.Element` features support signal binding, such as text, attributes, properties, [classname]`ClassList`, and [classname]`Style`. See <</flow/component-internals/element-api#,Element API>> for more details of those features.
412+
413+
Each feature works with same rules. When feature is bound to a signal, feature's value, like text content, is kept synchronized with the signal value while the element is in the attached state. When the element is in detached state, signal value changes have no effect. `null` signal unbinds the existing binding. While a signal is bound, any attempt to set the value manually in other way than through the signal, throws [classname]`BindingActiveException`. The same happens when trying to bind a new signal while one is already bound.
414+
415+
The following code serves as the base for the examples in the subsequent sections. It adds a button that increments a number signal by one and an empty span element:
416+
[source,java]
417+
----
418+
public class SimpleCounter extends VerticalLayout {
419+
420+
private final NumberSignal counter =
421+
SignalFactory.IN_MEMORY_SHARED.number("counter");
422+
423+
public SimpleCounter() {
424+
Button button = new Button("Increment by one");
425+
button.addClickListener(
426+
click -> counter.incrementBy(1));
427+
add(button);
428+
429+
Span span = new Span("");
430+
add(span);
431+
}
432+
}
433+
----
434+
435+
=== Text Binding
436+
437+
.`Element#bindText(Signal<String> signal)`
438+
[source,java]
439+
----
440+
// NumberSignal's Double type has to be mapped to String.
441+
Signal<String> signal = counter.map(value -> String.format("Clicked %.0f times", value));
442+
443+
span.getElement().bindText(signal);
444+
// span's text content is now "Clicked 0 times"
445+
----
446+
.Basic functionality (same rules with all Element bindings)
447+
[source,java]
448+
----
449+
// The following code demonstrates behavior step by step:
450+
span.getElement().getText(); // returns "Clicked 0 times"
451+
span.getElement().setText(""); // throws BindingActiveException
452+
453+
span.getElement().removeFromParent(); // detaching from the UI
454+
span.getElement().getText(); // returns "Clicked 0 times"
455+
span.getElement().setText(""); // throws BindingActiveException
456+
counter.value(5); // updating the signal value
457+
span.getElement().getText(); // returns "Clicked 0 times"
458+
add(span); // re-attaching the element to the UI
459+
span.getElement().getText(); // returns "Clicked 5 times"
460+
461+
span.getElement().bindText(null); // unbinds the existing binding
462+
span.getElement().getText(); // returns "Clicked 5 times"
463+
span.getElement().setText("");
464+
span.getElement().getText(); // returns ""
465+
counter.value(0);
466+
span.getElement().getText(); // returns ""
467+
----
468+
469+
=== Attribute Binding
470+
471+
.`Element#bindAttribute(String attribute, Signal<String> signal)`
472+
[source,java]
473+
----
474+
ValueSignal<Boolean> hidden = new ValueSignal<>(false);
475+
476+
span.getElement().bindAttribute("hidden", hidden.map(value -> value ? "" : null));
477+
// DOM has "<span hidden>".
478+
479+
hidden.value(!hidden.peek());
480+
// DOM has "<span>" now.
481+
// Boolean is mapped to "" when true and null when false.
482+
// Some other value like 'foo' would be "<span hidden='foo'>".
483+
484+
----
485+
486+
=== Property Binding
487+
488+
Supports various value types: `String`, `Boolean`, `Double`, `BaseJsonNode`, `Object` (bean), `List` and `Map`.
489+
490+
Typed Lists and Maps are not supported, i.e. the signal must be of type `Signal<List<?>>` or `Signal<Map<?,?>`.
491+
492+
.`Element#bindProperty(String name, Signal<?> signal)`
493+
[source,java]
494+
----
495+
ValueSignal<Boolean> hidden = new ValueSignal<>(false);
496+
497+
span.getElement().bindProperty("hidden", hidden);
498+
hidden.value(!hidden.peek()); // toggles 'hidden' property
499+
----
500+
.String type
501+
[source,java]
502+
----
503+
ValueSignal<String> title = new ValueSignal<>("Hello");
504+
span.getElement().bindProperty("title", title);
505+
title.value("World"); // updates 'title' property
506+
----
507+
.Double type
508+
[source,java]
509+
----
510+
NumberSignal width = new NumberSignal();
511+
width.value(100.5);
512+
span.getElement().bindProperty("width", width);
513+
width.incrementBy(50); // updates 'width' property to 150.5
514+
----
515+
516+
.Object (bean) type
517+
[source,java]
518+
----
519+
record Person(String name, int age) {
520+
}
521+
ValueSignal<Person> person = new ValueSignal<>(new Person("John", 30));
522+
span.getElement().bindProperty("person", person);
523+
person.value(new Person("Jane", 25));
524+
// element.person is now {name: 'Jane', age: 25}
525+
----
526+
527+
.List type
528+
[source,java]
529+
----
530+
ValueSignal<List<String>> items = new ValueSignal<>(List.of("Item 1", "Item 2"));
531+
span.getElement().bindProperty("items", items);
532+
items.value(List.of("Item A", "Item B", "Item C"));
533+
// element.items is now ['Item A', 'Item B', 'Item C']
534+
----
535+
.Map type
536+
[source,java]
537+
----
538+
ValueSignal<Map<String, String>> config = new ValueSignal<>(Map.of("key1", "value1"));
539+
span.getElement().bindProperty("config", config);
540+
config.value(Map.of("key1", "value1", "key2", "value2"));
541+
// element.config is now {key1: 'value1', key2: 'value2'}
542+
----
543+
.BaseJsonNode type
544+
[source,java]
545+
----
546+
ObjectMapper mapper = new ObjectMapper();
547+
ObjectNode jsonNode = mapper.createObjectNode();
548+
jsonNode.put("key", "value");
549+
ValueSignal<ObjectNode> jsonSignal = new ValueSignal<>(jsonNode);
550+
span.getElement().bindProperty("jsonData", jsonSignal);
551+
552+
ObjectNode updatedJson = mapper.createObjectNode();
553+
updatedJson.put("key", "updatedValue");
554+
updatedJson.put("newKey", "newValue");
555+
jsonSignal.value(updatedJson);
556+
// element.jsonData is now {key: 'updatedValue', newKey: 'newValue'}
557+
----
558+
559+
560+
561+
.Property change listener for 'change' DOM event
562+
[source,java]
563+
----
564+
// Adds a property change listener and configures 'hidden' property
565+
// to be synchronized to the server when 'change' DOM event is fired.
566+
span.getElement().addPropertyChangeListener("hidden", "change", event -> {
567+
Notification.show("'hidden' property changed to: " + event.getValue());
568+
});
569+
// property change event from the client will update the signal value
570+
571+
// Example javascript that dispatches change event from the browser where
572+
// element is <span>:
573+
// element.hidden = !element.hidden;
574+
// element.dispatchEvent(new Event('change'));
575+
----
576+
577+
=== ClassList Binding
578+
579+
.`ClassList#bind(String name, Signal<Boolean> signal)`
580+
[source,java]
581+
----
582+
ValueSignal<Boolean> foo = new ValueSignal<>(false);
583+
ValueSignal<Boolean> bar = new ValueSignal<>(true);
584+
585+
span.getElement().getClassList().bind("foo", foo);
586+
span.getElement().getClassList().bind("bar", bar);
587+
// DOM has "<span class='bar'>"
588+
foo.value(true);
589+
// DOM has "<span class='bar foo'>"
590+
591+
span.getElement().getClassList().clear();
592+
// DOM has "<span class>". Binding is also removed.
593+
----
594+
595+
=== Style Binding
596+
597+
.`Style#bind(String name, Signal<Boolean> signal)`
598+
[source,java]
599+
----
600+
ValueSignal<String> color = new ValueSignal<>("black");
601+
ValueSignal<String> background = new ValueSignal<>("white");
602+
603+
span.getElement().getStyle().bind("color", color);
604+
span.getElement().getStyle().bind("background", background);
605+
// DOM has "<span style='color: black; background: white'>"
606+
607+
color.value("red");
608+
background.value("gray");
609+
// DOM has "<span style='color: red; background: gray'>"
610+
611+
background.value(""); // same with null
612+
// DOM has "<span style='color: red;'>"
613+
614+
span.getElement().getStyle().clear();
615+
// DOM has "<span style>". Binding is also removed.
616+
----
617+
618+
=== SignalPropertySupport helper
619+
620+
Not all component features delegate directly to the state in [classname]`com.vaadin.flow.dom.Element`. For those features, the [classname]`SignalPropertySupport` helper class ensures that state management behaves consistently with other Element bindings.
621+
622+
For example, a component can have a Java API to bind `Signal<String>` to modify the https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent[`textContent`] property of the element in the browser. `SignalPropertySupport#create(Component, SerializableConsumer<T>)` creates a new instance of `SignalPropertySupport` with `bind(Signal<T>)`, `get()`, and `set(T)` methods. The given consumer sets the `textContent` based on the value, as shown in the following example.
623+
624+
.MyComponent creation
625+
[source,java]
626+
----
627+
// The following code demonstrates behavior step by step:
628+
MyComponent component = new MyComponent();
629+
component.bindTextContent(counter.map(v -> "Signal value: " + v));
630+
add(component);
631+
// textContent in browser is "Content: Signal value: 0.0"
632+
component.getTextContent(); // returns "Signal value: 0.0"
633+
component.setTextContent(""); // throws BindingActiveException
634+
635+
component.bindTextContent(null); // unbinds the existing binding
636+
component.setTextContent("");
637+
component.getTextContent(); // returns ""
638+
// textContent in browser is "Content: "
639+
----
640+
641+
.SignalPropertySupport usage
642+
[source,java]
643+
----
644+
class MyComponent extends Div {
645+
private final SignalPropertySupport<String> textProperty =
646+
SignalPropertySupport.create(this, value -> {
647+
getElement().executeJs("this.textContent = 'Content: ' + $0", value);
648+
});
649+
650+
public String getTextContent() {
651+
return textProperty.get();
652+
}
653+
654+
public void setTextContent(String text) {
655+
textProperty.set(text);
656+
}
657+
658+
public void bindTextContent(Signal<String> textSignal) {
659+
textProperty.bind(textSignal);
660+
}
661+
}
662+
----

0 commit comments

Comments
 (0)