Dependent fields
A rule that reads two values lives on one of them, so only one of the two changes ever runs it.
Get the confirmation wrong, then fix the first box to agree with what you typed — the
complaint underneath the confirmation is now wrong, and without RevalidateOn it
stays there until you go back and touch a field that was already right. React Hook Form spells
this deps; TanStack Form spells it onChangeListenTo.
Try each of these:
- Type an email, then a different one below it, then correct the first.
- Set a return date before the departure date, then move the departure back.
- Book fewer rooms than travellers, then reduce the travellers.
- Tick the hire car, give it some days, then untick it — the estimate drops the car again, because clearing a hidden field is a change like any other.
View source
var schema = BormBuilder.For<TripRequest>()
.Title("Request a trip")
// [Compare] on the model already wires the rule *and* its revalidation, exactly as the
// builder's MatchesField(...) does. Nothing extra to declare.
.Field(x => x.Email)
.Field(x => x.ConfirmEmail)
// A hand-written cross-field rule has to say what it reads. Without RevalidateOn, moving the
// departure date later leaves "must be on or after" sitting under a return date that is now fine.
.Field(x => x.Start)
.Field(x => x.End, f => f
.RevalidateOn(nameof(TripRequest.Start))
.Must(ctx => ctx.Value is not DateOnly end
|| ctx.Data.GetValue(nameof(TripRequest.Start)) is not DateOnly start
|| end >= start,
"The return date must be on or after the departure date."))
.Field(x => x.Travellers, f => f.Range(1, 12))
.Field(x => x.Rooms, f => f
.Range(1, 12)
.RevalidateOn(nameof(TripRequest.Travellers))
.Must(ctx => ctx.Value is not int rooms
|| ctx.Data.GetValue(nameof(TripRequest.Travellers)) is not int people
|| rooms * 4 >= people,
"That is not enough rooms for this many travellers."))
// Clearing a hidden field runs the same sweeps a typed-in value does, so the estimate below
// drops the hire car the moment the checkbox is unticked.
.Field(x => x.HireCar, f => f.AsSwitch())
.Field(x => x.CarDays, f => f
.Range(1, 60)
.VisibleWhen(nameof(TripRequest.HireCar), BormConditionOperator.IsTrue)
.RequiredWhen(nameof(TripRequest.HireCar), BormConditionOperator.IsTrue)
.ClearOnHide())
.Field(x => x.Estimate, f => f
.ReadOnly()
.Prefix("€")
.Computed(ctx => Estimate(ctx),
nameof(TripRequest.Travellers), nameof(TripRequest.Rooms), nameof(TripRequest.CarDays)))
.Build();