phlexi-display 0.0.1 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/lib/phlexi/display/base.rb +37 -169
  3. data/lib/phlexi/display/builder.rb +117 -0
  4. data/lib/phlexi/display/components/base.rb +1 -42
  5. data/lib/phlexi/display/components/concerns/displays_value.rb +54 -0
  6. data/lib/phlexi/display/components/date_time.rb +49 -0
  7. data/lib/phlexi/display/components/{error.rb → description.rb} +5 -5
  8. data/lib/phlexi/display/components/hint.rb +1 -1
  9. data/lib/phlexi/display/components/label.rb +3 -15
  10. data/lib/phlexi/display/components/number.rb +37 -0
  11. data/lib/phlexi/display/components/placeholder.rb +15 -0
  12. data/lib/phlexi/display/components/string.rb +17 -0
  13. data/lib/phlexi/display/components/wrapper.rb +4 -18
  14. data/lib/phlexi/display/theme.rb +22 -0
  15. data/lib/phlexi/display/version.rb +1 -1
  16. data/lib/phlexi/display.rb +1 -1
  17. metadata +24 -43
  18. data/lib/phlexi/display/components/checkbox.rb +0 -48
  19. data/lib/phlexi/display/components/collection_checkboxes.rb +0 -44
  20. data/lib/phlexi/display/components/collection_radio_buttons.rb +0 -35
  21. data/lib/phlexi/display/components/concerns/handles_array_input.rb +0 -21
  22. data/lib/phlexi/display/components/concerns/handles_input.rb +0 -53
  23. data/lib/phlexi/display/components/concerns/has_options.rb +0 -37
  24. data/lib/phlexi/display/components/concerns/submits_form.rb +0 -39
  25. data/lib/phlexi/display/components/file_input.rb +0 -32
  26. data/lib/phlexi/display/components/full_error.rb +0 -21
  27. data/lib/phlexi/display/components/input.rb +0 -84
  28. data/lib/phlexi/display/components/input_array.rb +0 -45
  29. data/lib/phlexi/display/components/radio_button.rb +0 -41
  30. data/lib/phlexi/display/components/select.rb +0 -69
  31. data/lib/phlexi/display/components/submit_button.rb +0 -41
  32. data/lib/phlexi/display/components/textarea.rb +0 -34
  33. data/lib/phlexi/display/field_options/associations.rb +0 -21
  34. data/lib/phlexi/display/field_options/autofocus.rb +0 -18
  35. data/lib/phlexi/display/field_options/collection.rb +0 -54
  36. data/lib/phlexi/display/field_options/disabled.rb +0 -18
  37. data/lib/phlexi/display/field_options/errors.rb +0 -92
  38. data/lib/phlexi/display/field_options/hints.rb +0 -22
  39. data/lib/phlexi/display/field_options/inferred_types.rb +0 -155
  40. data/lib/phlexi/display/field_options/labels.rb +0 -28
  41. data/lib/phlexi/display/field_options/length.rb +0 -53
  42. data/lib/phlexi/display/field_options/limit.rb +0 -66
  43. data/lib/phlexi/display/field_options/min_max.rb +0 -92
  44. data/lib/phlexi/display/field_options/multiple.rb +0 -65
  45. data/lib/phlexi/display/field_options/pattern.rb +0 -38
  46. data/lib/phlexi/display/field_options/placeholder.rb +0 -18
  47. data/lib/phlexi/display/field_options/readonly.rb +0 -18
  48. data/lib/phlexi/display/field_options/required.rb +0 -37
  49. data/lib/phlexi/display/field_options/themes.rb +0 -207
  50. data/lib/phlexi/display/field_options/validators.rb +0 -48
  51. data/lib/phlexi/display/option_mapper.rb +0 -154
  52. data/lib/phlexi/display/structure/dom.rb +0 -62
  53. data/lib/phlexi/display/structure/field_builder.rb +0 -236
  54. data/lib/phlexi/display/structure/field_collection.rb +0 -54
  55. data/lib/phlexi/display/structure/namespace.rb +0 -135
  56. data/lib/phlexi/display/structure/namespace_collection.rb +0 -48
  57. data/lib/phlexi/display/structure/node.rb +0 -18
