phlexi-form 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/Appraisals +4 -9
  3. data/README.md +117 -316
  4. data/TODO +4 -0
  5. data/config.ru +0 -3
  6. data/gemfiles/default.gemfile.lock +22 -2
  7. data/gemfiles/rails_7.gemfile +8 -0
  8. data/gemfiles/rails_7.gemfile.lock +282 -0
  9. data/lib/phlexi/form/base.rb +65 -56
  10. data/lib/phlexi/form/components/base.rb +14 -8
  11. data/lib/phlexi/form/components/checkbox.rb +5 -0
  12. data/lib/phlexi/form/components/collection_checkboxes.rb +28 -14
  13. data/lib/phlexi/form/components/collection_radio_buttons.rb +19 -13
  14. data/lib/phlexi/form/components/concerns/extracts_input.rb +53 -0
  15. data/lib/phlexi/form/components/concerns/handles_array_input.rb +21 -0
  16. data/lib/phlexi/form/components/concerns/handles_input.rb +23 -0
  17. data/lib/phlexi/form/components/concerns/has_options.rb +6 -2
  18. data/lib/phlexi/form/components/concerns/submits_form.rb +47 -0
  19. data/lib/phlexi/form/components/error.rb +1 -1
  20. data/lib/phlexi/form/components/file_input.rb +33 -0
  21. data/lib/phlexi/form/components/hint.rb +1 -1
  22. data/lib/phlexi/form/components/input.rb +38 -36
  23. data/lib/phlexi/form/components/input_array.rb +45 -0
  24. data/lib/phlexi/form/components/label.rb +2 -1
  25. data/lib/phlexi/form/components/radio_button.rb +11 -1
  26. data/lib/phlexi/form/components/select.rb +21 -8
  27. data/lib/phlexi/form/components/submit_button.rb +41 -0
  28. data/lib/phlexi/form/components/textarea.rb +2 -3
  29. data/lib/phlexi/form/field_options/associations.rb +21 -0
  30. data/lib/phlexi/form/field_options/autofocus.rb +1 -1
  31. data/lib/phlexi/form/field_options/collection.rb +26 -9
  32. data/lib/phlexi/form/field_options/errors.rb +17 -3
  33. data/lib/phlexi/form/field_options/hints.rb +5 -1
  34. data/lib/phlexi/form/field_options/{type.rb → inferred_types.rb} +21 -17
  35. data/lib/phlexi/form/field_options/multiple.rb +2 -0
  36. data/lib/phlexi/form/field_options/required.rb +1 -1
  37. data/lib/phlexi/form/field_options/themes.rb +207 -0
  38. data/lib/phlexi/form/field_options/validators.rb +2 -2
  39. data/lib/phlexi/form/option_mapper.rb +2 -2
  40. data/lib/phlexi/form/structure/dom.rb +21 -16
  41. data/lib/phlexi/form/structure/field_builder.rb +165 -121
  42. data/lib/phlexi/form/structure/field_collection.rb +20 -6
  43. data/lib/phlexi/form/structure/namespace.rb +48 -31
  44. data/lib/phlexi/form/structure/namespace_collection.rb +20 -20
  45. data/lib/phlexi/form/structure/node.rb +13 -3
  46. data/lib/phlexi/form/version.rb +1 -1
  47. data/lib/phlexi/form.rb +4 -1
  48. metadata +32 -7
  49. data/CODE_OF_CONDUCT.md +0 -84
@@ -5,13 +5,22 @@ require "phlex"
5
5
  module Phlexi
6
6
  module Form
7
7
  module Structure
8
+ # FieldBuilder class is responsible for building form fields with various options and components.
9
+ #
10
+ # @attr_reader [Structure::DOM] dom The DOM structure for the field.
11
+ # @attr_reader [Hash] options Options for the field.
12
+ # @attr_reader [Object] object The object associated with the field.
13
+ # @attr_reader [Hash] attributes Attributes for the field.
14
+ # @attr_accessor [Object] value The value of the field.
8
15
  class FieldBuilder < Node
