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.
- #Hidden(name) ⇒ Object
-
#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
127 128 129 130 |
# File 'lib/phlex_forms/builder.rb', line 127 def Checkbox(name, *modifiers, **) fo = field_object(name) render fo.checkbox(*modifiers, **fo.apply_validations()) end |
#Control(name) ⇒ Object
154 155 156 |
# File 'lib/phlex_forms/builder.rb', line 154 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 |
# 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) render fo.control(label: label_text, hint:, required: req) do render_field_input(fo, inferred.name, inferred.as, modifiers, choices:, required: req, **) end end |
#FileInput(name, *modifiers, **options) ⇒ Object
141 142 143 144 |
# File 'lib/phlex_forms/builder.rb', line 141 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 }
101 102 103 |
# File 'lib/phlex_forms/builder.rb', line 101 def group(legend: nil, **, &) render theme[:group].new(legend:, **), & end |
#Hidden(name) ⇒ Object
146 147 148 |
# File 'lib/phlex_forms/builder.rb', line 146 def Hidden(name, **) render field_object(name).hidden(**) end |
#Input(name, *modifiers, **options) ⇒ Object
Escape-hatch component API (stable signatures)
109 110 111 112 113 114 115 |
# File 'lib/phlex_forms/builder.rb', line 109 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
150 151 152 |
# File 'lib/phlex_forms/builder.rb', line 150 def Label(name, text = nil, *modifiers, **, &) render field_object(name).label(text, *modifiers, **, &) end |
#Radio(name, value, *modifiers) ⇒ Object
132 133 134 |
# File 'lib/phlex_forms/builder.rb', line 132 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 }
95 96 97 |
# File 'lib/phlex_forms/builder.rb', line 95 def row(columns: 2, **, &) render theme[:row].new(columns:, **), & end |
#Select(name, *modifiers, choices: nil, **options) ⇒ Object
117 118 119 120 |
# File 'lib/phlex_forms/builder.rb', line 117 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
122 123 124 125 |
# File 'lib/phlex_forms/builder.rb', line 122 def Textarea(name, *modifiers, **) fo = field_object(name) render fo.textarea(*modifiers, **fo.apply_validations()) end |
#Toggle(name, *modifiers, **options) ⇒ Object
136 137 138 139 |
# File 'lib/phlex_forms/builder.rb', line 136 def Toggle(name, *modifiers, **) fo = field_object(name) render fo.toggle(*modifiers, **fo.apply_validations()) end |