phlexi-form 0.2.0 → 0.3.0
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.
- checksums.yaml +4 -4
- data/Appraisals +4 -9
- data/README.md +117 -316
- data/TODO +4 -0
- data/config.ru +0 -3
- data/gemfiles/default.gemfile.lock +22 -2
- data/gemfiles/rails_7.gemfile +8 -0
- data/gemfiles/rails_7.gemfile.lock +282 -0
- data/lib/phlexi/form/base.rb +65 -56
- data/lib/phlexi/form/components/base.rb +14 -8
- data/lib/phlexi/form/components/checkbox.rb +5 -0
- data/lib/phlexi/form/components/collection_checkboxes.rb +28 -14
- data/lib/phlexi/form/components/collection_radio_buttons.rb +19 -13
- data/lib/phlexi/form/components/concerns/extracts_input.rb +53 -0
- data/lib/phlexi/form/components/concerns/handles_array_input.rb +21 -0
- data/lib/phlexi/form/components/concerns/handles_input.rb +23 -0
- data/lib/phlexi/form/components/concerns/has_options.rb +6 -2
- data/lib/phlexi/form/components/concerns/submits_form.rb +47 -0
- data/lib/phlexi/form/components/error.rb +1 -1
- data/lib/phlexi/form/components/file_input.rb +33 -0
- data/lib/phlexi/form/components/hint.rb +1 -1
- data/lib/phlexi/form/components/input.rb +38 -36
- data/lib/phlexi/form/components/input_array.rb +45 -0
- data/lib/phlexi/form/components/label.rb +2 -1
- data/lib/phlexi/form/components/radio_button.rb +11 -1
- data/lib/phlexi/form/components/select.rb +21 -8
- data/lib/phlexi/form/components/submit_button.rb +41 -0
- data/lib/phlexi/form/components/textarea.rb +2 -3
- data/lib/phlexi/form/field_options/associations.rb +21 -0
- data/lib/phlexi/form/field_options/autofocus.rb +1 -1
- data/lib/phlexi/form/field_options/collection.rb +26 -9
- data/lib/phlexi/form/field_options/errors.rb +17 -3
- data/lib/phlexi/form/field_options/hints.rb +5 -1
- data/lib/phlexi/form/field_options/{type.rb → inferred_types.rb} +21 -17
- data/lib/phlexi/form/field_options/multiple.rb +2 -0
- data/lib/phlexi/form/field_options/required.rb +1 -1
- data/lib/phlexi/form/field_options/themes.rb +207 -0
- data/lib/phlexi/form/field_options/validators.rb +2 -2
- data/lib/phlexi/form/option_mapper.rb +2 -2
- data/lib/phlexi/form/structure/dom.rb +21 -16
- data/lib/phlexi/form/structure/field_builder.rb +165 -121
- data/lib/phlexi/form/structure/field_collection.rb +20 -6
- data/lib/phlexi/form/structure/namespace.rb +48 -31
- data/lib/phlexi/form/structure/namespace_collection.rb +20 -20
- data/lib/phlexi/form/structure/node.rb +13 -3
- data/lib/phlexi/form/version.rb +1 -1
- data/lib/phlexi/form.rb +4 -1
- metadata +32 -7
- 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::
|
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, :
|
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
|
-
|
33
|
-
|
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 =
|
38
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
60
|
-
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
102
|
-
|
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
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
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
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
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
|
-
#
|
146
|
+
# Creates a full error tag for the field.
|
121
147
|
#
|
122
|
-
# @
|
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
|
-
#
|
125
|
-
#
|
126
|
-
#
|
127
|
-
#
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
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
|
-
|
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
|
-
|
143
|
-
|
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
|
147
|
-
|
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
|
-
|
151
|
-
|
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
|
155
|
-
|
156
|
-
|
157
|
-
|
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
|
-
:
|
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
|
-
|
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
|
-
|
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
|
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
|
-
#
|
11
|
-
#
|
12
|
-
#
|
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
|
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
|
-
#
|
41
|
-
#
|
42
|
-
#
|
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 ||=
|
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
|
56
|
-
# render
|
57
|
-
# render
|
58
|
-
#
|
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(
|
77
|
+
collection ||= Array(object_value_for(key: key))
|
69
78
|
create_child(key, NamespaceCollection, collection:, &)
|
70
79
|
end
|
71
80
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
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
|
-
@
|
13
|
-
|
14
|
+
@collection = collection
|
15
|
+
@block = block
|
16
|
+
each(&block)
|
14
17
|
end
|
15
18
|
|
16
|
-
def
|
17
|
-
|
18
|
-
|
19
|
+
def extract_input(params)
|
20
|
+
namespace = build_namespace(0)
|
21
|
+
@block.call(namespace)
|
19
22
|
|
20
|
-
|
21
|
-
|
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
|
-
|
30
|
+
namespaces.each(&)
|
30
31
|
end
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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.
|
7
|
-
#
|
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
|