9
16
  include Phlex::Helpers
17
+ include FieldOptions::Associations
18
+ include FieldOptions::Themes
10
19
  include FieldOptions::Validators
11
20
  include FieldOptions::Labels
12
21
  include FieldOptions::Hints
13
22
  include FieldOptions::Errors
14
- include FieldOptions::Type
23
+ include FieldOptions::InferredTypes
15
24
  include FieldOptions::Collection
16
25
  include FieldOptions::Placeholder
17
26
  include FieldOptions::Required
@@ -24,175 +33,210 @@ module Phlexi
24
33
  include FieldOptions::Multiple
25
34
  include FieldOptions::Limit
26
35
 
27
- attr_reader :dom, :options, :object, :attributes
28
- attr_accessor :value
29
- alias_method :serialize, :value
30
- alias_method :assign, :value=
36
+ attr_reader :dom, :options, :object, :input_attributes, :value
31
37
 
32
- def initialize(key, parent:, object: nil, value: :__i_form_builder_nil_value_i__, attributes: {}, **options)
33
- key = :"#{key}"
38
+ # Initializes a new FieldBuilder instance.
39
+ #
40
+ # @param key [Symbol, String] The key for the field.
41
+ # @param parent [Structure::Namespace] The parent object.
42
+ # @param object [Object, nil] The associated object.
43
+ # @param value [Object] The initial value for the field.
44
+ # @param input_attributes [Hash] Default attributes to apply to input fields.
45
+ # @param options [Hash] Additional options for the field.
46
+ def initialize(key, parent:, object: nil, value: NIL_VALUE, input_attributes: {}, **options)
34
47
  super(key, parent: parent)
35
48
 
36
49
  @object = object
37
- @value = if value != :__i_form_builder_nil_value_i__
38
- value
39
- else
40
- object.respond_to?(key) ? object.send(key) : nil
41
- end
42
- @attributes = attributes
50
+ @value = determine_initial_value(value)
51
+ @input_attributes = input_attributes
43
52
  @options = options
44
53
  @dom = Structure::DOM.new(field: self)
45
54
  end
46
55
 
47
- def label_tag(**attributes)
48
- attributes = self.attributes.deep_merge(attributes)
49
- label_class = attributes.delete(:class) || themed(attributes.delete(:theme) || :label)
50
- Components::Label.new(self, class: label_class, **attributes)
56
+ # Creates a label tag for the field.
57
+ #
58
+ # @param attributes [Hash] Additional attributes for the label.
59
+ # @return [Components::Label] The label component.
60
+ def label_tag(**, &)
61
+ create_component(Components::Label, :label, **, &)
51
62
  end
52
63
 
53
- def input_tag(**attributes)
54
- attributes = self.attributes.deep_merge(attributes)
55
- input_class = attributes.delete(:class) || themed(attributes.delete(:theme) || :input)
56
- Components::Input.new(self, class: input_class, **attributes)
64
+ # Creates an input tag for the field.
65
+ #
66
+ # @param attributes [Hash] Additional attributes for the input.
67
+ # @return [Components::Input] The input component.
68
+ def input_tag(**, &)
69
+ create_component(Components::Input, :input, **, &)
57
70
  end
58
71
 
59
- def checkbox_tag(**attributes)
60
- attributes = self.attributes.deep_merge(attributes)
61
- checkbox_class = attributes.delete(:class) || themed(attributes.delete(:theme) || :checkbox)
62
- Components::Checkbox.new(self, class: checkbox_class, **attributes)
72
+ def file_input_tag(**, &)
73
+ create_component(Components::FileInput, :file, **, &)
63
74
  end
64
75
 
