Combobox, tags & change handlers
Three things a generated form needs once it stops being a demo. A dropdown with two hundred countries in it should be typed into, not scrolled. A list of labels should be chips, not a repeater with reorder buttons on every word. And "when the country changes, clear the city" is neither a computed value nor a cascading lookup — it writes something the user is still free to change afterwards.
View source
BormBuilder.For<TalentProfile>()
// --- Combobox -----------------------------------------------------------------------------
// Built on the browser's own <input list> + <datalist>, so filtering, keyboard navigation,
// touch behaviour and screen-reader support are the platform's rather than a thousand lines of
// hand-rolled ARIA. What the library adds is the one thing a bare datalist cannot do: map the
// label the user reads onto the value the model stores.
.Field(x => x.Country, f => f
.Options(("fr", "France"), ("gb", "United Kingdom"), /* … 200 more … */)
.AsCombobox() // closed: an answer on no list is reported
.Clearable()
.Required())
// A combobox that is a shortcut rather than a constraint. The list proposes; anything is accepted.
.Field(x => x.City, f => f
.AsCombobox(allowCustom: true)
.OptionsFrom( // and it cascades, exactly as a <select> does
load: async ctx => await directory.GetCitiesAsync(ctx.Value("Country") as string),
value: c => c, label: c => c,
dependsOn: nameof(TalentProfile.Country)))
// --- Change handler -----------------------------------------------------------------------
// Neither Computed (which owns its field's value outright) nor a cascading dependency (which
// reloads choices) can express "write something the user may then edit". TanStack Form calls
// these listeners; Formly calls them hooks. Paths resolve relative to the owning object, so the
// same handler on a repeater row means that row.
.Field(x => x.Plan, f => f
.AsRadio()
.Options(("solo", "Solo"), ("team", "Team"), ("enterprise", "Enterprise"))
.OnChange(ctx => ctx.SetSibling("Seats", ctx.Value switch
{
"solo" => 1,
"team" => 5,
_ => 25
})))
// --- Tags ---------------------------------------------------------------------------------
// The same List<string> an ArrayOf(..., Text) holds. The difference is the affordance: a
// repeater gives every entry a row with add, remove, duplicate and reorder buttons, which is
// right for an invoice line and wrong for the word "urgent". Enter or a comma commits;
// backspace on an empty box takes the last one back.
.Field(x => x.Skills, f => f
.AsTags(max: 8)
.Help("Enter or comma to add. Backspace on an empty box removes the last one."))
.Build();