phlexi-display 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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