65
- def collection_checkboxes_tag(**attributes, &)
66
- attributes = self.attributes.deep_merge(attributes)
67
- collection_checkboxes_class = attributes.delete(:class) || themed(attributes.delete(:theme) || :collection_checkboxes)
68
- Components::CollectionCheckboxes.new(self, class: collection_checkboxes_class, **attributes, &)
76
+ # Creates a checkbox tag for the field.
77
+ #
78
+ # @param attributes [Hash] Additional attributes for the checkbox.
79
+ # @return [Components::Checkbox] The checkbox component.
80
+ def checkbox_tag(**, &)
81
+ create_component(Components::Checkbox, :checkbox, **, &)
69
82
  end
70
83
 
71
- def radio_button_tag(**attributes)
72
- attributes = self.attributes.deep_merge(attributes)
73
- radio_class = attributes.delete(:class) || themed(attributes.delete(:theme) || :radio)
74
- Components::RadioButton.new(self, class: radio_class, **attributes)
84
+ # Creates collection checkboxes for the field.
85
+ #
86
+ # @param attributes [Hash] Additional attributes for the collection checkboxes.
87
+ # @yield [block] The block to be executed for each checkbox.
88
+ # @return [Components::CollectionCheckboxes] The collection checkboxes component.
89
+ def collection_checkboxes_tag(**, &)
90
+ create_component(Components::CollectionCheckboxes, :collection_checkboxes, **, &)
75
91
  end
76
92
 
77
- def collection_radio_buttons_tag(**attributes, &)
78
- attributes = self.attributes.deep_merge(attributes)
79
- collection_radio_buttons_class = attributes.delete(:class) || themed(attributes.delete(:theme) || :collection_radio_buttons)
80
- Components::CollectionRadioButtons.new(self, class: collection_radio_buttons_class, **attributes, &)
93
+ # Creates a radio button tag for the field.
94
+ #
95
+ # @param attributes [Hash] Additional attributes for the radio button.
96
+ # @return [Components::RadioButton] The radio button component.
97
+ def radio_button_tag(**, &)
98
+ create_component(Components::RadioButton, :radio, **, &)
81
99
  end
82
100
 
83
- def textarea_tag(**attributes)
84
- attributes = self.attributes.deep_merge(attributes)
85
- textarea_class = attributes.delete(:class) || themed_input(attributes.delete(:theme) || :textarea)
86
- Components::Textarea.new(self, class: textarea_class, **attributes)
101
+ # Creates collection radio buttons for the field.
102
+ #
103
+ # @param attributes [Hash] Additional attributes for the collection radio buttons.
104
+ # @yield [block] The block to be executed for each radio button.
105
+ # @return [Components::CollectionRadioButtons] The collection radio buttons component.
106
+ def collection_radio_buttons_tag(**, &)
107
+ create_component(Components::CollectionRadioButtons, :collection_radio_buttons, **, &)
87
108
  end
88
109
 
89
- def select_tag(**attributes)
90
- attributes = self.attributes.deep_merge(attributes)
91
- select_class = attributes.delete(:class) || themed_input(attributes.delete(:theme) || :select)
92
- Components::Select.new(self, class: select_class, **attributes)
110
+ # Creates a textarea tag for the field.
111
+ #
112
+ # @param attributes [Hash] Additional attributes for the textarea.
113
+ # @return [Components::Textarea] The textarea component.
114
+ def textarea_tag(**, &)
115
+ create_component(Components::Textarea, :textarea, **, &)
93
116
  end
94
117
 
95
- def hint_tag(**attributes)
96
- attributes = self.attributes.deep_merge(attributes)
97
- hint_class = attributes.delete(:class) || themed(attributes.delete(:theme) || :hint)
98
- Components::Hint.new(self, class: hint_class, **attributes)
118
+ # Creates a select tag for the field.
119
+ #
120
+ # @param attributes [Hash] Additional attributes for the select.
121
+ # @return [Components::Select] The select component.
122
+ def select_tag(**, &)
123
+ create_component(Components::Select, :select, **, &)
99
124
  end
100
125
 
