Class: Forms::Form

Inherits:
Phlex::HTML
  • Object
show all
Includes:
Phlex::Rails::Helpers::FormAuthenticityToken, PhlexForms::Builder
Defined in:
lib/forms/form.rb

Direct Known Subclasses

Base

Constant Summary

Constants included from PhlexForms::Builder

PhlexForms::Builder::INPUT_TYPE_INFERENCE, PhlexForms::Builder::INPUT_TYPE_MODIFIERS

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from PhlexForms::Builder

#Checkbox, #Control, #FileInput, #Hidden, #Input, #Label, #Radio, #Select, #Textarea, #Toggle, #field, #group, #row

Constructor Details

#initialize(*modifiers, model: nil, scope: nil, url: nil, method: nil, validate: false, field_variants: nil, theme: nil, live: nil, **options) ⇒ Form

Returns a new instance of Form.



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/forms/form.rb', line 28

def initialize(*modifiers, model: nil, scope: nil, url: nil, method: nil, validate: false,
               field_variants: nil, theme: nil, live: nil, **options)
  if live
    raise ArgumentError,
      "live validation requires a Forms::Base subclass — the endpoint rebuilds the " \
      "form from its class, and an inline block cannot be serialized. Declare " \
      "`live model: #{record_from(model)&.class&.name || 'YourModel'}` on the class instead."
  end

  super()
  @base_modifiers = modifiers
  @field_variants = Array(field_variants)
  @theme = PhlexForms::Theme.resolve(theme)
  @options = options
  @model = record_from(model)
  # scope: false opts out of scoping entirely — bare field names for
  # reactive row editors / <template>-cloned rows. nil means "derive".
  @scope = scope == false ? nil : (scope&.to_s || derive_scope(@model))
  @url = url || derive_url(model)
  @method = method || derive_method(@model)
  @errors = (@model.errors if @model.respond_to?(:errors))
  @validate = validate
end

Instance Attribute Details

#errorsObject (readonly)

Returns the value of attribute errors.



20
21
22
# File 'lib/forms/form.rb', line 20

def errors
  @errors
end

#methodObject (readonly)

Returns the value of attribute method.



20
21
22
# File 'lib/forms/form.rb', line 20

def method
  @method
end

#modelObject (readonly) Also known as: object

Returns the value of attribute model.



20
21
22
# File 'lib/forms/form.rb', line 20

def model
  @model
end

#scopeObject (readonly) Also known as: object_name

Returns the value of attribute scope.



20
21
22
# File 'lib/forms/form.rb', line 20

def scope
  @scope
end

#themeObject (readonly)

Returns the value of attribute theme.



20
21
22
# File 'lib/forms/form.rb', line 20

def theme
  @theme
end

#urlObject (readonly)

Returns the value of attribute url.



20
21
22
# File 'lib/forms/form.rb', line 20

def url
  @url
end

#validateObject (readonly)

Returns the value of attribute validate.



20
21
22
# File 'lib/forms/form.rb', line 20

def validate
  @validate
end

Instance Method Details

#collection_check_boxes(name, collection, value_method, text_method) ⇒ Object

Rails-style collection_check_boxes. Emits a hidden field so an empty selection still submits, then yields a builder per collection item.



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/forms/form.rb', line 118

def collection_check_boxes(name, collection, value_method, text_method, &)
  return unless block_given?

  input(type: "hidden", name: "#{field_name(name)}[]", value: "")
  current = Array(@model&.public_send(name))
  current_ids = current.map { |v| v.respond_to?(value_method) ? v.public_send(value_method) : v }

  collection.each do |item|
    item_value = item.public_send(value_method)
    item_text = text_method.is_a?(Proc) ? text_method.call(item) : item.public_send(text_method)
    yield Forms::CollectionCheckBoxBuilder.new(
      object: item, value: item_value, text: item_text,
      checked: current_ids.include?(item_value),
      name: "#{field_name(name)}[]", id: "#{field_id(name)}_#{item_value}"
    )
  end
end

#collection_select(name, collection, value_method, text_method, options = {}, html_options = {}) ⇒ Object

Rails-style collection_select over an enumerable of records.



137
138
139
140
141
142
143
144
# File 'lib/forms/form.rb', line 137

