Class: Forms::Field

Inherits:
Object
  • Object
show all
Defined in:
lib/forms/field.rb

Direct Known Subclasses

Live::Field

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

Instance Method Summary collapse

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

#errorsObject (readonly)

Returns the value of attribute errors.



14
15
16
# File 'lib/forms/field.rb', line 14

def errors
  @errors
end

#modelObject (readonly)

Returns the value of attribute model.



14
15
16
# File 'lib/forms/field.rb', line 14

def model
  @model
end

#nameObject (readonly)

Returns the value of attribute name.



14
15
16
# File 'lib/forms/field.rb', line 14

def name
  @name
end

#scopeObject (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


164
165
166
167
168
169
170
171
# File 'lib/forms/field.rb', line 164

def apply_validations(options)
  return consume_validate(options) if options.key?(:validate)

  data = form_introspector.data_attributes_for(@name)
  return options if data.empty?

  options.merge(data: merge_data(options[: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

#choices_select(choices = nil, *modifiers, **options) ⇒ Object

Enhanced select: choices.js-backed when searchable, native otherwise.



84
85
86
87
88
89
90
91
92
# File 'lib/forms/field.rb', line 84

def choices_select(choices = nil, *modifiers, **options)
  searchable = options.delete(:searchable) { false }
  opts = select_options(options)
  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



98
99
100
# File 'lib/forms/field.rb', line 98

def control(label: nil, hint: nil, required: false, **, &)
  theme[:control].new(label:, hint:, error: field_error_message, for: field_id, required:, **, &)
end

#field_idObject



135
136
137
138
139
140
141
# File 'lib/forms/field.rb', line 135

def field_id
  if @scope
    "#{@scope.tr('[', '_').delete(']')}_#{@name}"
  else
    @name.to_s
  end
end

#field_labelObject

Humanized label text: the model's human_attribute_name when available.



105
106
107
108
109
110
111
# File 'lib/forms/field.rb', line 105

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_nameObject



131
132
133
# File 'lib/forms/field.rb', line 131

def field_name
  @scope ? "#{@scope}[#{@name}]" : @name.to_s
end

#field_valueObject

Ransack-safe getter: dispatches through method_missing consumers (e.g. Ransack::Search predicate getters) but only swallows the direct dispatch miss.



145
146
147
148
149
150
151
152
153
# File 'lib/forms/field.rb', line 145

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

#hiddenObject



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

Returns:

  • (Boolean)


125
126
127
128
129
# File 'lib/forms/field.rb', line 125

def invalid?
  return false unless @errors

  @errors.include?(@name) || @errors.include?(@error_name)
end

#label(text = nil, *modifiers, &block) ⇒ Object



94
95
96
# File 'lib/forms/field.rb', line 94

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: radio_button



69
70
71
72
73
74
75
76
# File 'lib/forms/field.rb', line 69

def radio(value, *modifiers, **options)
  theme[:radio].new(
    *modifiers,
    value:,
    checked: field_value == value,
    **field_attributes.merge(options).merge(id: "#{field_id}_#{value}")
  )
end

#required?Boolean

Inferred from the model's presence validators (ActiveModel). False when the model doesn't expose validators.

Returns:

  • (Boolean)


115
116
117
118
119
120
121
122
123
# File 'lib/forms/field.rb', line 115

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



79
80
81
# File 'lib/forms/field.rb', line 79

def select(choices = nil, **options)
  theme[:select].new(choices:, selected: field_value, **select_options(options))
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