Skip to content

Commit 4c87bac

Browse files
authored
feat: add signal binding for attribute in Element (#22525)
Introduces bindAttribute(String attribute, Signal<String> signal) method in Element to bind a Signal to attribute. fixes: #22407
1 parent 60e580c commit 4c87bac

File tree

9 files changed

+531
-8
lines changed

9 files changed

+531
-8
lines changed

flow-server/src/main/java/com/vaadin/flow/dom/Element.java

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,56 @@ public String getTag() {
244244
return getStateProvider().getTag(getNode());
245245
}
246246

247+
/**
248+
* Binds a {@link Signal}'s value to a given attribute and keeps the
249+
* attribute value synchronized with the signal value while the element is
250+
* in attached state. When the element is in detached state, signal value
251+
* changes have no effect. <code>null</code> signal unbinds existing
252+
* binding.
253+
* <p>
254+
* Same rules applies for the attribute name and value from the bound Signal
255+
* as in {@link #setAttribute(String, String)}.
256+
* <p>
257+
* While a Signal is bound to an attribute, any attempt to set or remove
258+
* attribute value manually throws
259+
* {@link com.vaadin.signals.BindingActiveException}. Same happens when
260+
* trying to bind a new Signal while one is already bound.
261+
* <p>
262+
* Binding style or class attribute to a Signal is not supported.
263+
* <p>
264+
* Example of usage:
265+
*
266+
* <pre>
267+
* ValueSignal&lt;String&gt; signal = new ValueSignal&lt;&gt;("");
268+
* Element element = new Element("span");
269+
* getElement().appendChild(element);
270+
* element.bindAttribute("mol", signal);
271+
* signal.value("42"); // The element now has attribute mol="42"
272+
* </pre>
273+
*
274+
* @param attribute
275+
* the name of the attribute
276+
* @param signal
277+
* the signal to bind or <code>null</code> to unbind any existing
278+
* binding
279+
* @throws com.vaadin.signals.BindingActiveException
280+
* thrown when there is already an existing binding
281+
* @see #setAttribute(String, String)
282+
*/
283+
public void bindAttribute(String attribute, Signal<String> signal) {
284+
String validAttribute = validateAttribute(attribute);
285+
286+
Optional<CustomAttribute> customAttribute = CustomAttribute
287+
.get(validAttribute);
288+
if (customAttribute.isPresent()) {
289+
throw new UnsupportedOperationException(
290+
"Binding style or class attribute to a Signal is not supported.");
291+
} else {
292+
getStateProvider().bindAttributeSignal(this, validAttribute,
293+
signal);
294+
}
295+
}
296+
247297
/**
248298
* Sets the given attribute to the given value.
249299
* <p>
@@ -272,7 +322,10 @@ public String getTag() {
272322
* @return this element
273323
*/
274324
public Element setAttribute(String attribute, String value) {
275-
String lowerCaseAttribute = validateAttribute(attribute, value);
325+
if (value == null) {
326+
throw new IllegalArgumentException("Value cannot be null");
327+
}
328+
String lowerCaseAttribute = validateAttribute(attribute);
276329

277330
Optional<CustomAttribute> customAttribute = CustomAttribute
278331
.get(lowerCaseAttribute);
@@ -334,7 +387,10 @@ public Element setAttribute(String attribute, boolean value) {
334387
*/
335388
public Element setAttribute(String attribute,
336389
AbstractStreamResource resource) {
337-
String lowerCaseAttribute = validateAttribute(attribute, resource);
390+
String lowerCaseAttribute = validateAttribute(attribute);
391+
if (resource == null) {
392+
throw new IllegalArgumentException("Value cannot be null");
393+
}
338394

339395
Optional<CustomAttribute> customAttribute = CustomAttribute
340396
.get(lowerCaseAttribute);
@@ -1367,7 +1423,7 @@ public Optional<Component> getComponent() {
13671423
return getStateProvider().getComponent(getNode());
13681424
}
13691425

1370-
private String validateAttribute(String attribute, Object value) {
1426+
private String validateAttribute(String attribute) {
13711427
if (attribute == null) {
13721428
throw new IllegalArgumentException(ATTRIBUTE_NAME_CANNOT_BE_NULL);
13731429
}
@@ -1378,10 +1434,6 @@ private String validateAttribute(String attribute, Object value) {
13781434
"Attribute \"%s\" is not a valid attribute name",
13791435
lowerCaseAttribute));
13801436
}
1381-
1382-
if (value == null) {
1383-
throw new IllegalArgumentException("Value cannot be null");
1384-
}
13851437
return lowerCaseAttribute;
13861438
}
13871439

flow-server/src/main/java/com/vaadin/flow/dom/ElementStateProvider.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.vaadin.flow.server.AbstractStreamResource;
2626
import com.vaadin.flow.server.StreamResource;
2727
import com.vaadin.flow.shared.Registration;
28+
import com.vaadin.signals.Signal;
2829

2930
/**
3031
* Handles storing and retrieval of the state information for an element using a
@@ -66,6 +67,22 @@ public interface ElementStateProvider extends Serializable {
6667
*/
6768
void setAttribute(StateNode node, String attribute, String value);
6869

70+
/**
71+
* Binds the given signal to the given attribute. <code>null</code> signal
72+
* unbinds existing binding.
73+
*
74+
* @param owner
75+
* the owner element for which the signal is bound, not
76+
* <code>null</code>
77+
* @param attribute
78+
* the name of the attribute
79+
* @param signal
80+
* the signal to bind or <code>null</code> to unbind any existing
81+
* binding
82+
*/
83+
void bindAttributeSignal(Element owner, String attribute,
84+
Signal<String> signal);
85+
6986
/**
7087
* Sets the given attribute to the given {@link StreamResource} value.
7188
*

flow-server/src/main/java/com/vaadin/flow/dom/impl/AbstractTextElementStateProvider.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import com.vaadin.flow.internal.StateNode;
3333
import com.vaadin.flow.server.AbstractStreamResource;
3434
import com.vaadin.flow.shared.Registration;
35+
import com.vaadin.signals.Signal;
3536

3637
/**
3738
* Abstract element state provider for text nodes. Operations that are not
@@ -60,6 +61,12 @@ public void setAttribute(StateNode node, String attribute, String value) {
6061
throw new UnsupportedOperationException();
6162
}
6263

64+
@Override
65+
public void bindAttributeSignal(Element owner, String attribute,
66+
Signal<String> signal) {
67+
throw new UnsupportedOperationException();
68+
}
69+
6370
@Override
6471
public String getAttribute(StateNode node, String attribute) {
6572
return null;

flow-server/src/main/java/com/vaadin/flow/dom/impl/BasicElementStateProvider.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
import com.vaadin.flow.internal.nodefeature.VirtualChildrenList;
5858
import com.vaadin.flow.server.AbstractStreamResource;
5959
import com.vaadin.flow.shared.Registration;
60+
import com.vaadin.signals.Signal;
6061

6162
/**
6263
* Implementation which stores data for basic elements, i.e. elements which are
@@ -190,7 +191,16 @@ public void setAttribute(StateNode node, String attribute, String value) {
190191
assert attribute.equals(attribute.toLowerCase(Locale.ENGLISH));
191192

192193
getAttributeFeature(node).set(attribute, value);
194+
}
195+
196+
@Override
197+
public void bindAttributeSignal(Element owner, String attribute,
198+
Signal<String> signal) {
199+
assert attribute != null;
200+
assert attribute.equals(attribute.toLowerCase(Locale.ENGLISH));
193201

202+
getAttributeFeature(owner.getNode()).bindSignal(owner, attribute,
203+
signal);
194204
}
195205

196206
@Override

flow-server/src/main/java/com/vaadin/flow/dom/impl/ShadowRootStateProvider.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.vaadin.flow.dom.ClassList;
2323
import com.vaadin.flow.dom.DomEventListener;
2424
import com.vaadin.flow.dom.DomListenerRegistration;
25+
import com.vaadin.flow.dom.Element;
2526
import com.vaadin.flow.dom.Node;
2627
import com.vaadin.flow.dom.NodeVisitor;
2728
import com.vaadin.flow.dom.PropertyChangeListener;
@@ -37,6 +38,7 @@
3738
import com.vaadin.flow.internal.nodefeature.VirtualChildrenList;
3839
import com.vaadin.flow.server.AbstractStreamResource;
3940
import com.vaadin.flow.shared.Registration;
41+
import com.vaadin.signals.Signal;
4042

4143
/**
4244
* Implementation which handles shadow root nodes.
@@ -95,6 +97,12 @@ public void setAttribute(StateNode node, String attribute, String value) {
9597
throw new UnsupportedOperationException();
9698
}
9799

100+
@Override
101+
public void bindAttributeSignal(Element owner, String attribute,
102+
Signal<String> signal) {
103+
throw new UnsupportedOperationException();
104+
}
105+
98106
@Override
99107
public void setAttribute(StateNode node, String attribute,
100108
AbstractStreamResource resource) {

flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/ElementAttributeMap.java

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import tools.jackson.databind.JsonNode;
2626
import tools.jackson.databind.node.ObjectNode;
2727

28+
import com.vaadin.flow.dom.Element;
29+
import com.vaadin.flow.dom.ElementEffect;
2830
import com.vaadin.flow.internal.JacksonUtils;
2931
import com.vaadin.flow.internal.NodeOwner;
3032
import com.vaadin.flow.internal.StateNode;
@@ -36,6 +38,8 @@
3638
import com.vaadin.flow.server.StreamResourceRegistry;
3739
import com.vaadin.flow.server.VaadinSession;
3840
import com.vaadin.flow.shared.Registration;
41+
import com.vaadin.signals.BindingActiveException;
42+
import com.vaadin.signals.Signal;
3943

4044
/**
4145
* Map for element attribute values.
@@ -51,6 +55,10 @@ public class ElementAttributeMap extends NodeMap {
5155

5256
private Map<String, Registration> pendingRegistrations;
5357

58+
private Map<String, Signal<String>> attributeToSignalCache;
59+
60+
private Map<Signal<String>, Registration> attributeSignalToRegistrationCache;
61+
5462
/**
5563
* Creates a new element attribute map for the given node.
5664
*
@@ -70,9 +78,47 @@ public ElementAttributeMap(StateNode node) {
7078
* the value
7179
*/
7280
public void set(String attribute, String value) {
81+
if (hasSignal(attribute)) {
82+
throw new BindingActiveException(
83+
"setAttribute is not allowed while a binding for the given attribute exists.");
84+
}
7385
doSet(attribute, value);
7486
}
7587

88+
/**
89+
* Binds the given signal to the given attribute. <code>null</code> signal
90+
* unbinds existing binding.
91+
*
92+
* @param owner
93+
* the element owning the attribute, not <code>null</code>
94+
* @param attribute
95+
* the name of the attribute
96+
* @param signal
97+
* the signal to bind or <code>null</code> to unbind any existing
98+
* binding
99+
*/
100+
public void bindSignal(Element owner, String attribute,
101+
Signal<String> signal) {
102+
ensureSignalCache();
103+
var previousSignal = attributeToSignalCache.get(attribute);
104+
if (signal != null && previousSignal != null) {
105+
throw new BindingActiveException();
106+
}
107+
108+
Registration registration = signal != null ? ElementEffect.bind(owner,
109+
signal, (element, value) -> doSet(attribute, value)) : null;
110+
if (registration != null) {
111+
attributeSignalToRegistrationCache.put(signal, registration);
112+
}
113+
if (signal == null && attributeSignalToRegistrationCache
114+
.containsKey(previousSignal)) {
115+
attributeSignalToRegistrationCache.remove(previousSignal).remove();
116+
attributeToSignalCache.remove(attribute);
117+
} else {
118+
attributeToSignalCache.put(attribute, signal);
119+
}
120+
}
121+
76122
/**
77123
* Checks whether an attribute with the given name has been set.
78124
*
@@ -85,6 +131,11 @@ public boolean has(String attribute) {
85131
return contains(attribute);
86132
}
87133

134+
private boolean hasSignal(String attribute) {
135+
return attributeToSignalCache != null
136+
&& attributeToSignalCache.get(attribute) != null;
137+
}
138+
88139
/**
89140
* Removes the named attribute.
90141
*
@@ -93,6 +144,10 @@ public boolean has(String attribute) {
93144
*/
94145
@Override
95146
public Serializable remove(String attribute) {
147+
if (hasSignal(attribute)) {
148+
throw new BindingActiveException(
149+
"removeAttribute is not allowed while a binding for the given attribute exists.");
150+
}
96151
unregisterResource(attribute);
97152
return super.remove(attribute);
98153
}
@@ -247,7 +302,11 @@ public void execute() {
247302

248303
private void doSet(String attribute, Serializable value) {
249304
unregisterResource(attribute);
250-
put(attribute, value);
305+
if (value == null) {
306+
super.remove(attribute);
307+
} else {
308+
put(attribute, value);
309+
}
251310
}
252311

253312
private void unsetResource(String attribute) {
@@ -267,4 +326,12 @@ private VaadinSession getSession() {
267326
return ((StateTree) owner).getUI().getSession();
268327
}
269328

329+
private void ensureSignalCache() {
330+
if (attributeToSignalCache == null) {
331+
attributeToSignalCache = new HashMap<>();
332+
}
333+
if (attributeSignalToRegistrationCache == null) {
334+
attributeSignalToRegistrationCache = new HashMap<>();
335+
}
336+
}
270337
}

0 commit comments

Comments
 (0)