Basics
Forms and Form Helpers
Wheels ships a form helper for every native HTML input type. Most of them come in two flavours — an object-bound version that reads from and writes to a model, and a tag-style version that doesn’t. This page walks every helper, shows the canonical form pattern, and documents the new data-auto-id attribute that makes browser tests stable across naming conventions.
You’ll learn:
- How
startFormTag/endFormTagwire a form to a named route and include CSRF automatically - The object-bound pattern — one call per field, values and errors wired to the model
- Every object-bound helper Wheels ships, including the HTML5 inputs
- When to reach for the tag-style variants instead
- What
data-auto-idis, why it was added, and how to opt out
startFormTag and endFormTag
Section titled “startFormTag and endFormTag”Every Wheels form opens with startFormTag and closes with endFormTag. The helper generates the <form> element, resolves the URL from a named route, and automatically includes a CSRF token as a hidden field.
<cfoutput> ##startFormTag(route="posts", method="post")## <input type="text" name="post[title]"> <button type="submit">Create</button> ##endFormTag()##</cfoutput>Key behaviours:
- Generates
<form action="..." method="post">with CSRF token auto-included. route="..."uses named routes — see Routing.method="put"/method="delete"— Wheels emits a hidden_methodfield that gets translated server-side. Browsers only send GET and POST natively, so Wheels (like Rails, Laravel, and friends) uses the hidden field convention to expose the full set of REST verbs.
Object-bound helpers — the core pattern
Section titled “Object-bound helpers — the core pattern”Most Wheels form helpers take an objectName and a property. The helper looks up the model instance in the view’s scope, reads the property off it to fill in the value, and generates the right name, id, and data-auto-id so the controller can round-trip the value back into a model on submit.
A field call like textField(objectName="post", property="title") generates:
name="post[title]"— the controller sees this asparams.post.titleid="post-title"— stable DOM id for labels and CSSdata-auto-id="post_title"— underscore variant for test selectors (see below)
The canonical form pattern wraps inputs in labels, calls errorMessagesFor at the top, and ends with a submit button:
<cfoutput> ##startFormTag(route="posts")## ##errorMessagesFor("post")## <label> Title ##textField(objectName="post", property="title")## </label> <label> Body ##textArea(objectName="post", property="body", rows=6)## </label> <label> Status ##select(objectName="post", property="status", options="draft,published,archived")## </label> <button type="submit">Save</button> ##endFormTag()##</cfoutput>The controller side is then just model("Post").new(params.post) — the post[...] field names deserialize straight into a nested struct. See Controllers and Actions for the params handling.
Every object-bound helper
Section titled “Every object-bound helper”| Helper | Renders | Common options |
|---|---|---|
textField | <input type="text"> | type (overrideable), placeholder |
passwordField | <input type="password"> | placeholder |
emailField | <input type="email"> | placeholder, required |
urlField | <input type="url"> | placeholder |
numberField | <input type="number"> | min, max, step |
telField | <input type="tel"> | placeholder, pattern |
searchField | <input type="search"> | placeholder |
colorField | <input type="color"> | — (value is a hex colour) |
rangeField | <input type="range"> | min, max, step |
dateField | <input type="date"> | min, max (both as YYYY-MM-DD) |
hiddenField | <input type="hidden"> | — |
fileField | <input type="file"> | accept, multiple |
textArea | <textarea> | rows, cols |
select | <select> | options, includeBlank, multiple |
checkBox | <input type="checkbox"> + hidden companion for the unchecked state | checkedValue, uncheckedValue |
radioButton | <input type="radio"> | tagValue (the value this button submits) |
Every helper also accepts the standard Wheels form options — label, labelPlacement, prepend, append, prependToLabel, appendToLabel, errorElement, errorClass, encode — plus any raw HTML attribute (class, placeholder, required, autofocus, and so on) which is passed through to the rendered tag.
See vendor/wheels/view/formsobject.cfc for the full argument surface.
Tag-style helpers
Section titled “Tag-style helpers”When you’re not bound to a model — a login form, a search bar, a one-off contact form — the object-bound helpers are the wrong tool. Every object-bound helper has a Tag suffix variant that takes name= and value= directly:
textFieldTag,passwordFieldTag,emailFieldTag,urlFieldTag,numberFieldTag,telFieldTag,searchFieldTag,colorFieldTag,rangeFieldTag,dateFieldTaghiddenFieldTag,fileFieldTag,textAreaTag,selectTag,checkBoxTag,radioButtonTag
<cfoutput> ##startFormTag(route="search", method="get")## ##textFieldTag(name="q", value=params.q ?: "", placeholder="Search...")## <button type="submit">Go</button> ##endFormTag()##</cfoutput>Tag-style helpers never emit data-auto-id — their id is already a plain name with no dash/underscore ambiguity, so the companion attribute would just be noise.
data-auto-id — stable test selectors (new in 4.0)
Section titled “data-auto-id — stable test selectors (new in 4.0)”Wheels object-bound helpers emit id="post-title" with a dash between the object name and the property. Every other major web framework — Rails, Django, Laravel — uses an underscore: id="post_title". Developers coming from those frameworks write browser tests targeting #post_title, then spend hours chasing a silent selector miss.
In 4.0, every object-bound helper also emits data-auto-id="post_title". Tests target either the dashed id (stable, never changing) or the underscore data-auto-id attribute:
<input type="text" name="post[title]" id="post-title" data-auto-id="post_title">Both selectors work. #post-title remains the canonical DOM id — nothing about it has changed. [data-auto-id="post_title"] is the new escape hatch for tests written against the framework-agnostic underscore convention.
Opt out globally in config/settings.cfm with set(formHelperDataAutoId=false). It’s on by default.
Error messages
Section titled “Error messages”errorMessagesFor(objectName) renders a summary block at the top of the form listing every validation error on the object. Pair it with per-field inline errors if you want — errorMessageOn(objectName=..., property=...) renders a single error for a single property, or use your own CSS to style fields based on the presence of validation errors.
<cfoutput> ##startFormTag(route="posts")## ##errorMessagesFor("post")## ##textField(objectName="post", property="title")## ##textArea(objectName="post", property="body")## <button>Save</button> ##endFormTag()##</cfoutput>The summary block only renders when the object has errors — on a new-form render before any submit, nothing appears. See Validation and Error Display for the validations that populate these errors.
Labels
Section titled “Labels”Wheels does not ship a standalone label() helper. Labels are a first-class argument on every object-bound form helper via label= and labelPlacement=:
textField(objectName="post", property="title", label="Post title")— wraps the input in a<label for="post-title">Post title</label>withlabelPlacementcontrolling whether the text comesaround(default),before, orafterthe input.label=falsesuppresses the auto-label — use this when you want to write the<label>tag by hand (for icons, extra classes, or explicitforattributes).
The third and most common pattern is in the canonical example above: skip the label argument entirely and wrap the helper call inside a raw <label> element in your markup. The browser associates the label with the input via containment, no for="..." attribute needed.
Form submission with Turbo Frames
Section titled “Form submission with Turbo Frames”Wrap a form (or the block that contains it) in a <turbo-frame id="...">. When the user submits, Turbo intercepts the response and replaces just that frame’s contents — no full-page reload, no scroll reset. The controller action is unchanged; only the response template needs to render a matching frame.
See tutorial Part 4: Validations + Turbo Frames for a worked example, including the server-side render.