Skip to content

Commit 704541b

Browse files
authored
feat: add signal binding to element text (#22406) (#22543)
Implements signal binding to element text. Fixes #22406
1 parent ae76608 commit 704541b

File tree

11 files changed

+413
-15
lines changed

11 files changed

+413
-15
lines changed

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

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,16 @@
4949
import com.vaadin.flow.internal.JacksonUtils;
5050
import com.vaadin.flow.internal.JavaScriptSemantics;
5151
import com.vaadin.flow.internal.StateNode;
52+
import com.vaadin.flow.internal.nodefeature.TextBindingFeature;
5253
import com.vaadin.flow.internal.nodefeature.VirtualChildrenList;
5354
import com.vaadin.flow.server.AbstractStreamResource;
5455
import com.vaadin.flow.server.Command;
5556
import com.vaadin.flow.server.StreamResource;
5657
import com.vaadin.flow.server.StreamResourceRegistry;
5758
import com.vaadin.flow.server.streams.ElementRequestHandler;
5859
import com.vaadin.flow.shared.Registration;
60+
import com.vaadin.signals.BindingActiveException;
61+
import com.vaadin.signals.Signal;
5962

6063
/**
6164
* Represents an element in the DOM.
@@ -1172,35 +1175,91 @@ public boolean isTextNode() {
11721175
* the text content to set, <code>null</code> is interpreted as
11731176
* an empty string
11741177
* @return this element
1178+
* @throws BindingActiveException
1179+
* if a binding has been set on the text content of this element
11751180
*/
11761181
public Element setText(String textContent) {
1182+
TextBindingFeature feature = getNode()
1183+
.getFeature(TextBindingFeature.class);
1184+
if (feature.hasBinding()) {
1185+
throw new BindingActiveException(
1186+
"setText is not allowed while a binding for text exists.");
1187+
}
1188+
11771189
if (textContent == null) {
11781190
// Browsers work this way
11791191
textContent = "";
11801192
}
1193+
setTextContent(textContent);
1194+
1195+
return this;
1196+
}
11811197

1198+
private void setTextContent(String textContent) {
11821199
if (isTextNode()) {
11831200
getStateProvider().setTextContent(getNode(), textContent);
11841201
} else {
11851202
if (textContent.isEmpty()) {
11861203
removeAllChildren();
11871204
} else {
1188-
setTextContent(textContent);
1205+
Element child;
1206+
if (getChildCount() == 1 && getChild(0).isTextNode()) {
1207+
child = getChild(0).setText(textContent);
1208+
} else {
1209+
child = createText(textContent);
1210+
}
1211+
removeAllChildren();
1212+
appendChild(child);
11891213
}
11901214
}
1191-
1192-
return this;
11931215
}
11941216

1195-
private void setTextContent(String textContent) {
1196-
Element child;
1197-
if (getChildCount() == 1 && getChild(0).isTextNode()) {
1198-
child = getChild(0).setText(textContent);
1217+
/**
1218+
* Binds a {@link Signal}'s value to the text content of this element and
1219+
* creates a Signal effect function executing the setter whenever the signal
1220+
* value changes. <code>null</code> signal unbinds the existing binding.
1221+
* <p>
1222+
* Text content is synchronized with the signal value whenever the element
1223+
* is attached. When the element is detached, signal value changes have no
1224+
* effect.
1225+
* <p>
1226+
* While a Signal is bound to an attribute, any attempt to set the text
1227+
* content manually throws
1228+
* {@link com.vaadin.signals.BindingActiveException}. Same happens when
1229+
* trying to bind a new Signal while one is already bound.
1230+
* <p>
1231+
* Example of usage:
1232+
*
1233+
* <pre>
1234+
* ValueSignal&lt;String&gt; signal = new ValueSignal&lt;&gt;("");
1235+
* Element element = new Element("span");
1236+
* getElement().appendChild(element);
1237+
* element.bindText(signal);
1238+
* signal.value("text"); // The element text content is set to "text"
1239+
* </pre>
1240+
*
1241+
* @param signal
1242+
* the signal to bind or <code>null</code> to unbind any existing
1243+
* binding
1244+
* @throws BindingActiveException
1245+
* thrown when there is already an existing binding
1246+
* @see #setText(String)
1247+
*/
1248+
public void bindText(Signal<String> signal) {
1249+
TextBindingFeature feature = getNode()
1250+
.getFeature(TextBindingFeature.class);
1251+
1252+
if (signal == null) {
1253+
feature.removeBinding();
11991254
} else {
1200-
child = createText(textContent);
1255+
if (feature.hasBinding() && getNode().isAttached()) {
1256+
throw new BindingActiveException();
1257+
}
1258+
1259+
Registration registration = ElementEffect.bind(this, signal,
1260+
(element, value) -> setTextContent(value));
1261+
feature.setBinding(registration, signal);
12011262
}
1202-
removeAllChildren();
1203-
appendChild(child);
12041263
}
12051264

12061265
/**

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import com.vaadin.flow.internal.nodefeature.PolymerServerEventHandlers;
5454
import com.vaadin.flow.internal.nodefeature.ReturnChannelMap;
5555
import com.vaadin.flow.internal.nodefeature.ShadowRootData;
56+
import com.vaadin.flow.internal.nodefeature.TextBindingFeature;
5657
import com.vaadin.flow.internal.nodefeature.VirtualChildrenList;
5758
import com.vaadin.flow.server.AbstractStreamResource;
5859
import com.vaadin.flow.shared.Registration;
@@ -85,7 +86,7 @@ public class BasicElementStateProvider extends AbstractNodeStateProvider {
8586
PolymerServerEventHandlers.class, ClientCallableHandlers.class,
8687
PolymerEventListenerMap.class, ShadowRootData.class,
8788
AttachExistingElementFeature.class, VirtualChildrenList.class,
88-
ReturnChannelMap.class, InertData.class };
89+
ReturnChannelMap.class, InertData.class, TextBindingFeature.class };
8990

9091
private BasicElementStateProvider() {
9192
// Not meant to be sub classed and only once instance should ever exist

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@
1616
package com.vaadin.flow.dom.impl;
1717

1818
import java.io.ObjectStreamException;
19-
import java.util.Collections;
19+
import java.util.List;
2020

2121
import com.vaadin.flow.dom.Node;
2222
import com.vaadin.flow.internal.StateNode;
2323
import com.vaadin.flow.internal.nodefeature.ComponentMapping;
2424
import com.vaadin.flow.internal.nodefeature.ReturnChannelMap;
25+
import com.vaadin.flow.internal.nodefeature.TextBindingFeature;
2526
import com.vaadin.flow.internal.nodefeature.TextNodeMap;
2627

2728
/**
@@ -52,7 +53,7 @@ public static StateNode createStateNode(String text) {
5253
assert text != null;
5354

5455
StateNode node = new StateNode(
55-
Collections.singletonList(TextNodeMap.class),
56+
List.of(TextNodeMap.class, TextBindingFeature.class),
5657
ComponentMapping.class, ReturnChannelMap.class);
5758
node.getFeature(TextNodeMap.class).setText(text);
5859

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ private <T extends NodeFeature> NodeFeatureData(
6666
NodeFeatures.ELEMENT_DATA);
6767
registerFeature(TextNodeMap.class, TextNodeMap::new,
6868
NodeFeatures.TEXT_NODE);
69+
registerFeature(TextBindingFeature.class, TextBindingFeature::new,
70+
NodeFeatures.TEXT_BINDING);
6971
registerFeature(ModelList.class, ModelList::new,
7072
NodeFeatures.TEMPLATE_MODELLIST);
7173
registerFeature(BasicTypeValue.class, BasicTypeValue::new,

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,11 @@ public final class NodeFeatures {
142142
*/
143143
public static final int INERT_DATA = 26;
144144

145+
/**
146+
* Id for {@link TextBindingFeature}.
147+
*/
148+
public static final int TEXT_BINDING = 27;
149+
145150
private NodeFeatures() {
146151
// Only static
147152
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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.internal.nodefeature;
17+
18+
import com.vaadin.flow.internal.StateNode;
19+
import com.vaadin.flow.shared.Registration;
20+
import com.vaadin.signals.Signal;
21+
22+
/**
23+
* Node feature for binding a {@link Signal} to the text content of a node.
24+
* <p>
25+
* For internal use only. May be renamed or removed in a future release.
26+
*/
27+
public class TextBindingFeature extends ServerSideFeature {
28+
/**
29+
* Creates a TextBindingFeature for the given node.
30+
*
31+
* @param node
32+
* the node which supports the feature
33+
*/
34+
public TextBindingFeature(StateNode node) {
35+
super(node);
36+
}
37+
38+
private Registration registration;
39+
private Signal<String> textSignal;
40+
41+
public void setBinding(Registration registration,
42+
Signal<String> textSignal) {
43+
this.registration = registration;
44+
this.textSignal = textSignal;
45+
}
46+
47+
public boolean hasBinding() {
48+
return textSignal != null && registration != null;
49+
}
50+
51+
public void removeBinding() {
52+
if (registration != null) {
53+
registration.remove();
54+
}
55+
registration = null;
56+
textSignal = null;
57+
}
58+
}

0 commit comments

Comments
 (0)