Skip to content

Commit 2e1331a

Browse files
authored
feat: add SignalPropertySupport helper (#22680)
* feat: add SignalPropertySupport helper Provides a SignalPropertySupport helper class that encapsulates the state management needed for making getXyz, setXyz, and bindXyz behave in the same way as Signal for Element properties. Fixes: #22672 * Cleanup test class * Clarified Javadoc
1 parent d5205d3 commit 2e1331a

File tree

2 files changed

+473
-0
lines changed

2 files changed

+473
-0
lines changed
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/*
2+
* Copyright 2000-2025 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.component;
17+
18+
import java.io.Serializable;
19+
import java.util.Objects;
20+
21+
import com.vaadin.flow.function.SerializableConsumer;
22+
import com.vaadin.flow.shared.Registration;
23+
import com.vaadin.signals.BindingActiveException;
24+
import com.vaadin.signals.Signal;
25+
26+
/**
27+
* Helper class for binding a {@link Signal} to a property of a
28+
* {@link Component}. Not all component features delegate directly to the state
29+
* in {@link com.vaadin.flow.dom.Element}. For those features, this helper class
30+
* ensures state management behaves consistently with Element properties.
31+
* <p>
32+
* Example of usage:
33+
*
34+
* <pre>
35+
* ValueSignal&lt;String&gt; signal = new ValueSignal&lt;&gt;("");
36+
* MyComponent component = new MyComponent();
37+
* add(component);
38+
* component.bindTextContent(signal);
39+
* signal.set("Hello"); // component content showing now "Content: Hello" text
40+
* </pre>
41+
*
42+
* <pre>
43+
* &#064;Tag(div)
44+
* public class MyComponent extends Component {
45+
* private final SignalPropertySupport&lt;String&gt; textProperty = SignalPropertySupport
46+
* .create(this, value -> {
47+
* getElement().executeJs("this.textContent = 'Content: ' + $0",
48+
* value);
49+
* });
50+
*
51+
* public String getText() {
52+
* return textProperty.get();
53+
* }
54+
*
55+
* public void setText(String text) {
56+
* textProperty.set(text);
57+
* }
58+
*
59+
* public void bindTextContent(Signal&lt;String&gt; textSignal) {
60+
* textProperty.bind(textSignal);
61+
* }
62+
* }
63+
* </pre>
64+
*
65+
* @param <T>
66+
* the type of the property
67+
*/
68+
public class SignalPropertySupport<T> implements Serializable {
69+
70+
private final SerializableConsumer<T> valueChangeConsumer;
71+
72+
private final Component owner;
73+
74+
private Registration registration;
75+
76+
private Signal<T> signal;
77+
78+
private T value;
79+
80+
private SignalPropertySupport(Component owner,
81+
SerializableConsumer<T> valueChangeConsumer) {
82+
this.owner = Objects.requireNonNull(owner,
83+
"Owner component cannot be null");
84+
this.valueChangeConsumer = Objects.requireNonNull(valueChangeConsumer,
85+
"Value change consumer cannot be null");
86+
}
87+
88+
/**
89+
* Creates a new instance of SignalPropertySupport for the given owner
90+
* component and a value change consumer to be called when property value is
91+
* updated. The property value is updated either manually with
92+
* {@link #set(Object)}, or automatically via {@link Signal} value change
93+
* while the owner component is in the attached state and signal is bound
94+
* with {@link #bind(Signal)}.
95+
*
96+
* @param owner
97+
* the owner component for which the value change consumer is
98+
* applied, must not be null
99+
* @param valueChangeConsumer
100+
* the consumer to be called when the value changes, must not be
101+
* null
102+
* @param <T>
103+
* the type of the property
104+
* @return a new instance of SignalPropertySupport
105+
* @see #bind(Signal)
106+
*/
107+
public static <T> SignalPropertySupport<T> create(Component owner,
108+
SerializableConsumer<T> valueChangeConsumer) {
109+
return new SignalPropertySupport<>(owner, valueChangeConsumer);
110+
}
111+
112+
/**
113+
* Binds a {@link Signal}'s value to this property support and keeps the
114+
* value synchronized with the signal value while the component is in
115+
* attached state. When the component is in detached state, signal value
116+
* changes have no effect. <code>null</code> signal unbinds existing
117+
* binding.
118+
* <p>
119+
* While a Signal is bound to a property support, any attempt to set value
120+
* manually throws {@link com.vaadin.signals.BindingActiveException}. Same
121+
* happens when trying to bind a new Signal while one is already bound.
122+
*
123+
* @param signal
124+
* the signal to bind or <code>null</code> to unbind any existing
125+
* binding
126+
* @throws com.vaadin.signals.BindingActiveException
127+
* thrown when there is already an existing binding
128+
*/
129+
public void bind(Signal<T> signal) {
130+
if (signal != null && this.signal != null) {
131+
throw new BindingActiveException();
132+
}
133+
this.signal = signal;
134+
if (signal == null && registration != null) {
135+
registration.remove();
136+
registration = null;
137+
}
138+
if (signal != null) {
139+
registration = ComponentEffect.effect(owner, () -> {
140+
value = signal.value();
141+
valueChangeConsumer.accept(value);
142+
});
143+
}
144+
}
145+
146+
/**
147+
* Gets the current value of this property support.
148+
*
149+
* @return the current value
150+
*/
151+
public T get() {
152+
return value;
153+
}
154+
155+
/**
156+
* Sets the value of this property support.
157+
*
158+
* @param value
159+
* the value to set
160+
* @throws com.vaadin.signals.BindingActiveException
161+
* thrown when there is an existing binding
162+
*/
163+
public void set(T value) {
164+
if (signal != null) {
165+
throw new BindingActiveException();
166+
}
167+
this.value = value;
168+
valueChangeConsumer.accept(value);
169+
}
170+
}

0 commit comments

Comments
 (0)