Forms & Input Elements
FlightPlan ships eleven form-related classes covering everything from a simple text box to multi-mode radio groups and read-back-aware checkbox groups. Most render unstyled by default — apps add classes (or use OpsO suite styles) for visual polish; a few (ComboBox, CheckBoxGroup) pull baseline styling from CoreStyles so they work anywhere. The demos below show the raw HTML output so you can see what each control produces.
The AutoSubmit Pattern (read this first)
FlightPlan uses a single hidden field — __EVENTTARGET — to track which control caused a form submit. Form renders this field automatically on POST forms. Buttons set its value via onclick; Inputs, DropDownLists, DatePickers, and RadioButtonGroups can opt in via WithAutoSubmit() to fire the form submit when their value changes.
GET forms do not get the field. GetPostBackSource() reads __EVENTTARGET out of the POST body, so a GET form could never read it back — it would only serialize into the query string as visible noise on every submit. The controls that write to it are all null-guarded, so AutoSubmit still submits normally on a GET form; you just don't learn which control triggered it.
In the page's HandleAction (or the page's POST handler), check __EVENTTARGET to know what to do. This is the framework's substitute for ASP.NET-style server-side controls — covered in detail on the GET vs POST and HandleAction guide pages.
// Inside a POST Form, this hidden input is rendered automatically:
// <input type="hidden" id="__EVENTTARGET" name="__EVENTTARGET" value="">
// A button sets it on click:
// onclick="var t=document.getElementById('__EVENTTARGET'); if (t) t.value='btnSave';"
// A WithAutoSubmit() input sets it on change AND submits the form:
// onchange="var t=document.getElementById('__EVENTTARGET'); if (t) t.value='filterStatus';
// this.form.submit();"
// The 'if (t)' guard is what lets these controls work on a GET form,
// where the hidden field is deliberately absent."
Form
Wraps form controls and submits to a server route. Defaults to POST. Always include a Form around interactive controls — without it, AutoSubmit can't find the form to submit and __EVENTTARGET won't be in the post body.
var form = content.Add(new Form("/MyApp/SaveSettings"));
form.Add(new Input(InputType.Text, "username").WithLabel("Username"));
form.Add(new Button(ButtonType.Submit, "Save").WithName("btnSave"));
Input: Text-Like Types
One Input class handles eight HTML input types (Text, Hidden, Number, Email, Password, Checkbox, Radio, Date) — pick the right one via the InputType enum in the constructor. Common modifiers: WithValue, WithPlaceholder, WithLabel, WithRequired, WithMaxLength.
form.Add(new Input(InputType.Text, "name")
.WithLabel("Full Name")
.WithPlaceholder("Ada Lovelace")
.WithMaxLength(50)
.WithRequired());
form.Add(new Input(InputType.Email, "email")
.WithLabel("Email"));
form.Add(new Input(InputType.Number, "age")
.WithLabel("Age")
.WithValue("30"));
form.Add(new Input(InputType.Password, "pwd")
.WithLabel("Password"));
Input: Checkbox
For checkboxes, the label renders AFTER the input (instead of before, like text inputs). Use WithChecked to set the initial state and WithAutoSubmit to fire a form submit when toggled.
form.Add(new Input(InputType.Checkbox, "active")
.WithLabel(" Account is active")
.WithChecked());
form.Add(new Input(InputType.Checkbox, "newsletter")
.WithLabel(" Subscribe to newsletter"));
DatePicker
HTML5 date picker — equivalent to Input(InputType.Date) but with strongly-typed DateTime values and min/max constraints. Pair with WithAutoSubmit on filter dashboards so changing the date refreshes the page.
form.Add(new DatePicker("startDate", "Start Date ")
.WithValue(DateTime.Today.AddDays(-30))
.WithMin(DateTime.Today.AddYears(-1))
.WithMax(DateTime.Today));
form.Add(new DatePicker("endDate", "End Date ")
.WithValue(DateTime.Today)
.WithAutoSubmit());
TextArea
Multi-line text input. Configure with WithRows and WithCols, set initial content with WithValue.
form.Add(new TextArea("notes")
.WithRows(4)
.WithCols(50)
.WithValue("Existing notes go here...")
.WithRequired());
DropDownList
A labeled <select> with three useful conveniences: WithPlaceholder for a disabled "choose one" option, WithShowAllOption for a blank-value "All" entry (great for filter dashboards), and WithAutoSubmit to fire on change. Items can be added one at a time, in bulk, or as (value, text) tuples.
form.Add(new DropDownList("status", "Status ")
.WithShowAllOption(true, "All Statuses")
.WithSelectedValue("active")
.AddItems("active", "pending", "closed")
.WithAutoSubmit());
form.Add(new DropDownList("priority", "Priority ")
.WithPlaceholder("— Select priority —")
.AddItem("1", "High")
.AddItem("2", "Medium")
.AddItem("3", "Low"));
ComboBox
A text input paired with a dropdown trigger (▾ glyph on the right edge). Clicking the glyph opens the FULL list of options — unlike a native <input list> + <datalist>, the dropdown does NOT filter against the input's current value, so users see every option every time. Clicking an option replaces the input contents. Free-text entry is allowed; the dropdown is a convenience list, not a constraint.
Use ComboBox when users will either pick from a short list of known values OR type in something new — e.g., an Owner field where most owners are recurring but a new one can be added without ceremony.
Backed by a small custom JS handler emitted once per page (via HtmlBuilder.ClaimOnce). Items use the same AddItem/AddItems family as DropDownList. Other fluent methods: WithValue, WithPlaceholder, WithRequired, WithReadOnly, WithDisabled.
form.Add(new ComboBox("owner", _record.Owners)
.AddItems("alice", "bob", "charlie")
.WithRequired());
RadioButtonGroup
A group of radio buttons that can render in three modes:
- RadioRenderMode.Radio — standard radios with labels (works in any app)
- RadioRenderMode.Tabs — tab-style buttons (requires .tab and .tab-group CSS — provided by OpsO suite styles)
- RadioRenderMode.Buttons — connected button group (requires .btn-radio and .button-group CSS — provided by OpsO suite styles)
Layout (RadioLayout.Horizontal vs Vertical) only affects the standard Radio mode.
form.Add(new RadioButtonGroup("contactMethod", "Contact via ")
.AddItems("Email", "Phone", "Mail")
.WithSelectedValue("Email")
.WithRenderMode(RadioRenderMode.Radio)
.WithLayout(RadioLayout.Horizontal));
In an OpsO app, swap WithRenderMode to Tabs or Buttons for the same data and the suite's styles take over to produce a tab strip or a connected button group. The demo above shows standard radios because the Guide app doesn't load OpsO styles.
CheckBoxGroup
A labeled group of checkboxes built from a value list (a DB query or a const array), rendered in a bordered box with an optional master "All" toggle. Items use the same AddItem / AddItems family as DropDownList, including (value, text) tuples. Styling comes from CoreStyles, so it works in any app; the All toggle's check/uncheck-all behavior and "re-sync the All box when members change" logic are handled by a small JS handler emitted once per page (via HtmlBuilder.ClaimOnce).
Reading the selection back (important)
HTML checkboxes in a group normally share one name and submit multiple values — but FormData is a Dictionary<string,string>, so duplicate names collapse and all but one value is lost. CheckBoxGroup sidesteps this: each member gets a UNIQUE field name ("BU_0", "BU_1", …) with the real value in the value attribute. You read the checked set back by the GROUP name with the BasePage helper GetCheckBoxGroupValues. Unchecked boxes submit nothing, so they're simply absent from the result; the All toggle has no name and never appears in read-back.
// Build a group. The All toggle is OPT-IN — call WithShowAllOption() to add it
// (off by default, since an All toggle isn't always appropriate).
form.Add(new CheckBoxGroup("BU", "BU:")
.AddItems("Oncology", "Vaccines", "Specialty", "None")
.WithShowAllOption() // master check/uncheck-all toggle
.WithAllSelected()); // default everything on (call AFTER AddItems)
// Auto-postback variant for filter dashboards — any change submits the form:
form.Add(new CheckBoxGroup("status", "Status:")
.AddItems(statuses)
.WithAutoSubmit());
// Read the checked values back by the GROUP name (not the per-box names):
List<string> picked = GetCheckBoxGroupValues("BU");
Selection setters follow a predictable pairing: WithSelectedValues(list) SETS the selection (replaces), WithSelectedValue(one) ADDS to it, and WithAllSelected() checks every item. On a postback you typically rebuild each group from GetCheckBoxGroupValues so the user's choices survive the round-trip.
Persisting a selection across visits
Two BasePage helpers store a multi-select in a single cookie so a page reopens with the user's last choices. SetCookieList url-escapes each value and joins with '|' (a delimiter that can't collide with an escaped value); GetCookieList returns the decoded list, or null when the cookie is absent. They're general-purpose — any List<string> works, not just checkbox groups.
// Save on every postback (the form always carries the current checkboxes):
if (IsPostBack())
SetCookieList("RepReg_BU", GetCheckBoxGroupValues("BU"), 30); // 30-day expiry
// Restore when building the group; null (never saved) falls back to a default:
List<string>? saved = GetCookieList("RepReg_BU");
group.WithSelectedValues(saved ?? defaultBUs);
Label
Most controls accept WithLabel directly, so you rarely instantiate Label by hand. Use it standalone when you need a label for an element the framework doesn't auto-label (like a TextArea, or a hand-built control), or when you want a label with mixed-content children.
form.Add(new Label("Comments ", forId: "comments"));
form.Add(new Br());
form.Add(new TextArea("comments").WithRows(3));
Button
Renders a <button>. ButtonType picks Button (no default action), Submit (submits the parent form), or Reset (clears the form). WithName sets the value of __EVENTTARGET on click — that's how your HandleAction code knows which button was pressed.
WithPleaseWait shows a full-screen overlay (PleaseWaitOverlay) when the button is clicked, useful for long-running submits.
form.Add(new Button(ButtonType.Submit, "Save")
.WithName("btnSave"));
form.Add(new Button(ButtonType.Submit, "Generate Report")
.WithName("btnReport")
.WithPleaseWait("Building report — this can take a minute..."));
form.Add(new Button(ButtonType.Reset, "Reset"));
LinkButton
A navigation link that looks like a button — renders as <a href> with whatever button class you give it. Use this for navigating to another page (where Button submits a form). WithPleaseWait works the same way as on Button.
content.Add(new LinkButton("Edit User", "/Users/Edit/42")
.WithClass("btn-primary"));
content.Add(new LinkButton("Download Report", "/Reports/Quarterly.pdf")
.WithClass("btn-secondary")
.WithPleaseWait("Preparing your download..."));
Pass WithClass("btn-primary") (or btn-secondary, btn-danger) to get suite-styled buttons. OpsO apps get these classes via OpsOStyles; Guide defines its own minimal versions for these demos.