Skip to content

Commit 5506852

Browse files
mshabarovclaude
andauthored
refactor!: Rename Binder.Binding.value() to valueSignal() and return signal (#23665)
* refactor: Rename Binder.Binding.value() to valueSignal() and return signal Reworks Binding.value() to return a ValueSignal<TARGET> instead of the raw value, and renames the method to valueSignal() to reflect this change. The valueSignal is lazily initialized and updated whenever: - The field value changes through user interaction - The field value is set from a bean (via setBean/readBean) Simplifies the implementation by removing internalValidationTriggerSignal and reusing valueSignal for tracking purposes. When validators call valueSignal().get(), they automatically track the signal, and updates to the signal trigger re-validation without needing a separate boolean toggle signal. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * rename to bindingValueSignal * add multiple signal calls test --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f0cd662 commit 5506852

File tree

2 files changed

+84
-54
lines changed

2 files changed

+84
-54
lines changed

flow-data/src/main/java/com/vaadin/flow/data/binder/Binder.java

Lines changed: 32 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -372,8 +372,8 @@ void setIsAppliedPredicate(
372372
SerializablePredicate<Binding<BEAN, TARGET>> isAppliedPredicate);
373373

374374
/**
375-
* Returns the current converted and validated value of this binding's
376-
* field.
375+
* Returns a signal holding the current converted and validated value of
376+
* this binding's field.
377377
* <p>
378378
* This method is primarily designed for implementing cross-field
379379
* validation, where one field's validator needs to access the value of
@@ -384,17 +384,17 @@ void setIsAppliedPredicate(
384384
* The Binder automatically runs validators inside a
385385
* {@link Signal#effect(Component, com.vaadin.flow.signals.function.EffectAction)}
386386
* context. This makes validators reactive to signal changes - when you
387-
* call {@code value()} on another binding from within a validator, the
388-
* validator will automatically re-run whenever that other binding's
389-
* value changes.
387+
* call {@code valueSignal()} on another binding from within a
388+
* validator, the validator will automatically re-run whenever that
389+
* other binding's value changes.
390390
* <p>
391391
* For cross-field validation to work automatically, the fields must be
392392
* attached to the UI component tree. Detached fields will not trigger
393393
* automatic re-validation, though manual validation via
394394
* {@link Binder#validate()} will still work.
395395
* <p>
396396
* <b>Example - Cross-field validation:</b>
397-
*
397+
*
398398
* <pre>
399399
* {@code
400400
* Binder<UserRegistration> binder = new Binder<>(
@@ -406,10 +406,9 @@ void setIsAppliedPredicate(
406406
* Binding<UserRegistration, String> passwordBinding = binder
407407
* .forField(passwordField).bind("password");
408408
*
409-
* binder.forField(confirmField)
410-
* .withValidator(text -> text.equals(passwordBinding.value()),
411-
* "Both fields must match")
412-
* .bind("confirmPassword");
409+
* binder.forField(confirmField).withValidator(
410+
* text -> text.equals(passwordBinding.valueSignal().get()),
411+
* "Both fields must match").bind("confirmPassword");
413412
*
414413
* add(passwordField, confirmField);
415414
*
@@ -430,8 +429,8 @@ void setIsAppliedPredicate(
430429
* }
431430
* </pre>
432431
*
433-
* @return the current converted and validated value of this binding's
434-
* field
432+
* @return a signal holding the current converted and validated value of
433+
* this binding's field
435434
*
436435
* @see BindingBuilder#withValidator(Validator)
437436
* @see com.vaadin.flow.component.HasValue#bindValue
@@ -440,9 +439,9 @@ void setIsAppliedPredicate(
440439
*
441440
* @since 25.1
442441
*/
443-
default TARGET value() {
442+
default ValueSignal<TARGET> valueSignal() {
444443
throw new UnsupportedOperationException(
445-
"value() is not supported by "
444+
"valueSignal() is not supported by "
446445
+ getClass().getSimpleName());
447446
}
448447
}
@@ -1513,7 +1512,7 @@ protected static class BindingImpl<BEAN, FIELDVALUE, TARGET>
15131512

15141513
private transient Registration signalRegistration;
15151514

1516-
private transient ValueSignal<Boolean> internalValidationTriggerSignal;
1515+
private transient ValueSignal<TARGET> bindingValueSignal;
15171516

15181517
public BindingImpl(BindingBuilderImpl<BEAN, FIELDVALUE, TARGET> builder,
15191518
ValueProvider<BEAN, TARGET> getter,
@@ -1612,9 +1611,10 @@ public void unbind() {
16121611
if (signalRegistration != null) {
16131612
signalRegistration.remove();
16141613
signalRegistration = null;
1615-
internalValidationTriggerSignal = null;
16161614
}
16171615

1616+
bindingValueSignal = null;
1617+
16181618
field = null;
16191619
}
16201620

@@ -1764,11 +1764,12 @@ private void handleFieldValueChange(
17641764
removeFromChangedBindingsIfReverted(
17651765
getBinder()::removeFromChangedBindings);
17661766
getBinder().fireEvent(event);
1767-
if (internalValidationTriggerSignal != null) {
1768-
// Trigger re-validation signal to notify validators using
1769-
// value()
1770-
internalValidationTriggerSignal
1771-
.set(!internalValidationTriggerSignal.peek());
1767+
if (bindingValueSignal != null) {
1768+
// Update the value signal with the new field value
1769+
// This automatically triggers re-validation of validators
1770+
// that depend on this binding's value
1771+
HasValue<?, TARGET> field = (HasValue<?, TARGET>) getField();
1772+
bindingValueSignal.set(field.getValue());
17721773
}
17731774
}
17741775
}
@@ -1827,6 +1828,11 @@ private void convertAndSetFieldValue(TARGET modelValue) {
18271828
try {
18281829
field.setValue(convertedValue);
18291830
initialValue = modelValue;
1831+
if (bindingValueSignal != null) {
1832+
// Update the value signal when field value is set from
1833+
// bean
1834+
bindingValueSignal.set(modelValue);
1835+
}
18301836
} catch (RuntimeException e) {
18311837
/*
18321838
* Add an additional hint to the exception for the typical
@@ -2002,21 +2008,12 @@ public void setIsAppliedPredicate(
20022008
}
20032009

20042010
@Override
2005-
public TARGET value() {
2006-
HasValue<?, TARGET> field = (HasValue<?, TARGET>) getField();
2007-
trackUsageOfInternalValidationSignal();
2008-
return field.getValue();
2009-
}
2010-
2011-
private void trackUsageOfInternalValidationSignal() {
2012-
if (!UsageTracker.isActive()) {
2013-
return;
2014-
}
2015-
if (internalValidationTriggerSignal == null) {
2016-
internalValidationTriggerSignal = new ValueSignal<>(false);
2011+
public ValueSignal<TARGET> valueSignal() {
2012+
if (bindingValueSignal == null) {
2013+
HasValue<?, TARGET> field = (HasValue<?, TARGET>) getField();
2014+
bindingValueSignal = new ValueSignal<>(field.getValue());
20172015
}
2018-
// registers tracking
2019-
internalValidationTriggerSignal.get();
2016+
return bindingValueSignal;
20202017
}
20212018

20222019
}

flow-data/src/test/java/com/vaadin/flow/data/binder/BinderSignalTest.java

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -58,33 +58,33 @@ public void setup() {
5858

5959
/**
6060
* Returns validator that is valid only if both target value and cross-field
61-
* binding value are not empty. Calls Binding.value() to get the other field
62-
* value.
61+
* binding value are not empty. Calls Binding.valueSignal() to get the other
62+
* field value.
6363
*/
6464
private SerializablePredicate<String> hasTextValuesValidator(
6565
Binder.Binding<?, String> otherFieldBinding) {
6666
return (String value) -> !value.isEmpty()
67-
&& !otherFieldBinding.value().isEmpty();
67+
&& !otherFieldBinding.valueSignal().get().isEmpty();
6868
}
6969

70-
// verifies that Binding.value() works with property name bindings
70+
// verifies that Binding.valueSignal() works with property name bindings
7171
@Test
7272
public void bindingValue_withBinderBindPropertyName() {
7373
item.setFirstName("Alice");
7474

7575
var field = new TestTextField();
7676
var binding = binder.bind(field, "firstName");
7777

78-
assertEquals("", binding.value());
78+
assertEquals("", binding.valueSignal().peek());
7979
assertEquals("", field.getValue());
8080

8181
binder.setBean(item);
8282

83-
assertEquals("Alice", binding.value());
83+
assertEquals("Alice", binding.valueSignal().peek());
8484
assertEquals("Alice", field.getValue());
8585
}
8686

87-
// verifies that Binding.value() works with getter/setter bindings
87+
// verifies that Binding.valueSignal() works with getter/setter bindings
8888
@Test
8989
public void bindingValue_withBinderBindGetterSetter() {
9090
binder = new Binder<>();
@@ -94,16 +94,17 @@ public void bindingValue_withBinderBindGetterSetter() {
9494
var binding = binder.bind(field, Person::getFirstName,
9595
Person::setFirstName);
9696

97-
assertEquals("", binding.value());
97+
assertEquals("", binding.valueSignal().peek());
9898
assertEquals("", field.getValue());
9999

100100
binder.setBean(item);
101101

102-
assertEquals("Alice", binding.value());
102+
assertEquals("Alice", binding.valueSignal().peek());
103103
assertEquals("Alice", field.getValue());
104104
}
105105

106-
// verifies that Binding.value() with a signal-bound field works correctly
106+
// verifies that Binding.valueSignal() with a signal-bound field works
107+
// correctly
107108
@Test
108109
public void bindingValue_withSignal() {
109110
binder = new Binder<>();
@@ -119,13 +120,13 @@ public void bindingValue_withSignal() {
119120
binder.setBean(item);
120121
signal.set("foo");
121122

122-
assertEquals("Alice", binding.value());
123+
assertEquals("Alice", binding.valueSignal().peek());
123124

124125
UI.getCurrent().add(field);
125-
assertEquals("foo", binding.value());
126+
assertEquals("foo", binding.valueSignal().peek());
126127

127128
signal.set("bar");
128-
assertEquals("bar", binding.value());
129+
assertEquals("bar", binding.valueSignal().peek());
129130
}
130131

131132
// verifies that bindValue throws NPE for null signal
@@ -227,9 +228,11 @@ public void bindingValue_crossFieldValidation_withMixedFields() {
227228
.withConverter(new StringToIntegerConverter(
228229
"Value must be an integer"))
229230
.withValidator(
230-
value -> value > 0 && !lastNameBinding.value().isEmpty()
231+
value -> value > 0
232+
&& !lastNameBinding.valueSignal().get()
233+
.isEmpty()
231234
&& !firstNameSignal.get().isEmpty()
232-
&& !emailBinding.value().isEmpty(),
235+
&& !emailBinding.valueSignal().get().isEmpty(),
233236
ageValidationError)
234237
.bind("age");
235238
binder.setBean(item);
@@ -658,9 +661,8 @@ public void unbind_removesSignalRegistration() {
658661
var firstNameBinding = binder.forField(firstNameField)
659662
.bind("firstName");
660663
binder.forField(lastNameField)
661-
.withValidator(
662-
value -> !value.isEmpty()
663-
&& !firstNameBinding.value().isEmpty(),
664+
.withValidator(value -> !value.isEmpty()
665+
&& !firstNameBinding.valueSignal().get().isEmpty(),
664666
"Both names required")
665667
.bind("lastName");
666668

@@ -815,7 +817,7 @@ public void bindingValue_converterNotTracking() {
815817
public Result<Integer> convertToModel(String value,
816818
ValueContext context) {
817819
// this should not start tracking
818-
lastNameBinding.value();
820+
lastNameBinding.valueSignal().get();
819821
converterCalls.incrementAndGet();
820822
return super.convertToModel(value, context);
821823
}
@@ -899,6 +901,37 @@ public void getValidationStatus_readBean_initialStatus() {
899901
testInitialStatusChangeRunEffects(item -> binder.readBean(item));
900902
}
901903

904+
@Test
905+
public void bindingValue_multipleValueSignalCalls_revalidateTriggeredForEach() {
906+
item.setFirstName("Alice");
907+
item.setLastName("Smith");
908+
909+
AtomicInteger validatorCalls = new AtomicInteger(0);
910+
911+
UI.getCurrent().add(firstNameField, lastNameField);
912+
var lastNameBinding = binder.forField(lastNameField).bind("lastName");
913+
binder.forField(firstNameField).withValidator((String value) -> {
914+
validatorCalls.incrementAndGet();
915+
return !value.isEmpty()
916+
// multiple calls to same valueSignal()
917+
&& !lastNameBinding.valueSignal().get().isEmpty()
918+
&& !lastNameBinding.valueSignal().get().isEmpty();
919+
}, "First and last name are required").bind("firstName");
920+
921+
binder.setBean(item);
922+
923+
assertEquals(2, validatorCalls.get());
924+
925+
assertTrue(binder.isValid());
926+
927+
assertEquals(3, validatorCalls.get());
928+
929+
lastNameField.setValue("");
930+
// value change runs revalidation for both Signal.get() calls in the
931+
// validator
932+
assertEquals(5, validatorCalls.get());
933+
}
934+
902935
private void testInitialStatusChangeRunEffects(
903936
Consumer<Person> binderSetup) {
904937
item.setFirstName("");

0 commit comments

Comments
 (0)