Module: PhlexForms::Builder
- Included in:
- Forms::FieldsForBuilder, Forms::Form
- Defined in:
- lib/phlex_forms/builder.rb
Overview
The shared form-builder surface, included by Forms::Form and Forms::FieldsForBuilder so the two no longer copy-paste the API.
Two layers:
* The Control-first primary verb `field` — renders label + input +
error/hint in one call, inferring the input type and the `required` flag
from the model (structure, column types, validators — see
PhlexForms::Inference) with the attribute-name map as tiebreaker.
* Lower-level escape hatches (`Input`, `Select`, `Textarea`, `Checkbox`,
`Toggle`, `FileInput`, `Hidden`, `Label`, `Control`, `submit`) with the
same signatures they had before extraction, for when a caller needs full
control (custom Stimulus wiring, bespoke layout, ...).
Hosts mix this in and provide: render, model, scope, errors, and a
field(name)-style field_object that returns a Forms::Field for a name.
Constant Summary collapse
- INPUT_TYPE_MODIFIERS =
Positional symbols that name an input type (so
f.Input(:x, :email)works). %i[ text email password tel url search number date time datetime color range file hidden month week ].freeze
- INPUT_TYPE_INFERENCE =
Attribute-name -> input type inference. One canonical map (previously duplicated and divergent across Form and Field).
{ email: :email, password: :password, password_confirmation: :password, current_password: :password, new_password: :password, new_password_confirmation: :password, phone: :tel, telephone: :tel, tel: :tel, mobile: :tel, url: :url, website: :url, homepage: :url, search: :search, query: :search, date: :date, birthday: :date, birthdate: :date, born_on: :date, started_at: :date, ended_at: :date, time: :time, datetime: :datetime, timestamp: :datetime, color: :color, colour: :color }.freeze
Instance Method Summary collapse
- #Checkbox(name, *modifiers, **options) ⇒ Object
- #Control(name) ⇒ Object
-
#field(name, *modifiers, label: nil, hint: nil, as: nil, required: nil, choices: nil, **options) ⇒ Object
Render a complete field: label + control-wrapped input + error/hint.
- #FileInput(name, *modifiers, **options) ⇒ Object
-
#group(legend: nil) ⇒ Object
A fieldset with a legend for sectioning related fields.
-
#group_aria(fo, as, label_text, hint) ⇒ Object
For a checkbox_group field: the stable ids to stamp on the Control's label/hint (control_opts), and a group
aria:hash pointing back at them (group_opts) — passed through to the group div, no special leaf API. - #Hidden(name) ⇒ Object
-
#hidden_field(name) ⇒ Object
Rails FormBuilder-compatible hidden field, for straight migration of
form.hidden_field(:token, value: x)call sites. -
#Input(name, *modifiers, **options) ⇒ Object
------------------------------------------------------------------ Escape-hatch component API (stable signatures) ------------------------------------------------------------------.
- #Label(name, text = nil, *modifiers) ⇒ Object
- #Radio(name, value, *modifiers) ⇒ Object
-
#row(columns: 2) ⇒ Object
Side-by-side fields in a responsive grid (stacked on mobile).
- #Select(name, *modifiers, choices: nil, **options) ⇒ Object
- #Textarea(name, *modifiers, **options) ⇒ Object
- #Toggle(name, *modifiers, **options) ⇒ Object
Instance Method Details
#Checkbox(name, *modifiers, **options) ⇒ Object
155 156 157 158 |
# File 'lib/phlex_forms/builder.rb', line 155 def Checkbox(name, *modifiers, **) fo = field_object(name) render fo.checkbox(*modifiers, **fo.apply_validations()) end |
#Control(name) ⇒ Object
189 190 191 |
# File 'lib/phlex_forms/builder.rb', line 189 def Control(name, **, &) render field_object(name).control(**, &) end |
#field(name, *modifiers, label: nil, hint: nil, as: nil, required: nil, choices: nil, **options) ⇒ Object
Render a complete field: label + control-wrapped input + error/hint.
f.field :email # type=email, label auto, required inferred
f.field :notify # boolean column -> toggle
f.field :role # AR enum -> select, humanized
f.field :country # belongs_to -> select over the association
f.field :bio, as: :textarea, rows: 6 # explicit as: always wins
f.field :name, :primary, label: "Full name", hint: "As on your ID"
Options:
label: label text (defaults to the model's humanized attribute name;
pass false to omit the label)
hint: help text shown when there is no error
as: override the rendered control (:select, :textarea, :toggle,
:checkbox, :file, :hidden, :rich_textarea, and text-like types)
required: force the required flag (otherwise inferred from validations)
choices: choices for :select (implies as: :select when given)
Remaining positional modifiers/keywords pass through to the inner input.
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/phlex_forms/builder.rb', line 67 def field(name, *modifiers, label: nil, hint: nil, as: nil, required: nil, choices: nil, **) modifiers = default_field_variants + modifiers inferred = PhlexForms::Inference.resolve(model:, name:, as:, modifiers:, choices:) # error_name: for a rewritten field (:country -> :country_id) errors still # live on the association name. fo = field_object(inferred.name, error_name: name) req = required.nil? ? (fo.required? || inferred.required == true) : required label_text = label == false ? nil : (label || inferred.label || fo.field_label) = inferred.attributes.merge() if inferred.multiple [:multiple] = true unless .key?(:multiple) [:name] ||= "#{fo.field_name}[]" end = fo.apply_validations() choices ||= materialize_choices(inferred.choices) # A checkbox_group renders div[role="group"], which a plain <label for> can't # name. Give the Control's label/hint stable ids (control_opts) and point the # group at them with a passed-through aria: { labelledby:, describedby: } # (group_opts), reusing the Control's own visible chrome (issue #17). control_opts, group_opts = group_aria(fo, inferred.as, label_text, hint) = .merge(group_opts) render fo.control(label: label_text, hint:, required: req, **control_opts) do render_field_input(fo, inferred.name, inferred.as, modifiers, choices:, required: req, **) end end |
#FileInput(name, *modifiers, **options) ⇒ Object
169 170 171 172 |
# File 'lib/phlex_forms/builder.rb', line 169 def FileInput(name, *modifiers, **) fo = field_object(name) render fo.file(*modifiers, **fo.apply_validations()) end |
#group(legend: nil) ⇒ Object
A fieldset with a legend for sectioning related fields. f.group(legend: "Address") { f.field :street }
129 130 131 |
# File 'lib/phlex_forms/builder.rb', line 129 def group(legend: nil, **, &) render theme[:group].new(legend:, **), & end |
#group_aria(fo, as, label_text, hint) ⇒ Object
For a checkbox_group field: the stable ids to stamp on the Control's
label/hint (control_opts), and a group aria: hash pointing back at them
(group_opts) — passed through to the group div, no special leaf API. Returns
[{}, {}] for every other field type (a label associates via for/id, no aria
needed) and when neither label nor hint is present.
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/phlex_forms/builder.rb', line 101 def group_aria(fo, as, label_text, hint) return [{}, {}] unless as == :checkbox_group control_opts = {} aria = {} if label_text control_opts[:label_id] = "#{fo.field_id}_label" aria[:labelledby] = control_opts[:label_id] end if hint control_opts[:hint_id] = "#{fo.field_id}_hint" aria[:describedby] = control_opts[:hint_id] end [control_opts, aria.empty? ? {} : { aria: }] end |
#Hidden(name) ⇒ Object
174 175 176 |
# File 'lib/phlex_forms/builder.rb', line 174 def Hidden(name, **) render field_object(name).hidden(**) end |
#hidden_field(name) ⇒ Object
Rails FormBuilder-compatible hidden field, for straight migration of
form.hidden_field(:token, value: x) call sites. Same model-bound path as
Hidden; an explicit value: wins over the model's current value.
181 182 183 |
# File 'lib/phlex_forms/builder.rb', line 181 def hidden_field(name, **) render field_object(name).hidden(**) end |
#Input(name, *modifiers, **options) ⇒ Object
Escape-hatch component API (stable signatures)
137 138 139 140 141 142 143 |
# File 'lib/phlex_forms/builder.rb', line 137 def Input(name, *modifiers, **) fo = field_object(name) type = resolve_input_type(name, modifiers) type = :"datetime-local" if type == :datetime remaining = modifiers - INPUT_TYPE_MODIFIERS render fo.input(*remaining, type:, **fo.apply_validations()) end |
#Label(name, text = nil, *modifiers) ⇒ Object
185 186 187 |
# File 'lib/phlex_forms/builder.rb', line 185 def Label(name, text = nil, *modifiers, **, &) render field_object(name).label(text, *modifiers, **, &) end |
#Radio(name, value, *modifiers) ⇒ Object
160 161 162 |
# File 'lib/phlex_forms/builder.rb', line 160 def Radio(name, value, *modifiers, **) render field_object(name).radio(value, *modifiers, **) end |
#row(columns: 2) ⇒ Object
Side-by-side fields in a responsive grid (stacked on mobile). f.row { f.field :first_name; f.field :last_name }
123 124 125 |
# File 'lib/phlex_forms/builder.rb', line 123 def row(columns: 2, **, &) render theme[:row].new(columns:, **), & end |
#Select(name, *modifiers, choices: nil, **options) ⇒ Object
145 146 147 148 |
# File 'lib/phlex_forms/builder.rb', line 145 def Select(name, *modifiers, choices: nil, **) fo = field_object(name) render fo.choices_select(choices, *modifiers, **fo.apply_validations()) end |
#Textarea(name, *modifiers, **options) ⇒ Object
150 151 152 153 |
# File 'lib/phlex_forms/builder.rb', line 150 def Textarea(name, *modifiers, **) fo = field_object(name) render fo.textarea(*modifiers, **fo.apply_validations()) end |
#Toggle(name, *modifiers, **options) ⇒ Object
164 165 166 167 |
# File 'lib/phlex_forms/builder.rb', line 164 def Toggle(name, *modifiers, **) fo = field_object(name) render fo.toggle(*modifiers, **fo.apply_validations()) end |