Skip to content

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.

Live form
Booking

Folded to upper case when you leave the field.

Surrounding spaces are trimmed rather than reported.

Today up to six months out — enforced, not just offered to the picker.

Reception is staffed between 14:00 and 22:00.

What the model holds

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) { /* … */ }
An unhandled error has occurred. Reload 🗙