Class: Forms::Field
- Inherits:
-
Object
- Object
- Forms::Field
- Defined in:
- lib/forms/field.rb
Direct Known Subclasses
Constant Summary collapse
- TOKEN_JOINED_DATA_KEYS =
Data keys whose values are Stimulus token lists — merged by joining, not replacing, so validation controllers and live triggers coexist.
%i[controller action].freeze
Instance Attribute Summary collapse
-
#errors ⇒ Object
readonly
Returns the value of attribute errors.
-
#model ⇒ Object
readonly
Returns the value of attribute model.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#scope ⇒ Object
readonly
Returns the value of attribute scope.
Instance Method Summary collapse
-
#apply_validations(options) ⇒ Object
Merge a per-call
validate:override into the field's option hash. - #checkbox(*modifiers) ⇒ Object (also: #check_box)
-
#checkbox_group(collection, value: :id, label: nil, item_label: nil) ⇒ Object
A model-bound checkbox group over a collection.
-
#choices_select(choices = nil, *modifiers, **options) ⇒ Object
Enhanced select: choices.js-backed when searchable, native otherwise.
- #control(label: nil, hint: nil, required: false) ⇒ Object
- #field_id ⇒ Object
-
#field_label ⇒ Object
Humanized label text: the model's human_attribute_name when available.
- #field_name ⇒ Object
-
#field_value ⇒ Object
Ransack-safe getter: dispatches through method_missing consumers (e.g. Ransack::Search predicate getters) but only swallows the direct dispatch miss.
- #file(*modifiers) ⇒ Object
- #hidden ⇒ Object
-
#initialize(name:, model:, scope:, errors:, form:, error_name: nil) ⇒ Field
constructor
error_name: where errors live when it differs from the input's name — an inferred belongs_to select is named :country_id but Rails attaches its errors to :country.
-
#input(*modifiers, type: :text) ⇒ Object
--- leaf component builders (return component instances to render) --- Component classes resolve through the form's theme (see PhlexForms::Theme), so the same field renders daisy or plain.
- #invalid? ⇒ Boolean
- #label(text = nil, *modifiers, &block) ⇒ Object
- #radio(value, *modifiers, **options) ⇒ Object (also: #radio_button)
-
#required? ⇒ Boolean
Inferred from the model's presence validators (ActiveModel).
- #rich_textarea(*modifiers) ⇒ Object (also: #rich_text_area)
- #select(choices = nil, **options) ⇒ Object
-
#tag_field(*modifiers, suggestions: []) ⇒ Object
A model-bound tag/chip input (phlex-reactive client-only primitives).
- #textarea(*modifiers) ⇒ Object (also: #text_area)
- #toggle(*modifiers) ⇒ Object
-
#wrapped_input(*modifiers, type: :text) ⇒ Object
daisyui v5 "icon/text inside the field" wrapper.
Constructor Details
#initialize(name:, model:, scope:, errors:, form:, error_name: nil) ⇒ Field
error_name: where errors live when it differs from the input's name — an inferred belongs_to select is named :country_id but Rails attaches its errors to :country.
19 20 21 22 23 24 25 26 |
# File 'lib/forms/field.rb', line 19 def initialize(name:, model:, scope:, errors:, form:, error_name: nil) @name = name @error_name = error_name || name @model = model @scope = scope @errors = errors @form = form end |
Instance Attribute Details
#errors ⇒ Object (readonly)
Returns the value of attribute errors.
14 15 16 |
# File 'lib/forms/field.rb', line 14 def errors @errors end |
#model ⇒ Object (readonly)
Returns the value of attribute model.
14 15 16 |
# File 'lib/forms/field.rb', line 14 def model @model end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
14 15 16 |
# File 'lib/forms/field.rb', line 14 def name @name end |
#scope ⇒ Object (readonly)
Returns the value of attribute scope.
14 15 16 |
# File 'lib/forms/field.rb', line 14 def scope @scope end |
Instance Method Details
#apply_validations(options) ⇒ Object
Merge a per-call validate: override into the field's option hash. Strips
:validate and, when rules apply, merges the Stimulus data into data:.
Falls back to the form-level introspector when :validate is absent.
validate: false → opt this field out
validate: true → the form-level introspector
validate: { length: {…} } → explicit inline rules
214 215 216 217 218 219 220 221 |
# File 'lib/forms/field.rb', line 214 def apply_validations() return consume_validate() if .key?(:validate) data = form_introspector.data_attributes_for(@name) return if data.empty? .merge(data: merge_data([:data], data)) end |
#checkbox(*modifiers) ⇒ Object Also known as: check_box
60 61 62 |
# File 'lib/forms/field.rb', line 60 def checkbox(*modifiers, **) theme[:checkbox].new(*modifiers, checked: field_value, **field_attributes.except(:value), **) end |
#checkbox_group(collection, value: :id, label: nil, item_label: nil) ⇒ Object
A model-bound checkbox group over a collection. Shares one array-valued
field name (scope[name][]) and derives the checked set from the model's
current value, matched by each item's resolved value: (issue #9).
field.checkbox_group(Tag.all, value: :id, label: ->(t) { t.name })
value: is a method name (Symbol) or a proc taking the item -> its submitted
value. The per-item visible text comes from item_label: (Symbol/Proc/String)
if given, else label:; when NEITHER is given each item is labelled by the
first of name/title/label/to_s it responds to (the same LABEL_METHODS chain
Inference uses for association choices) — so a plain
f.field(:tags, as: :checkbox_group, label: "Tags") shows readable item
text without an explicit accessor.
item_label: exists so the f.field path can pass a visible group heading as
label: (consumed by the Control) AND still customize the per-item text
here — the two no longer collide. item_label: is consumed here; it never
leaks to the group div.
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
# File 'lib/forms/field.rb', line 127 def checkbox_group(collection, value: :id, label: nil, item_label: nil, **) text = item_label || label # The model's current value is already the raw values (e.g. record.tag_ids # => [1, 3]), so compare against them directly — don't re-resolve value:. selected = Array(field_value) opts = Array(collection).map do |item| item_value = resolve_item(item, value) { value: item_value, label: text ? resolve_item(item, text) : infer_item_label(item), checked: selected.include?(item_value), id: "#{field_id}_#{item_value}" } end theme[:checkbox_group].new(name: "#{field_name}[]", id: field_id, options: opts, error: invalid?, **) end |
#choices_select(choices = nil, *modifiers, **options) ⇒ Object
Enhanced select: choices.js-backed when searchable, native otherwise.
88 89 90 91 92 93 94 95 96 |
# File 'lib/forms/field.rb', line 88 def choices_select(choices = nil, *modifiers, **) searchable = .delete(:searchable) { false } opts = () if searchable theme[:choices_select].new(*modifiers, choices:, selected: field_value, searchable: true, **opts) else theme[:select].new(*modifiers, choices:, selected: field_value, **opts) end end |
#control(label: nil, hint: nil, required: false) ⇒ Object
148 149 150 |
# File 'lib/forms/field.rb', line 148 def control(label: nil, hint: nil, required: false, **, &) theme[:control].new(label:, hint:, error: , for: field_id, required:, **, &) end |
#field_id ⇒ Object
185 186 187 188 189 190 191 |
# File 'lib/forms/field.rb', line 185 def field_id if @scope "#{@scope.tr('[', '_').delete(']')}_#{@name}" else @name.to_s end end |
#field_label ⇒ Object
Humanized label text: the model's human_attribute_name when available.
155 156 157 158 159 160 161 |
# File 'lib/forms/field.rb', line 155 def field_label if @model.respond_to?(:class) && @model.class.respond_to?(:human_attribute_name) @model.class.human_attribute_name(@name) else @name.to_s.tr("_", " ").capitalize end end |
#field_name ⇒ Object
181 182 183 |
# File 'lib/forms/field.rb', line 181 def field_name @scope ? "#{@scope}[#{@name}]" : @name.to_s end |
#field_value ⇒ Object
Ransack-safe getter: dispatches through method_missing consumers (e.g. Ransack::Search predicate getters) but only swallows the direct dispatch miss.
195 196 197 198 199 200 201 202 203 |
# File 'lib/forms/field.rb', line 195 def field_value return nil unless @model @model.public_send(@name) rescue NoMethodError => e raise unless e.receiver.equal?(@model) && e.name == @name nil end |
#file(*modifiers) ⇒ Object
56 57 58 |
# File 'lib/forms/field.rb', line 56 def file(*modifiers, **) theme[:file].new(*modifiers, **field_attributes.except(:value), **) end |
#hidden ⇒ Object
46 47 48 |
# File 'lib/forms/field.rb', line 46 def hidden(**) theme[:input].new(type: :hidden, **field_attributes, **) end |
#input(*modifiers, type: :text) ⇒ Object
--- leaf component builders (return component instances to render) --- Component classes resolve through the form's theme (see PhlexForms::Theme), so the same field renders daisy or plain.
32 33 34 |
# File 'lib/forms/field.rb', line 32 def input(*modifiers, type: :text, **) theme[:input].new(*modifiers, type:, **field_attributes, **) end |
#invalid? ⇒ Boolean
175 176 177 178 179 |
# File 'lib/forms/field.rb', line 175 def invalid? return false unless @errors @errors.include?(@name) || @errors.include?(@error_name) end |
#label(text = nil, *modifiers, &block) ⇒ Object
144 145 146 |
# File 'lib/forms/field.rb', line 144 def label(text = nil, *modifiers, **, &block) theme[:label].new(*modifiers, text: text || (block ? nil : field_label), for: field_id, **, &block) end |
#radio(value, *modifiers, **options) ⇒ Object Also known as:
69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/forms/field.rb', line 69 def radio(value, *modifiers, **) # field_attributes carries value: field_value (the model's CURRENT value). # Drop it here so it can't clobber this radio's own positional value — # otherwise every radio in the group renders the model's value (issue #13). attrs = field_attributes.except(:value).merge() theme[:radio].new( *modifiers, value:, checked: field_value == value, **attrs.merge(id: "#{field_id}_#{value}") ) end |
#required? ⇒ Boolean
Inferred from the model's presence validators (ActiveModel). False when the model doesn't expose validators.
165 166 167 168 169 170 171 172 173 |
# File 'lib/forms/field.rb', line 165 def required? return false unless @model.respond_to?(:class) && @model.class.respond_to?(:validators_on) @model.class.validators_on(@name).any? do |v| v.is_a?(ActiveModel::Validations::PresenceValidator) && !conditional?(v) end rescue StandardError false end |
#rich_textarea(*modifiers) ⇒ Object Also known as: rich_text_area
41 42 43 |
# File 'lib/forms/field.rb', line 41 def rich_textarea(*modifiers, **) theme[:rich_textarea].new(*modifiers, name: field_name, id: field_id, value: field_value, **) end |
#select(choices = nil, **options) ⇒ Object
83 84 85 |
# File 'lib/forms/field.rb', line 83 def select(choices = nil, **) theme[:select].new(choices:, selected: field_value, **()) end |
#tag_field(*modifiers, suggestions: []) ⇒ Object
A model-bound tag/chip input (phlex-reactive client-only primitives). suggestions: an Array of tags or a Hash of tag => haystack (synonyms the filter matches). Submits one comma-joined param under the field name.
101 102 103 104 105 106 107 |
# File 'lib/forms/field.rb', line 101 def tag_field(*modifiers, suggestions: [], **) theme[:tag_field].new( *modifiers, name: field_name, id: field_id, value: field_value, suggestions:, error: invalid?, ** ) end |
#textarea(*modifiers) ⇒ Object Also known as: text_area
36 37 38 |
# File 'lib/forms/field.rb', line 36 def textarea(*modifiers, **) theme[:textarea].new(*modifiers, **field_attributes, **) end |
#toggle(*modifiers) ⇒ Object
65 66 67 |
# File 'lib/forms/field.rb', line 65 def toggle(*modifiers, **) theme[:toggle].new(*modifiers, checked: field_value, **field_attributes.except(:value), **) end |
#wrapped_input(*modifiers, type: :text) ⇒ Object
daisyui v5 "icon/text inside the field" wrapper. The block renders the leading content (icon, prefix); the bare input is wired to this field.
52 53 54 |
# File 'lib/forms/field.rb', line 52 def wrapped_input(*modifiers, type: :text, **, &) theme[:wrapped_input].new(*modifiers, type:, **field_attributes.except(:error), error: invalid?, **, &) end |