101
- def error_tag(**attributes)
102
- attributes = self.attributes.deep_merge(attributes)
103
- error_class = attributes.delete(:class) || themed(attributes.delete(:theme) || :error)
104
- Components::Error.new(self, class: error_class, **attributes)
126
+ def input_array_tag(**, &)
127
+ create_component(Components::InputArray, :array, **, &)
105
128
  end
106
129
 
107
- def full_error_tag(**attributes)
108
- attributes = self.attributes.deep_merge(attributes)
109
- error_class = attributes.delete(:class) || themed(attributes.delete(:theme) || :error)
110
- Components::FullError.new(self, class: error_class, **attributes)
130
+ # Creates a hint tag for the field.
131
+ #
132
+ # @param attributes [Hash] Additional attributes for the hint.
133
+ # @return [Components::Hint] The hint component.
134
+ def hint_tag(**, &)
135
+ create_component(Components::Hint, :hint, **, &)
111
136
  end
112
137
 
113
- def wrapped(inner: {}, **attributes, &)
114
- attributes = self.attributes.deep_merge(attributes)
115
- wrapper_class = attributes.delete(:class) || themed(attributes.delete(:theme) || :wrapper)
116
- inner[:class] = inner.delete(:class) || themed(inner.delete(:theme) || :inner_wrapper)
117
- Components::Wrapper.new(self, class: wrapper_class, inner:, **attributes, &)
138
+ # Creates an error tag for the field.
139
+ #
140
+ # @param attributes [Hash] Additional attributes for the error.
141
+ # @return [Components::Error] The error component.
142
+ def error_tag(**, &)
143
+ create_component(Components::Error, :error, **, &)
118
144
  end
119
145
 
120
- # Wraps a field that's an array of values with a bunch of fields
146
+ # Creates a full error tag for the field.
121
147
  #
122
- # @example Usage
148
+ # @param attributes [Hash] Additional attributes for the full error.
149
+ # @return [Components::FullError] The full error component.
150
+ def full_error_tag(**, &)
151
+ create_component(Components::FullError, :full_error, **, &)
152
+ end
153
+
154
+ # Wraps the field with additional markup.
123
155
  #
124
- # ```ruby
125
- # Phlexi::Form.new User.new do
126
- # render field(:email).input_tag
127
- # render field(:name).input_tag
128
- # field(:roles).multi([["Admin", "admin"], ["Editor", "editor"]]) do |a|
129
- # render a.label_tag
130
- # render a.input_tag # => name="user[roles][]"
131
- # end
132
- # end
133
- # ```
134
- # The object within the block is a `FieldCollection` object
135
- def multi(range = nil, &)
136
- range ||= Array(collection)
137
- FieldCollection.new(field: self, range:, &)
156
+ # @param inner [Hash] Attributes for the inner wrapper.
157
+ # @param attributes [Hash] Additional attributes for the wrapper.
158
+ # @yield [block] The block to be executed within the wrapper.
159
+ # @return [Components::Wrapper] The wrapper component.
160
+ def wrapped(inner: {}, **attributes, &)
161
+ attributes = apply_component_theme(attributes, :wrapper)
162
+ inner = apply_component_theme(inner, :inner_wrapper)
163
+ Components::Wrapper.new(self, inner: inner, **attributes, &)
138
164
  end
139
165
 
140
- private
166
+ # Creates a repeated field collection.
167
+ #
168
+ # @param range [Integer, #to_a] The range of keys for each field.
169
+ # If an integer (e.g. 6) is passed, it is converted to a range = 1..6
170
+ # @yield [block] The block to be executed for each item in the collection.
171
+ # @return [FieldCollection] The field collection.
172
+ def repeated(range = nil, &)
173
+ FieldCollection.new(field: self, range: range, &)
174
+ end
141
175
 
