Skip to content

Commit 54fafc3

Browse files
authored
feat: Add bindRequiredIndicatorVisible to HasValue / HasValueAndElement (#23589)
Allow all HasValueAndElement components to bind the required indicator visible state to a Signal, following the same pattern as bindReadOnly.
1 parent a16c172 commit 54fafc3

File tree

3 files changed

+220
-0
lines changed

3 files changed

+220
-0
lines changed

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,4 +342,38 @@ default void bindReadOnly(Signal<Boolean> readOnlySignal) {
342342
"Binding read only state to a Signal is not supported by "
343343
+ getClass().getSimpleName());
344344
}
345+
346+
/**
347+
* Binds a {@link Signal}'s value to the required indicator visible state of
348+
* this component and keeps the state synchronized with the signal value
349+
* while the component is in attached state. When the component is in
350+
* detached state, signal value changes have no effect.
351+
* <p>
352+
* While a Signal is bound to the required indicator visible state, any
353+
* attempt to set the state manually with
354+
* {@link #setRequiredIndicatorVisible(boolean)} throws
355+
* {@link com.vaadin.flow.signals.BindingActiveException}. Same happens when
356+
* trying to bind a new Signal while one is already bound.
357+
* <p>
358+
* Example of usage:
359+
*
360+
* <pre>
361+
* ValueSignal&lt;Boolean&gt; signal = new ValueSignal&lt;&gt;(false);
362+
* Input component = new Input();
363+
* add(component);
364+
* component.bindRequiredIndicatorVisible(signal);
365+
* signal.set(true); // The required indicator becomes visible
366+
* </pre>
367+
*
368+
* @param requiredSignal
369+
* the signal to bind, not <code>null</code>
370+
* @throws com.vaadin.flow.signals.BindingActiveException
371+
* thrown when there is already an existing binding
372+
* @see #setRequiredIndicatorVisible(boolean)
373+
*/
374+
default void bindRequiredIndicatorVisible(Signal<Boolean> requiredSignal) {
375+
throw new UnsupportedOperationException(
376+
"Binding required indicator visible state to a Signal is not supported by "
377+
+ getClass().getSimpleName());
378+
}
345379
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,9 @@ default boolean isReadOnly() {
5555
default void bindReadOnly(Signal<Boolean> readOnlySignal) {
5656
getElement().bindProperty("readonly", readOnlySignal, null);
5757
}
58+
59+
@Override
60+
default void bindRequiredIndicatorVisible(Signal<Boolean> requiredSignal) {
61+
getElement().bindProperty("required", requiredSignal, null);
62+
}
5863
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
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;
17+
18+
import org.junit.jupiter.api.Test;
19+
20+
import com.vaadin.flow.dom.SignalsUnitTest;
21+
import com.vaadin.flow.signals.BindingActiveException;
22+
import com.vaadin.flow.signals.Signal;
23+
import com.vaadin.flow.signals.local.ValueSignal;
24+
25+
import static org.junit.jupiter.api.Assertions.assertFalse;
26+
import static org.junit.jupiter.api.Assertions.assertThrows;
27+
import static org.junit.jupiter.api.Assertions.assertTrue;
28+
29+
/**
30+
* Tests for {@link HasValue#bindRequiredIndicatorVisible(Signal)}.
31+
*/
32+
class HasValueBindRequiredIndicatorVisibleTest extends SignalsUnitTest {
33+
34+
@Test
35+
public void bindRequired_elementAttachedBefore_bindingActive() {
36+
TestComponent component = new TestComponent();
37+
UI.getCurrent().add(component);
38+
assertFalse(component.isRequiredIndicatorVisible());
39+
40+
ValueSignal<Boolean> signal = new ValueSignal<>(true);
41+
component.bindRequiredIndicatorVisible(signal);
42+
43+
assertTrue(component.isRequiredIndicatorVisible());
44+
}
45+
46+
@Test
47+
public void bindRequired_elementAttachedAfter_bindingActive() {
48+
TestComponent component = new TestComponent();
49+
assertFalse(component.isRequiredIndicatorVisible());
50+
51+
ValueSignal<Boolean> signal = new ValueSignal<>(true);
52+
component.bindRequiredIndicatorVisible(signal);
53+
UI.getCurrent().add(component);
54+
55+
assertTrue(component.isRequiredIndicatorVisible());
56+
}
57+
58+
@Test
59+
public void bindRequired_elementAttached_bindingActive() {
60+
TestComponent component = new TestComponent();
61+
UI.getCurrent().add(component);
62+
ValueSignal<Boolean> signal = new ValueSignal<>(true);
63+
component.bindRequiredIndicatorVisible(signal);
64+
65+
// initially true
66+
assertTrue(component.isRequiredIndicatorVisible());
67+
68+
// true -> false
69+
signal.set(false);
70+
assertFalse(component.isRequiredIndicatorVisible());
71+
72+
// false -> true
73+
signal.set(true);
74+
assertTrue(component.isRequiredIndicatorVisible());
75+
}
76+
77+
@Test
78+
public void bindRequired_elementNotAttached_bindingInactive() {
79+
TestComponent component = new TestComponent();
80+
ValueSignal<Boolean> signal = new ValueSignal<>(true);
81+
component.bindRequiredIndicatorVisible(signal);
82+
signal.set(false);
83+
84+
assertFalse(component.isRequiredIndicatorVisible());
85+
}
86+
87+
@Test
88+
public void bindRequired_elementDetached_bindingInactive() {
89+
TestComponent component = new TestComponent();
90+
UI.getCurrent().add(component);
91+
ValueSignal<Boolean> signal = new ValueSignal<>(true);
92+
component.bindRequiredIndicatorVisible(signal);
93+
component.removeFromParent();
94+
signal.set(false); // ignored
95+
96+
assertTrue(component.isRequiredIndicatorVisible());
97+
}
98+
99+
@Test
100+
public void bindRequired_elementReAttached_bindingActivate() {
101+
TestComponent component = new TestComponent();
102+
UI.getCurrent().add(component);
103+
ValueSignal<Boolean> signal = new ValueSignal<>(true);
104+
component.bindRequiredIndicatorVisible(signal);
105+
component.removeFromParent();
106+
signal.set(false);
107+
UI.getCurrent().add(component);
108+
109+
assertFalse(component.isRequiredIndicatorVisible());
110+
}
111+
112+
@Test
113+
public void bindRequired_bindOrSetRequiredWhileBindingIsActive_throwException() {
114+
TestComponent component = new TestComponent();
115+
UI.getCurrent().add(component);
116+
component.bindRequiredIndicatorVisible(new ValueSignal<>(true));
117+
118+
assertThrows(BindingActiveException.class, () -> component
119+
.bindRequiredIndicatorVisible(new ValueSignal<>(false)));
120+
assertThrows(BindingActiveException.class,
121+
() -> component.setRequiredIndicatorVisible(false));
122+
assertTrue(component.isRequiredIndicatorVisible());
123+
}
124+
125+
@Test
126+
public void bindRequired_nullSignal_throwsNPE() {
127+
TestComponent component = new TestComponent();
128+
UI.getCurrent().add(component);
129+
130+
assertThrows(NullPointerException.class,
131+
() -> component.bindRequiredIndicatorVisible(null));
132+
}
133+
134+
@Test
135+
public void bindRequired_nullSignalValue_setsRequiredToFalse() {
136+
TestComponent component = new TestComponent();
137+
UI.getCurrent().add(component);
138+
ValueSignal<Boolean> signal = new ValueSignal<>(true);
139+
component.bindRequiredIndicatorVisible(signal);
140+
assertTrue(component.isRequiredIndicatorVisible());
141+
142+
// null transforms to false (default value for boolean property)
143+
signal.set(null);
144+
assertFalse(component.isRequiredIndicatorVisible());
145+
}
146+
147+
@Test
148+
public void bindRequired_toggleSignalValue_requiredUpdates() {
149+
TestComponent component = new TestComponent();
150+
UI.getCurrent().add(component);
151+
ValueSignal<Boolean> signal = new ValueSignal<>(false);
152+
component.bindRequiredIndicatorVisible(signal);
153+
assertFalse(component.isRequiredIndicatorVisible());
154+
155+
signal.set(true);
156+
assertTrue(component.isRequiredIndicatorVisible());
157+
158+
signal.set(false);
159+
assertFalse(component.isRequiredIndicatorVisible());
160+
161+
signal.set(true);
162+
assertTrue(component.isRequiredIndicatorVisible());
163+
}
164+
165+
/**
166+
* Test component implementing {@link HasValueAndElement}.
167+
*/
168+
@Tag(Tag.INPUT)
169+
private static class TestComponent
170+
extends AbstractField<TestComponent, String> {
171+
172+
public TestComponent() {
173+
super("");
174+
}
175+
176+
@Override
177+
protected void setPresentationValue(String newPresentationValue) {
178+
// NOP
179+
}
180+
}
181+
}

0 commit comments

Comments
 (0)