Skip to content

Commit 3c26e12

Browse files
authored
feat: add signal bind methods for NativeDetails, HtmlObject, and HasStyle (#23594)
1 parent 2b9904e commit 3c26e12

File tree

6 files changed

+392
-0
lines changed

6 files changed

+392
-0
lines changed

flow-html-components/src/main/java/com/vaadin/flow/component/html/HtmlObject.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.vaadin.flow.server.StreamResource;
3030
import com.vaadin.flow.server.streams.AbstractDownloadHandler;
3131
import com.vaadin.flow.server.streams.DownloadHandler;
32+
import com.vaadin.flow.signals.Signal;
3233

3334
/**
3435
* Component representing a <code>&lt;object&gt;</code> element.
@@ -341,6 +342,26 @@ public String getData() {
341342
return get(dataDescriptor);
342343
}
343344

345+
/**
346+
* Binds the given signal to the "data" attribute of this component.
347+
* <p>
348+
* When a signal is bound, the data attribute is kept synchronized with the
349+
* signal value while the component is attached. When the component is
350+
* detached, signal value changes have no effect.
351+
* <p>
352+
* While a signal is bound, any attempt to set the data attribute manually
353+
* through {@link #setData(String)} throws a
354+
* {@link com.vaadin.flow.signals.BindingActiveException}.
355+
*
356+
* @param signal
357+
* the signal to bind the data attribute to, not {@code null}
358+
* @see #setData(String)
359+
* @since 25.1
360+
*/
361+
public void bindData(Signal<String> signal) {
362+
getElement().bindAttribute("data", signal);
363+
}
364+
344365
/**
345366
* Sets the "type" attribute value.
346367
*

flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeDetails.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.vaadin.flow.component.Synchronize;
2929
import com.vaadin.flow.component.Tag;
3030
import com.vaadin.flow.dom.Element;
31+
import com.vaadin.flow.function.SerializableConsumer;
3132
import com.vaadin.flow.shared.Registration;
3233
import com.vaadin.flow.signals.Signal;
3334

@@ -277,6 +278,30 @@ public void setOpen(boolean open) {
277278
getElement().setProperty("open", open);
278279
}
279280

281+
/**
282+
* Binds the open state to the given signal. Signal changes push to the DOM
283+
* property. If a non-null {@code writeCallback} is provided, client-side
284+
* property changes are pushed back through the callback, making the binding
285+
* two-way. If {@code writeCallback} is {@code null}, the binding is
286+
* read-only.
287+
* <p>
288+
* While a signal is bound, any attempt to set the open state manually
289+
* throws {@link com.vaadin.flow.signals.BindingActiveException}.
290+
*
291+
* @param signal
292+
* the signal to bind, not {@code null}
293+
* @param writeCallback
294+
* callback invoked when the client-side value changes, or
295+
* {@code null} for a read-only binding
296+
* @since 25.1
297+
*/
298+
public void bindOpen(Signal<Boolean> signal,
299+
SerializableConsumer<Boolean> writeCallback) {
300+
Objects.requireNonNull(signal, "Signal cannot be null");
301+
getElement().bindProperty("open",
302+
signal.map(v -> v == null ? Boolean.FALSE : v), writeCallback);
303+
}
304+
280305
/**
281306
* Represents the DOM event "toggle".
282307
* <p>
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright 2000-2026 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.html;
17+
18+
import org.junit.Test;
19+
20+
import com.vaadin.flow.component.UI;
21+
import com.vaadin.flow.dom.SignalsUnitTest;
22+
import com.vaadin.flow.signals.BindingActiveException;
23+
import com.vaadin.flow.signals.local.ValueSignal;
24+
25+
import static org.junit.Assert.assertEquals;
26+
import static org.junit.Assert.assertThrows;
27+
28+
/**
29+
* Tests for {@link HtmlObject#bindData(com.vaadin.flow.signals.Signal)}.
30+
*/
31+
public class HtmlObjectBindDataTest extends SignalsUnitTest {
32+
33+
@Test
34+
public void bindData_updatesAttributeOnSignalChange() {
35+
HtmlObject htmlObject = new HtmlObject();
36+
UI.getCurrent().add(htmlObject);
37+
38+
ValueSignal<String> signal = new ValueSignal<>("");
39+
htmlObject.bindData(signal);
40+
41+
signal.set("https://example.com/data.swf");
42+
assertEquals("https://example.com/data.swf",
43+
htmlObject.getElement().getAttribute("data"));
44+
45+
signal.set("https://example.com/other.swf");
46+
assertEquals("https://example.com/other.swf",
47+
htmlObject.getElement().getAttribute("data"));
48+
}
49+
50+
@Test
51+
public void bindData_attachedThenDetached_stopsUpdates() {
52+
HtmlObject htmlObject = new HtmlObject();
53+
UI.getCurrent().add(htmlObject);
54+
55+
ValueSignal<String> signal = new ValueSignal<>("initial");
56+
htmlObject.bindData(signal);
57+
assertEquals("initial", htmlObject.getElement().getAttribute("data"));
58+
59+
// Detach the component
60+
UI.getCurrent().remove(htmlObject);
61+
62+
// Update value after detach – attribute should remain unchanged
63+
signal.set("updated");
64+
assertEquals("initial", htmlObject.getElement().getAttribute("data"));
65+
}
66+
67+
@Test
68+
public void bindData_nullSignal_throwsNPE() {
69+
HtmlObject htmlObject = new HtmlObject();
70+
UI.getCurrent().add(htmlObject);
71+
72+
assertThrows(NullPointerException.class,
73+
() -> htmlObject.bindData(null));
74+
}
75+
76+
@Test(expected = BindingActiveException.class)
77+
public void bindData_setDataWhileBound_throwsException() {
78+
HtmlObject htmlObject = new HtmlObject();
79+
UI.getCurrent().add(htmlObject);
80+
81+
ValueSignal<String> signal = new ValueSignal<>("");
82+
htmlObject.bindData(signal);
83+
84+
htmlObject.setData("manual");
85+
}
86+
87+
@Test(expected = BindingActiveException.class)
88+
public void bindData_bindAgainWhileBound_throwsException() {
89+
HtmlObject htmlObject = new HtmlObject();
90+
UI.getCurrent().add(htmlObject);
91+
92+
ValueSignal<String> signal = new ValueSignal<>("");
93+
htmlObject.bindData(signal);
94+
95+
htmlObject.bindData(new ValueSignal<>("other"));
96+
}
97+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright 2000-2026 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.html;
17+
18+
import org.junit.Test;
19+
20+
import com.vaadin.flow.component.UI;
21+
import com.vaadin.flow.dom.SignalsUnitTest;
22+
import com.vaadin.flow.signals.BindingActiveException;
23+
import com.vaadin.flow.signals.local.ValueSignal;
24+
25+
import static org.junit.Assert.assertEquals;
26+
import static org.junit.Assert.assertFalse;
27+
import static org.junit.Assert.assertThrows;
28+
import static org.junit.Assert.assertTrue;
29+
30+
public class NativeDetailsBindOpenTest extends SignalsUnitTest {
31+
32+
@Test
33+
public void bindOpen_signalBound_propertySync() {
34+
NativeDetails details = new NativeDetails();
35+
ValueSignal<Boolean> signal = new ValueSignal<>(false);
36+
details.bindOpen(signal, signal::set);
37+
UI.getCurrent().add(details);
38+
39+
assertFalse(details.isOpen());
40+
41+
signal.set(true);
42+
assertTrue(details.isOpen());
43+
44+
signal.set(false);
45+
assertFalse(details.isOpen());
46+
}
47+
48+
@Test
49+
public void bindOpen_notAttached_noEffect() {
50+
NativeDetails details = new NativeDetails();
51+
ValueSignal<Boolean> signal = new ValueSignal<>(false);
52+
details.bindOpen(signal, signal::set);
53+
54+
boolean initial = details.isOpen();
55+
signal.set(true);
56+
assertEquals(initial, details.isOpen());
57+
}
58+
59+
@Test
60+
public void bindOpen_detachAndReattach() {
61+
NativeDetails details = new NativeDetails();
62+
ValueSignal<Boolean> signal = new ValueSignal<>(false);
63+
details.bindOpen(signal, signal::set);
64+
UI.getCurrent().add(details);
65+
66+
signal.set(true);
67+
assertTrue(details.isOpen());
68+
69+
details.removeFromParent();
70+
signal.set(false);
71+
assertTrue(details.isOpen());
72+
73+
UI.getCurrent().add(details);
74+
assertFalse(details.isOpen());
75+
}
76+
77+
@Test
78+
public void bindOpen_setWhileBound_throws() {
79+
NativeDetails details = new NativeDetails();
80+
ValueSignal<Boolean> signal = new ValueSignal<>(false);
81+
details.bindOpen(signal, signal::set);
82+
UI.getCurrent().add(details);
83+
84+
assertThrows(BindingActiveException.class, () -> details.setOpen(true));
85+
}
86+
87+
@Test
88+
public void bindOpen_doubleBind_throws() {
89+
NativeDetails details = new NativeDetails();
90+
ValueSignal<Boolean> signal = new ValueSignal<>(false);
91+
details.bindOpen(signal, signal::set);
92+
93+
assertThrows(BindingActiveException.class,
94+
() -> details.bindOpen(new ValueSignal<>(true), null));
95+
}
96+
97+
@Test
98+
public void bindOpen_nullSignal_throwsNPE() {
99+
NativeDetails details = new NativeDetails();
100+
assertThrows(NullPointerException.class,
101+
() -> details.bindOpen(null, null));
102+
}
103+
}

flow-server/src/main/java/com/vaadin/flow/component/HasStyle.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.vaadin.flow.dom.ClassList;
2121
import com.vaadin.flow.dom.Element;
2222
import com.vaadin.flow.dom.Style;
23+
import com.vaadin.flow.signals.Signal;
2324

2425
/**
2526
* Represents {@link Component} which has class attribute and inline styles.
@@ -161,6 +162,25 @@ default void addClassNames(String... classNames) {
161162
}
162163
}
163164

165+
/**
166+
* Binds the presence of the given CSS class name to a {@link Signal}. When
167+
* the signal's value is {@code true}, the class name is added; when
168+
* {@code false}, the class name is removed.
169+
* <p>
170+
* The binding is active while the component is attached to a UI. When
171+
* detached, signal value changes have no effect.
172+
*
173+
* @param className
174+
* the CSS class name to toggle, not {@code null} or blank
175+
* @param signal
176+
* the boolean signal to bind to, not {@code null}
177+
* @see ClassList#bind(String, Signal)
178+
* @since 25.1
179+
*/
180+
default void bindClassName(String className, Signal<Boolean> signal) {
181+
getClassNames().bind(className, signal);
182+
}
183+
164184
/**
165185
* Removes one or more CSS class names from component. Multiple class names
166186
* can be specified by using spaces or by giving multiple parameters.

0 commit comments

Comments
 (0)