Locked fields, tidy values & date windows
Three things a form needs once it survives its first submit. ReadOnlyWhen locks an
answer that has been settled without hiding it from a keyboard or a screen reader.
Trim() and Normalize(…) fix the value rather than complaining about it.
A date or time Range(…) is enforced, not merely offered to the picker.
Tick confirmed and the reference and arrival lock — still readable, still copyable, still in the tab order. Type spaces around the email, or a lower-case reference, then leave the field.
{
"Reference": "bk-1024",
"ContactEmail": "",
"Arrival": null,
"CheckIn": null,
"Confirmed": false
}View source
var schema = BormBuilder.For<BookingRequest>()
.Title("Booking")
.Field(x => x.Reference, f => f
// Fix the value, do not complain about it: on blur the reference is folded to upper case
// and its stray spaces are dropped, so the model never stores " ab-12 ".
.Normalize(v => v is string s ? s.Trim().ToUpperInvariant() : v)
// Settled, not inapplicable. DisabledWhen would take this out of the tab order and stop a
// screen reader announcing it at all — for a value the user still needs to read off.
.ReadOnlyWhen(nameof(BookingRequest.Confirmed), BormConditionOperator.IsTrue))
.Field(x => x.ContactEmail, f => f
.AsEmail().Required()
.Trim()) // a rule that rejects " [email protected] " for its spaces helps nobody
.Field(x => x.Arrival, f => f
.As(BormFieldType.Date)
// Rendered as the input's own min/max *and* enforced by the same rule, so a date typed past
// the picker is reported rather than quietly accepted.
.Range(DateTime.Today, DateTime.Today.AddMonths(6))
.ReadOnlyWhen(nameof(BookingRequest.Confirmed), BormConditionOperator.IsTrue))
.Field(x => x.CheckIn, f => f
.As(BormFieldType.Time)
.Range(new TimeOnly(14, 0), new TimeOnly(22, 0)))
.Field(x => x.Confirmed, f => f.AsSwitch())
.Build();
// A "Saved." banner belongs to the save, not to the absence of errors.
if (state.IsSubmitSuccessful) { /* … */ }