142
- def themed(component)
143
- tokens(resolve_theme(component), resolve_validity_theme(component)).presence if component
176
+ # Creates a submit button
177
+ #
178
+ # @param attributes [Hash] Additional attributes for the submit.
179
+ # @return [Components::SubmitButton] The submit button component.
180
+ def submit_button_tag(**, &)
181
+ create_component(Components::SubmitButton, :submit_button, **, &)
144
182
  end
145
183
 
146
- def themed_input(input_component)
147
- themed(input_component) || themed(:input) if input_component
184
+ def extract_input(params)
185
+ raise "field##{dom.name} did not define an input component" unless @field_input_extractor
186
+
187
+ @field_input_extractor.extract_input(params)
148
188
  end
149
189
 
150
- def resolve_theme(property)
151
- options[property] || theme[property]
190
+ protected
191
+
192
+ def create_component(component_class, theme_key, **attributes, &)
193
+ attributes = mix(input_attributes, attributes) if component_class.include?(Phlexi::Form::Components::Concerns::HandlesInput)
194
+ component = component_class.new(self, **apply_component_theme(attributes, theme_key), &)
195
+ if component_class.include?(Components::Concerns::ExtractsInput)
196
+ raise "input component already defined: #{@field_input_extractor.inspect}" if @field_input_extractor
197
+
198
+ @field_input_extractor = component
199
+ end
200
+
201
+ component
152
202
  end
153
203
 
154
- def resolve_validity_theme(property)
155
- validity_property = if has_errors?
156
- # Apply invalid class if the object has errors
157
- :"invalid_#{property}"
158
- elsif (object.respond_to?(:persisted?) && object.persisted?) && (object.respond_to?(:errors) && !object.errors.empty?)
159
- # The object is persisted, has been validated, and there are errors (not empty), but this field has no errors
160
- # Apply valid class
161
- :"valid_#{property}"
204
+ def apply_component_theme(attributes, theme_key)
205
+ theme_key = attributes.delete(:theme) || theme_key
206
+ if attributes.key?(:class!)
207
+ attributes
162
208
  else
163
- :"neutral_#{property}"
209
+ mix({class: themed(theme_key)}, attributes)
164
210
  end
165
-
166
- resolve_theme(validity_property)
167
- end
168
-
169
- def theme
170
- @theme ||= {
171
- # # label themes
172
- # label: "md:w-1/6 mt-2 block mb-2 text-sm font-medium",
173
- # invalid_label: "text-red-700 dark:text-red-500",
174
- # valid_label: "text-green-700 dark:text-green-500",
175
- # neutral_label: "text-gray-700 dark:text-white",
176
- # # input themes
177
- # input: "w-full p-2 border rounded-md shadow-sm font-medium text-sm dark:bg-gray-700",
178
- # invalid_input: "bg-red-50 border-red-500 dark:border-red-500 text-red-900 dark:text-red-500 placeholder-red-700 dark:placeholder-red-500 focus:ring-red-500 focus:border-red-500",
179
- # valid_input: "bg-green-50 border-green-500 dark:border-green-500 text-green-900 dark:text-green-400 placeholder-green-700 dark:placeholder-green-500 focus:ring-green-500 focus:border-green-500",
180
- # neutral_input: "border-gray-300 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white focus:ring-primary-500 focus:border-primary-500",
181
- # # hint themes
182
- # hint: "mt-2 text-sm text-gray-500 dark:text-gray-200",
183
- # # error themes
184
- # error: "mt-2 text-sm text-red-600 dark:text-red-500",
185
- # # wrapper themes
186
- # wrapper: "flex flex-col md:flex-row items-start space-y-2 md:space-y-0 md:space-x-2 mb-4",
187
- # inner_wrapper: "md:w-5/6 w-full",
188
- }.freeze
189
- end
190
-
191
- def reflection = nil
211
+ end
192
212
 
193
213
  def has_value?
194
214
  value.present?
195
215
  end
