Affixes, counters & live updates
The details that separate a generated form from a designed one. A price field wearing a currency
symbol; a summary that counts down to its limit as you type; a section heading that belongs to
the schema rather than to the page around it. The preview on the right is driven by
UpdateOnInput() — the model is written on every keystroke, so nothing here is wired
up by hand.
Updates as you type, straight from the model.
Untitled listing
No summary yet.
○ Draft
View source
var schema = BormBuilder.For<ListingDraft>()
.Title("New listing")
// A section heading that lives in the schema, so the page does not have to interleave its own
// markup with the generated fields.
.Static("basics", "The basics", "What someone sees first in the search results.")
.Field(x => x.Title, f => f
.Required()
.MaxLength(60)
.CharacterCount() // "12 / 60", counting as you type
.UpdateOnInput()) // write on every keystroke, not on blur
.Field(x => x.Summary, f => f
.AsTextArea(3)
.MaxLength(180)
.CharacterCount()
.UpdateOnInput(debounceMilliseconds: 150)) // one write per pause, not per character
.Static("details", "Details", configure: f => f.Attr("divider", true))
.Field(x => x.Price, f => f
.Prefix("€").Suffix("/ month") // decoration inside the field's border
.Range(0, 100_000).Step(50)
.UpdateOnInput())
.Field(x => x.FloorArea, f => f
.Suffix("m²")
.Range(1, 5_000)
.UpdateOnInput())
// Suggests without restricting: the field still accepts anything typed into it.
.Field(x => x.City, f => f
.Required()
.Suggest("Amsterdam", "Berlin", "Lisbon", "Paris", "Warsaw")
.Clearable() // one-click "empty this", shown only when non-empty
.UpdateOnInput()
.InputAttr("data-testid", "city")) // arbitrary HTML attributes, splatted onto the input
.Field(x => x.Passcode, f => f
.AsPassword()
.Revealable() // opt-in "show the characters" toggle
.MinLength(6))
.Field(x => x.Published, f => f.AsSwitch()) // a checkbox that announces itself as a switch
.Build();