def collection_select(name, collection, value_method, text_method, options = {}, html_options = {})
  choices = collection.map do |item|
    text = text_method.is_a?(Proc) ? text_method.call(item) : item.public_send(text_method)
    [text, item.public_send(value_method)]
  end
  choices = [[options[:prompt], ""]] + choices if options[:prompt]
  render field_object(name).select(choices, **options.except(:prompt), **html_options)
end

#default_field_variantsObject

Default variants prepended to every field's inner input: global config first, then this form's field_variants: (call-site modifiers stack last).



148
149
150
# File 'lib/forms/form.rb', line 148

def default_field_variants
  PhlexForms.config.field_variants + @field_variants
end

#field_id(name) ⇒ Object



154
# File 'lib/forms/form.rb', line 154

def field_id(name)    = @scope ? "#{@scope}_#{name}" : name.to_s

#field_name(name) ⇒ Object

Public name/id/value helpers for external components mirroring the Rails API.



153
# File 'lib/forms/form.rb', line 153

def field_name(name)  = @scope ? "#{@scope}[#{name}]" : name.to_s

#field_object(name, error_name: nil) ⇒ Object Also known as: []

Return a Forms::Field for a name (used by the Builder mixin).



72
73
74
# File 'lib/forms/form.rb', line 72

def field_object(name, error_name: nil)
  Forms::Field.new(name:, model: @model, scope: @scope, errors: @errors, form: self, error_name:)
end

#field_value(name) ⇒ Object



155
# File 'lib/forms/form.rb', line 155

def field_value(name) = field_object(name).field_value

#fields_for(association_name, model = nil, nested_attributes: true) ⇒ Object

Nested attributes. Yields a FieldsForBuilder per association (single) or per item (has_many), with the correctly-indexed nested scope. nested_attributes: false nests under the raw name (no _attributes suffix) for JSONB/hash columns that aren't Rails nested attributes.



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/forms/form.rb', line 100

def fields_for(association_name, model = nil, nested_attributes: true, &)
  return unless block_given?

  associated = model || (@model.public_send(association_name) if @model.respond_to?(association_name))
  attributes_key = nested_attributes ? "#{association_name}_attributes" : association_name.to_s
  base_scope = @scope ? "#{@scope}[#{attributes_key}]" : attributes_key

  if associated.respond_to?(:each_with_index)
    associated.each_with_index do |item, index|
      yield build_fields_for("#{base_scope}[#{index}]", item)
    end
  else
    yield build_fields_for(base_scope, associated)
  end
end

#rich_textarea(name, *modifiers) ⇒ Object Also known as: rich_text_area



81
82
83
# File 'lib/forms/form.rb', line 81

def rich_textarea(name, *modifiers, **)
  render field_object(name).rich_textarea(*modifiers, **)
end

#submitObject



77
78
79
# File 'lib/forms/form.rb', line 77

def submit(*, **, &)
  render theme[:submit].new(*, model: @model, **, &)
end

#time_zone_select(name, *modifiers, selected: nil) ⇒ Object



86
87
88
89
90
91
92
93
94
# File 'lib/forms/form.rb', line 86

def time_zone_select(name, *modifiers, selected: nil, **)
  render Forms::TimeZoneSelect.new(
    *modifiers,
    name: field_name(name),
    id: field_id(name),
    selected: selected || @model&.public_send(name),
    **
  )
end

#validations_introspectorObject

Introspector for the bound model when client-side validation is enabled, or a no-op Null otherwise. Callers can always call #data_attributes_for(attr).



54
55
56
57
58
59
60
61
# File 'lib/forms/form.rb', line 54

def validations_introspector
  @validations_introspector ||=
    if @validate
      Forms::Validations::Introspector.for(@model)
    else
      Forms::Validations::Introspector::Null.new
    end
end

#view_templateObject



63
64
65
66
67
68
69
# File 'lib/forms/form.rb', line 63

def view_template(&)
  form(action: @url, accept_charset: "UTF-8", method: form_method, **form_attributes) do
    authenticity_token_field unless @method&.to_sym == :get
    method_field if @method && %i[get post].exclude?(@method.to_sym)
    yield self if block_given?
  end
end