216
+
217
+ def determine_initial_value(value)
218
+ return value unless value == NIL_VALUE
219
+
220
+ determine_value_from_association || determine_value_from_object
221
+ end
222
+
223
+ def determine_value_from_object
224
+ object.respond_to?(key) ? object.public_send(key) : nil
225
+ end
226
+
227
+ def determine_value_from_association
228
+ return nil unless association_reflection.present?
229
+
230
+ value = object.public_send(key)
231
+ case association_reflection.macro
232
+ when :has_many, :has_and_belongs_to_many
233
+ value&.map { |v| v.public_send(association_reflection.klass.primary_key) }
234
+ when :belongs_to, :has_one
235
+ value&.public_send(association_reflection.klass.primary_key)
236
+ else
237
+ raise ArgumentError, "Unsupported association type: #{association_reflection.macro}"
238
+ end
239
+ end
196
240
  end
197
241
  end
198
242
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "phlex"
4
+
3
5
  module Phlexi
4
6
  module Form
5
7
  module Structure
@@ -7,18 +9,30 @@ module Phlexi
7
9
  include Enumerable
8
10
 
9
11
  class Builder
10
- attr_reader :key
12
+ include Phlex::Helpers
13
+
14
+ attr_reader :key, :index
11
15
 
12
- def initialize(key, field)
16
+ def initialize(key, field, index)
13
17
  @key = key.to_s
14
18
  @field = field
19
+ @index = index
15
20
  end
16
21
 
17
- def field(**)
18
- @field.class.new(key, attributes: @field.attributes, **, parent: @field).tap do |field|
22
+ def field(**options)
23
+ options = mix({input_attributes: @field.input_attributes}, options)
24
+ @field.class.new(key, **options, parent: @field).tap do |field|
19
25
  yield field if block_given?
20
26
  end
21
27
  end
28
+
29
+ def hidden_field_tag(value: "", force: false)
30
+ raise "Attempting to build hidden field on non-first field in a collection" unless index == 0 || force
31
+
32
+ @field.class
33
+ .new("hidden", parent: @field)
34
+ .input_tag(type: :hidden, value:)
35
+ end
22
36
  end
23
37
 
24
38
  def initialize(field:, range:, &)
@@ -35,8 +49,8 @@ module Phlexi
35
49
  end
36
50
 
37
51
  def each(&)
38
- @range.each do |key|
39
- yield Builder.new(key, @field)
52
+ @range.each.with_index do |key, index|
53
+ yield Builder.new(key, @field, index)
40
54
  end
41
55
  end
42
56
  end
@@ -3,22 +3,27 @@
3
3
  module Phlexi
4
4
  module Form
5
5
  module Structure
6
- # A Namespace maps and object to values, but doesn't actually have a value itself. For
6
+ # A Namespace maps an object to values, but doesn't actually have a value itself. For
7
7
  # example, a `User` object or ActiveRecord model could be passed into the `:user` namespace.
8
- # To access the values on a Namespace, the `field` can be called for single values.
9
8
  #
10
- # Additionally, to access namespaces within a namespace, such as if a `User has_many :addresses` in
11
- # ActiveRecord, the `namespace` method can be called which will return another Namespace object and
12
- # set the current Namespace as the parent.
9
+ # To access single values on a Namespace, #field can be used.
10
+ #
11
+ # To access nested objects within a namespace, two methods are available:
12
+ #
13
+ # 1. #nest_one: Used for single nested objects, such as if a `User belongs_to :profile` in
14
+ # ActiveRecord. This method returns another Namespace object.
15
+ #
16
+ # 2. #nest_many: Used for collections of nested objects, such as if a `User has_many :addresses` in
17
+ # ActiveRecord. This method returns a NamespaceCollection object.
13
18
  class Namespace < Structure::Node
14
19
  include Enumerable
15
20
 
16
21
  attr_reader :builder_klass, :object
17
22
 
18
- def initialize(key, parent:, object: nil, builder_klass: Field)
23
+ def initialize(key, parent:, builder_klass:, object: nil)
19
24
  super(key, parent: parent)