@@ -1,154 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Display
5
- # OptionMapper is responsible for converting a collection of objects into a hash of options
6
- # suitable for form controls, such as `select > options`.
7
- # Both values and labels are converted to strings.
8
- #
9
- # @example Basic usage
10
- # collection = [["First", 1], ["Second", 2]]
11
- # mapper = OptionMapper.new(collection)
12
- # mapper.each { |value, label| puts "#{value}: #{label}" }
13
- #
14
- # @example Using with ActiveRecord objects
15
- # users = User.all
16
- # mapper = OptionMapper.new(users)
17
- # mapper.each { |id, name| puts "#{id}: #{name}" }
18
- #
19
- # @example Array access with different value types
20
- # mapper = OptionMapper.new([["Integer", 1], ["String", "2"], ["Symbol", :three]])
21
- # puts mapper["1"] # Output: "Integer"
22
- # puts mapper["2"] # Output: "String"
23
- # puts mapper["three"] # Output: "Symbol"
24
- #
25
- # @note This class is thread-safe as it doesn't maintain mutable state.
26
- class OptionMapper
27
- include Enumerable
28
-
29
- # Initializes a new OptionMapper instance.
30
- #
31
- # @param collection [#call, #to_a] The collection to be mapped.
32
- # @param label_method [Symbol, nil] The method to call on each object to get the label.
33
- # @param value_method [Symbol, nil] The method to call on each object to get the value.
34
- def initialize(collection, label_method: nil, value_method: nil)
35
- @raw_collection = collection
36
- @label_method = label_method
37
- @value_method = value_method
38
- end
39
-
40
- # Iterates over the collection, yielding value-label pairs.
41
- #
42
- # @yieldparam value [String] The string value for the current item.
43
- # @yieldparam label [String] The string label for the current item.
44
- # @return [Enumerator] If no block is given.
45
- def each(&)
46
- collection.each(&)
47
- end
48
-
49
- # @return [Array<String>] An array of all labels in the collection.
50
- def labels
51
- collection.values
52
- end
53
-
54
- # @return [Array<String>] An array of all values in the collection.
55
- def values
56
- collection.keys
57
- end
58
-
59
- # Retrieves the label for a given value.
60
- #
61
- # @param value [#to_s] The value to look up.
62
- # @return [String, nil] The label corresponding to the value, or nil if not found.
63
- def [](value)
64
- collection[value.to_s]
65
- end
66
-
67
- private
68
-
69
- # @return [Hash<String, String>] The materialized collection as a hash of string value => string label.
70
- def collection
71
- @collection ||= materialize_collection(@raw_collection)
72
- end
73
-
74
- # Converts the raw collection into a materialized hash.
75
- #
76
- # @param collection [#call, #to_a] The collection to be materialized.
77
- # @return [Hash<String, String>] The materialized collection as a hash of string value => string label.
78
- # @raise [ArgumentError] If the collection cannot be materialized into an enumerable.
79
- def materialize_collection(collection)
80
- case collection
81
- in Hash => hash
82
- hash.transform_keys(&:to_s).transform_values(&:to_s)
83
- in Array => arr
84
- array_to_hash(arr)
85
- in Range => range
86
- range_to_hash(range)
87
- in Proc => proc
88
- materialize_collection(proc.call)
89
- in Symbol
90
- raise ArgumentError, "Symbol collections are not supported in this context"
91
- in Set => set
92
- array_to_hash(set.to_a)
93
- else
94
- array_to_hash(Array(collection))
95
- end
96
- rescue ArgumentError
97
- # Rails.logger.warn("Unhandled inclusion collection type: #{e}")
98
- {}
99
- end
100
-
101
- # Converts an array to a hash using detected or specified methods.
102
- #
103
- # @param array [Array] The array to convert.
104
- # @return [Hash<String, String>] The resulting hash of string value => string label.
105
- def array_to_hash(array)
106
- sample = array.first || array.last
107
- methods = detect_methods_for_sample(sample)
108
-
109
- array.each_with_object({}) do |item, hash|
110
- value = item.public_send(methods[:value]).to_s
111
- label = item.public_send(methods[:label]).to_s
112
- hash[value] = label
113
- end
114
- end
115
-
116
- # Converts a range to a hash.
117
- #
118
- # @param range [Range] The range to convert.
119
- # @return [Hash<String, String>] The range converted to a hash of string value => string label.
120
- # @raise [ArgumentError] If the range is unbounded.
121
- def range_to_hash(range)
122
- raise ArgumentError, "Cannot safely materialize an unbounded range" if range.begin.nil? || range.end.nil?
123
-
124
- range.each_with_object({}) { |value, hash| hash[value.to_s] = value.to_s }
125
- end
126
-
127
- # Detects suitable methods for label and value from a sample object.
128
- #
129
- # @param sample [Object] A sample object from the collection.
130
- # @return [Hash{Symbol => Symbol}] A hash containing :label and :value keys with corresponding method names.
131
- def detect_methods_for_sample(sample)
132
- case sample
133
- when Array
134
- {value: :last, label: :first}
135
- else
136
- {
137
- value: @value_method || collection_value_methods.find { |m| sample.respond_to?(m) },
138
- label: @label_method || collection_label_methods.find { |m| sample.respond_to?(m) }
139
- }
140
- end
141
- end
142
-
143
- # @return [Array<Symbol>] An array of method names to try for collection values.
144
- def collection_value_methods
145
- @collection_value_methods ||= %i[id to_s].freeze
146
- end
147
-
148
- # @return [Array<Symbol>] An array of method names to try for collection labels.
149
- def collection_label_methods
150
- @collection_label_methods ||= %i[to_label name title to_s].freeze
151
- end
152
- end
153
- end
154
- end
@@ -1,62 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Display
5
- module Structure
6
- # Generates DOM IDs, names, etc. for a Field, Namespace, or Node based on
7
- # norms that were established by Rails. These can be used outsidef or Rails in
8
- # other Ruby web frameworks since it has now dependencies on Rails.
9
- class DOM
10
- def initialize(field:)
11
- @field = field
12
- end
13
-
14
- # Converts the value of the field to a String, which is required to work
15
- # with Phlex. Assumes that `Object#to_s` emits a format suitable for the web form.
16
- def value
17
- @field.value.to_s
18
- end
19
-
20
- # Walks from the current node to the parent node, grabs the names, and seperates
21
- # them with a `_` for a DOM ID.
22
- def id
23
- @id ||= begin
24
- root, *rest = lineage
25
- root_key = root.respond_to?(:dom_id) ? root.dom_id : root.key
26
- rest.map(&:key).unshift(root_key).join("_")
27
- end
28
- end
29
-
30
- # The `name` attribute of a node, which is influenced by Rails.
31
- # All node names, except the parent node, are wrapped in a `[]` and collections
32
- # are left empty. For example, `user[addresses][][street]` would be created for a form with
33
- # data shaped like `{user: {addresses: [{street: "Sesame Street"}]}}`.
34
- def name
35
- @name ||= begin
36
- root, *names = keys
37
- names.map { |name| "[#{name}]" }.unshift(root).join
38
- end
39
- end
40
-
41
- # One-liner way of walking from the current node all the way up to the parent.
42
- def lineage
43
- @lineage ||= Enumerator.produce(@field, &:parent).take_while(&:itself).reverse
44
- end
45
-
46
- # Emit the id, name, and value in an HTML tag-ish that doesnt have an element.
47
- def inspect
48
- "<#{self.class.name} id=#{id.inspect} name=#{name.inspect} value=#{value.inspect}/>"
49
- end
50
-
51
- private
52
-
53
- def keys
54
- @keys ||= lineage.map do |node|
55
- # If the parent of a field is a field, the name should be nil.
56
- node.key unless node.parent.is_a? FieldBuilder
57
- end
58
- end
59
- end
60
- end
61
- end
62
- end
@@ -1,236 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "phlex"
4
-
5
- module Phlexi
6
- module Display
7
- module Structure
8
- # FieldBuilder class is responsible for building display 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.
15
- class FieldBuilder < Node
16
- include Phlex::Helpers
17
- include FieldOptions::Associations
18
- include FieldOptions::Themes
19
- include FieldOptions::Validators
20
- include FieldOptions::Labels
21
- include FieldOptions::Hints
22
- include FieldOptions::Errors
23
- include FieldOptions::InferredTypes
24
- include FieldOptions::Collection
25
- include FieldOptions::Placeholder
26
- include FieldOptions::Required
27
- include FieldOptions::Autofocus
28
- include FieldOptions::Disabled
29
- include FieldOptions::Readonly
30
- include FieldOptions::Length
31
- include FieldOptions::MinMax
32
- include FieldOptions::Pattern
33
- include FieldOptions::Multiple
34
- include FieldOptions::Limit
35
-
36
- attr_reader :dom, :options, :object, :input_attributes, :value
37
-
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)
47
- super(key, parent: parent)
48
-
49
- @object = object
50
- @value = determine_initial_value(value)
51
- @input_attributes = input_attributes
52
- @options = options
53
- @dom = Structure::DOM.new(field: self)
54
- end
55
-
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(**attributes, &)
61
- create_component(Components::Label, :label, **attributes, &)
62
- end
63
-
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(**attributes, &)
69
- create_component(Components::Input, :input, **attributes, &)
70
- end
71
-
72
- def file_input_tag(**attributes, &)
73
- create_component(Components::FileInput, :file, **attributes, &)
74
- end
75
-
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(**attributes, &)
81
- create_component(Components::Checkbox, :checkbox, **attributes, &)
82
- end
83
-
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(**attributes, &)
90
- create_component(Components::CollectionCheckboxes, :collection_checkboxes, **attributes, &)
91
- end
92
-
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(**attributes, &)
98
- create_component(Components::RadioButton, :radio, **attributes, &)
99
- end
100
-
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(**attributes, &)
107
- create_component(Components::CollectionRadioButtons, :collection_radio_buttons, **attributes, &)
108
- end
109
-
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(**attributes, &)
115
- create_component(Components::Textarea, :textarea, **attributes, &)
116
- end
117
-
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(**attributes, &)
123
- create_component(Phlex::UI::Select, :select, **attributes, &)
124
- end
125
-
126
- def input_array_tag(**attributes, &)
127
- create_component(Components::InputArray, :array, **attributes, &)
128
- end
129
-
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(**attributes, &)
135
- create_component(Components::Hint, :hint, **attributes, &)
136
- end
137
-
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(**attributes, &)
143
- create_component(Components::Error, :error, **attributes, &)
144
- end
145
-
146
- # Creates a full error tag for the field.
147
- #
148
- # @param attributes [Hash] Additional attributes for the full error.
149
- # @return [Components::FullError] The full error component.
150
- def full_error_tag(**attributes, &)
151
- create_component(Components::FullError, :full_error, **attributes, &)
152
- end
153
-
154
- # Wraps the field with additional markup.
155
- #
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
- wrapper_class = attributes.delete(:class) || themed(attributes.delete(:theme) || :wrapper)
162
- inner[:class] = inner.delete(:class) || themed(inner.delete(:theme) || :inner_wrapper)
163
- Components::Wrapper.new(self, class: wrapper_class, inner: inner, **attributes, &)
164
- end
165
-
166
- # Creates a multi-value field collection.
167
- #
168
- # @param range [Integer, #to_a] The range of keys for each field. If an integer is passed, keys will begin from 1.
169
- # @yield [block] The block to be executed for each item in the collection.
170
- # @return [FieldCollection] The field collection.
171
- def multi(range = nil, &)
172
- FieldCollection.new(field: self, range: range, &)
173
- end
174
-
175
- # Creates a submit button
176
- #
177
- # @param attributes [Hash] Additional attributes for the submit.
178
- # @return [Components::SubmitButton] The submit button component.
179
- def submit_button_tag(**attributes, &)
180
- create_component(Components::SubmitButton, :submit_button, **attributes, &)
181
- end
182
-
183
- def extract_input(params)
184
- raise "field##{dom.name} did not define an input component" unless @field_input_component
185
-
186
- @field_input_component.extract_input(params)
187
- end
188
-
189
- protected
190
-
191
- def create_component(component_class, theme_key, **attributes, &)
192
- if component_class.include?(Phlexi::Display::Components::Concerns::HandlesInput)
193
- raise "input component already defined: #{@field_input_component.inspect}" if @field_input_component
194
-
195
- attributes = input_attributes.deep_merge(attributes)
196
- @field_input_component = component_class.new(self, class: component_class_for(theme_key, attributes), **attributes, &)
197
- else
198
- component_class.new(self, class: component_class_for(theme_key, attributes), **attributes, &)
199
- end
200
- end
201
-
202
- def component_class_for(theme_key, attributes)
203
- attributes.delete(:class) || themed(attributes.key?(:theme) ? attributes.delete(:theme) : theme_key)
204
- end
205
-
206
- def has_value?
207
- value.present?
208
- end
209
-
210
- def determine_initial_value(value)
211
- return value unless value == NIL_VALUE
212
-
213
- determine_from_association || determine_value_from_object
214
- end
215
-
216
- def determine_value_from_object
217
- object.respond_to?(key) ? object.public_send(key) : nil
218
- end
219
-
220
- def determine_from_association
221
- return nil unless reflection.present?
222
-
223
- value = object.public_send(key)
224
- case reflection.macro
225
- when :has_many, :has_and_belongs_to_many
226
- value&.map { |v| v.public_send(reflection.klass.primary_key) }
227
- when :belongs_to, :has_one
228
- value&.public_send(reflection.klass.primary_key)
229
- else
230
- raise ArgumentError, "Unsupported association type: #{reflection.macro}"
231
- end
232
- end
233
- end
234
- end
235
- end
236
- end
@@ -1,54 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Display
5
- module Structure
6
- class FieldCollection
7
- include Enumerable
8
-
9
- class Builder
10
- attr_reader :key, :index
11
-
12
- def initialize(key, field, index)
13
- @key = key.to_s
14
- @field = field
15
- @index = index
16
- end
17
-
18
- def field(**)
19
- @field.class.new(key, input_attributes: @field.input_attributes, **, parent: @field).tap do |field|
20
- yield field if block_given?
21
- end
22
- end
23
-
24
- def hidden_field_tag(value: "", force: false)
25
- raise "Attempting to build hidden field on non-first field in a collection" unless index == 0 || force
26
-
27
- @field.class
28
- .new("hidden", parent: @field)
29
- .input_tag(type: :hidden, value:)
30
- end
31
- end
32
-
33
- def initialize(field:, range:, &)
34
- @field = field
35
- @range = case range
36
- when Range, Array
37
- range
38
- when Integer
39
- 1..range
40
- else
41
- range.to_a
42
- end
43
- each(&) if block_given?
44
- end
45
-
46
- def each(&)
47
- @range.each.with_index do |key, index|
48
- yield Builder.new(key, @field, index)
49
- end
50
- end
51
- end
52
- end
53
- end
54
- end
@@ -1,135 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Display
5
- module Structure
6
- # A Namespace maps and object to values, but doesn't actually have a value itself. For
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
- #
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.
13
- class Namespace < Structure::Node
14
- include Enumerable
15
-
16
- attr_reader :builder_klass, :object
17
-
18
- def initialize(key, parent:, builder_klass:, object: nil)
19
- super(key, parent: parent)
20
- @builder_klass = builder_klass
21
- @object = object
22
- @children = {}
23
- yield self if block_given?
24
- end
25
-
26
- def field(key, **attributes)
27
- create_child(key, attributes.delete(:builder_klass) || builder_klass, object: object, **attributes).tap do |field|
28
- yield field if block_given?
29
- end
30
- end
31
-
32
- def submit_button(key = nil, **attributes, &)
33
- field(key || SecureRandom.hex).submit_button_tag(**attributes, &)
34
- end
35
-
36
- # Creates a `Namespace` child instance with the parent set to the current instance, adds to
37
- # the `@children` Hash to ensure duplicate child namespaces aren't created, then calls the
38
- # method on the `@object` to get the child object to pass into that namespace.
39
- #
40
- # For example, if a `User#permission` returns a `Permission` object, we could map that to a
41
- # form like this:
42
- #
43
- # ```ruby
44
- # Superform :user, object: User.new do |form|
45
- # form.nest_one :permission do |permission|
46
- # form.field :role
47
- # end
48
- # end
49
- # ```
50
- def nest_one(key, object: nil, &)
51
- object ||= object_value_for(key: key)
52
- create_child(key, self.class, object:, builder_klass:, &)
53
- end
54
-
55
- # Wraps an array of objects in Namespace classes. For example, if `User#addresses` returns
56
- # an enumerable or array of `Address` classes:
57
- #
58
- # ```ruby
59
- # Phlexi::Display.new User.new do |form|
60
- # render form.field(:email).input_tag
61
- # render form.field(:name).input_tag
62
- # form.nest_many :addresses do |address|
63
- # render address.field(:street).input_tag
64
- # render address.field(:state).input_tag
65
- # render address.field(:zip).input_tag
66
- # end
67
- # end
68
- # ```
69
- # The object within the block is a `Namespace` object that maps each object within the enumerable
70
- # to another `Namespace` or `Field`.
71
- def nest_many(key, collection: nil, &)
72
- collection ||= Array(object_value_for(key: key))
73
- create_child(key, NamespaceCollection, collection:, &)
74
- end
75
-
76
- def extract_input(params)
77
- if params.is_a?(Array)
78
- each_with_object({}) do |child, hash|
79
- hash.merge! child.extract_input(params[0])
80
- end
81
- else
82
- input = each_with_object({}) do |child, hash|
83
- hash.merge! child.extract_input(params[key])
84
- end
85
- {key => input}
86
- end
87
- end
88
-
89
- # Iterates through the children of the current namespace, which could be `Namespace` or `Field`
90
- # objects.
91
- def each(&)
92
- @children.values.each(&)
93
- end
94
-
95
- def dom_id
96
- @dom_id ||= begin
97
- id = if object.nil?
98
- nil
99
- elsif object.class.respond_to?(:primary_key)
100
- object.public_send(object.class.primary_key) || :new
101
- elsif object.respond_to?(:id)
102
- object.id || :new
103
- end
104
- [key, id].compact.join("_").underscore
105
- end
106
- end
107
-
108
- # Creates a root Namespace, which is essentially a form.
109
- def self.root(*, builder_klass:, **, &)
110
- new(*, parent: nil, builder_klass:, **, &)
111
- end
112
-
113
- protected
114
-
115
- # Calls the corresponding method on the object for the `key` name, if it exists. For example
116
- # if the `key` is `email` on `User`, this method would call `User#email` if the method is
117
- # present.
118
- #
119
- # This method could be overwritten if the mapping between the `@object` and `key` name is not
120
- # a method call. For example, a `Hash` would be accessed via `user[:email]` instead of `user.send(:email)`
121
- def object_value_for(key:)
122
- @object.send(key) if @object.respond_to? key
123
- end
124
-
125
- private
126
-
127
- # Checks if the child exists. If it does then it returns that. If it doesn't, it will
128
- # build the child.
129
- def create_child(key, child_class, **kwargs, &block)
130
- @children.fetch(key) { @children[key] = child_class.new(key, parent: self, **kwargs, &block) }
131
- end
132
- end
133
- end
134
- end
135
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Display
5
- module Structure
6
- class NamespaceCollection < Node
7
- include Enumerable
8
-
9
- def initialize(key, parent:, collection: nil, &block)
10
- raise ArgumentError, "block is required" unless block.present?
11
-
12
- super(key, parent: parent)
13
-
14
- @collection = collection
15
- @block = block
16
- each(&block)
17
- end
18
-
19
- def extract_input(params)
20
- namespace = build_namespace(0)
21
- @block.call(namespace)
22
-
23
- inputs = params[key].map { |param| namespace.extract_input([param]) }
24
- {key => inputs}
25
- end
26
-
27
- private
28
-
29
- def each(&)
30
- namespaces.each(&)
31
- end
32
-
33
- # Builds and memoizes namespaces for the collection.
34
- #
35
- # @return [Array<Hash>] An array of namespace hashes.
36
- def namespaces
37
- @namespaces ||= @collection.map.with_index do |object, key|
38
- build_namespace(key, object: object)
39
- end
40
- end
41
-
42
- def build_namespace(index, **)
43
- parent.class.new(index, parent: self, builder_klass: parent.builder_klass, **)
44
- end
45
- end
46
- end
47
- end
48
- end