Escape hatches
When field's one-call convenience isn't enough: bare components, wrapped inputs, and the custom-widget recipe.
The component API#
Every control field composes is also callable directly, with stable signatures — a bare input, no label/control wrapper, still fully model-bound:
f.Input(:name, :primary, :lg) # bare input, variants stacked
f.Select(:role, choices: roles) # native <select>
f.Textarea(:bio, :ghost)
f.Checkbox(:terms)
f.Toggle(:notify)
f.Radio(:size, "large")
f.FileInput(:avatar)
f.Hidden(:token)
f.Label(:email)
f.Control(:email, label: "Email") { f.Input(:email) }PascalCase marks the component layer; lowercase field/row/group/ submit are the composed verbs. Both apply the model's value, error state, and client-validation data the same way.
The custom-widget recipe#
A bespoke widget — date picker, tag field, remote select — wraps in Control and binds through the three public helpers. This is the supported path, not a fork reason:
f.Control(:starts_at, label: "Starts") do
render MyDatePicker.new(
name: f.field_name(:starts_at),
id: f.field_id(:starts_at),
value: f.field_value(:starts_at)
)
endThe control contributes the label, error/hint slot, and layout; your widget owns the input. Errors on the attribute render exactly like any other field's.
Icons inside the field#
wrapped_input renders the daisyUI v5 <label class="input"> pattern — leading content (an icon, a prefix), the bare input, optional trailing content:
f.field_object(:search).wrapped_input(:primary) do
LucideIcon("search", class: "opacity-50")
endIcons default to a bundled inline SVG so the gem is self-contained; see Configuration for the glyphs renderer.
Specialised controls#
f.Select(:tags, choices:, searchable: true, multiple: true)— a choices.js-backed searchable/multi select (needs thechoices.jspeer).f.rich_textarea(:body)— the rich-text editor binding (ActionText-compatible value handling).f.time_zone_select(:time_zone)— grouped time zones with a sensible selected default.