20
- @object = object
21
25
  @builder_klass = builder_klass
26
+ @object = object
22
27
  @children = {}
23
28
  yield self if block_given?
24
29
  end
@@ -29,6 +34,10 @@ module Phlexi
29
34
  end
30
35
  end
31
36
 
37
+ def submit_button(key = :submit_button, **, &)
38
+ field(key).submit_button_tag(**, &)
39
+ end
40
+
32
41
  # Creates a `Namespace` child instance with the parent set to the current instance, adds to
33
42
  # the `@children` Hash to ensure duplicate child namespaces aren't created, then calls the
34
43
  # method on the `@object` to get the child object to pass into that namespace.
@@ -37,14 +46,14 @@ module Phlexi
37
46
  # form like this:
38
47
  #
39
48
  # ```ruby
40
- # Superform :user, object: User.new do |form|
41
- # form.nest_one :permission do |permission|
42
- # form.field :role
49
+ # Phlexi::Form(User.new, as: :user) do
50
+ # nest_one :profile do |profile|
51
+ # render profile.field(:gender).input_tag
43
52
  # end
44
53
  # end
45
54
  # ```
46
55
  def nest_one(key, object: nil, &)
47
- object ||= object_for(key: key)
56
+ object ||= object_value_for(key: key)
48
57
  create_child(key, self.class, object:, builder_klass:, &)
49
58
  end
50
59
 
@@ -52,10 +61,10 @@ module Phlexi
52
61
  # an enumerable or array of `Address` classes:
53
62
  #
54
63
  # ```ruby
55
- # Phlexi::Form.new User.new do |form|
56
- # render form.field(:email).input_tag
57
- # render form.field(:name).input_tag
58
- # form.nest_many :addresses do |address|
64
+ # Phlexi::Form(User.new) do
65
+ # render field(:email).input_tag
66
+ # render field(:name).input_tag
67
+ # nest_many :addresses do |address|
59
68
  # render address.field(:street).input_tag
60
69
  # render address.field(:state).input_tag
61
70
  # render address.field(:zip).input_tag
@@ -65,17 +74,20 @@ module Phlexi
65
74
  # The object within the block is a `Namespace` object that maps each object within the enumerable
66
75
  # to another `Namespace` or `Field`.
67
76
  def nest_many(key, collection: nil, &)
68
- collection ||= Array(object_for(key: key))
77
+ collection ||= Array(object_value_for(key: key))
69
78
  create_child(key, NamespaceCollection, collection:, &)
70
79
  end
71
80
 
72
- # Creates a Hash of Hashes and Arrays that represent the fields and collections of the Superform.
73
- # This can be used to safely update ActiveRecord objects without the need for Strong Parameters.
74
- # You will want to make sure that all the fields displayed in the form are ones that you're OK updating
75
- # from the generated hash.
76
- def serialize
77
- each_with_object({}) do |child, hash|
78
- hash[child.key] = child.serialize
81
+ def extract_input(params)
82
+ if params.is_a?(Array)
83
+ each_with_object({}) do |child, hash|
84
+ hash.merge! child.extract_input(params[0])
85
+ end
86
+ else
87
+ input = each_with_object({}) do |child, hash|
88
+ hash.merge! child.extract_input(params[key])
89
+ end
90
+ {key => input}
79
91
  end
80
92
  end
81
93
 
@@ -85,17 +97,22 @@ module Phlexi
85
97
  @children.values.each(&)
86
98
  end
87
99
 
88
- # Assigns a hash to the current namespace and children namespace.
89
- def assign(hash)
90
- each do |child|
91
- child.assign hash[child.key]
100
+ def dom_id
101
+ @dom_id ||= begin
102
+ id = if object.nil?
103
+ nil
104
+ elsif object.class.respond_to?(:primary_key)
105
+ object.public_send(object.class.primary_key) || :new
106
+ elsif object.respond_to?(:id)
107
+ object.id || :new
108
+ end
109
+ [key, id].compact.join("_").underscore
92
110
  end
93
- self
94
111
  end
95
112
 
96
113
  # Creates a root Namespace, which is essentially a form.
97
- def self.root(*, **, &)
98
- new(*, parent: nil, **, &)
114
+ def self.root(*, builder_klass:, **, &)
115
+ new(*, parent: nil, builder_klass:, **, &)
99
116
  end
100
117
 
101
118
  protected
@@ -106,7 +123,7 @@ module Phlexi
106
123
  #
107
124
  # This method could be overwritten if the mapping between the `@object` and `key` name is not
108
125
  # a method call. For example, a `Hash` would be accessed via `user[:email]` instead of `user.send(:email)`
109
- def object_for(key:)
126
+ def object_value_for(key:)
110
127
  @object.send(key) if @object.respond_to? key
111
128
  end
112
129
 
@@ -6,36 +6,36 @@ module Phlexi
6
6
  class NamespaceCollection < Node
7
7
  include Enumerable
8
8
 
9
- def initialize(key, parent:, collection: nil, &)
9
+ def initialize(key, parent:, collection: nil, &block)
10
+ raise ArgumentError, "block is required" unless block.present?
11
+
10
12
  super(key, parent: parent)
11
13
 
12
- @namespaces = enumerate(collection)
13
- each(&) if block_given?
14
+ @collection = collection
15
+ @block = block
16
+ each(&block)
14
17
  end
15
18
 
16
- def serialize
17
- map(&:serialize)
18
- end
19
+ def extract_input(params)
20
+ namespace = build_namespace(0)
21
+ @block.call(namespace)
19
22
 
20
- def assign(array)
21
- # The problem with zip-ing the array is if I need to add new
22
- # elements to it and wrap it in the namespace.
23
- zip(array) do |namespace, hash|
24
- namespace.assign hash
25
- end
23
+ inputs = params[key].map { |param| namespace.extract_input([param]) }
24
+ {key => inputs}
26
25
  end
27
26
 
27
+ private
28
+
28
29
  def each(&)
29
- @namespaces.each(&)
30
+ namespaces.each(&)
30
31
  end
31
32
 
32
- private
33
-
34
- def enumerate(enumerator)
35
- Enumerator.new do |y|
36
- enumerator.each.with_index do |object, key|
37
- y << build_namespace(key, object: object)
38
- end
33
+ # Builds and memoizes namespaces for the collection.
34
+ #
35
+ # @return [Array<Namespace>] An array of namespace objects.
36
+ def namespaces
37
+ @namespaces ||= @collection.map.with_index do |object, key|
38
+ build_namespace(key, object: object)
39
39
  end
40
40
  end
41
41
 
@@ -3,15 +3,25 @@
3
3
  module Phlexi
4
4
  module Form
5
5
  module Structure
6
- # Superclass for Namespace and Field classes. Not much to it other than it has a `name`
7
- # and `parent` node attribute. Think of it as a tree.
6
+ # Superclass for Namespace and Field classes. Represents a node in the form tree structure.
7
+ #
8
+ # @attr_reader [Symbol] key The node's key
9
+ # @attr_reader [Node, nil] parent The node's parent in the tree structure
8
10
  class Node
9
11
  attr_reader :key, :parent
10
12
 
13
+ # Initializes a new Node instance.
14
+ #
15
+ # @param key [Symbol, String] The key for the node
16
+ # @param parent [Node, nil] The parent node
11
17
  def initialize(key, parent:)
12
- @key = key
18
+ @key = :"#{key}"
13
19
  @parent = parent
14
20
  end
21
+
22
+ def inspect
23
+ "<#{self.class.name} key=#{key.inspect} parent=#{id.inspect} />"
24
+ end
15
25
  end
16
26
  end
17
27
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlexi
4
4
  module Form
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end