superform 0.5.0 → 0.6.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/CHANGELOG.md +131 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +131 -29
- data/README.md +200 -66
- data/SPEC_STYLE_GUIDE.md +146 -0
- data/lib/generators/superform/install/USAGE +6 -2
- data/lib/generators/superform/install/install_generator.rb +11 -22
- data/lib/generators/superform/install/templates/base.rb +33 -0
- data/lib/superform/dom.rb +51 -0
- data/lib/superform/field.rb +121 -0
- data/lib/superform/field_collection.rb +33 -0
- data/lib/superform/form.rb +34 -0
- data/lib/superform/namespace.rb +134 -0
- data/lib/superform/namespace_collection.rb +51 -0
- data/lib/superform/node.rb +12 -0
- data/lib/superform/rails/components/base.rb +31 -0
- data/lib/superform/rails/components/button.rb +20 -0
- data/lib/superform/rails/components/checkbox.rb +19 -0
- data/lib/superform/rails/components/field.rb +11 -0
- data/lib/superform/rails/components/input.rb +59 -0
- data/lib/superform/rails/components/label.rb +20 -0
- data/lib/superform/rails/components/select.rb +43 -0
- data/lib/superform/rails/components/textarea.rb +12 -0
- data/lib/superform/rails/form.rb +240 -0
- data/lib/superform/rails/option_mapper.rb +36 -0
- data/lib/superform/rails/strong_parameters.rb +73 -0
- data/lib/superform/rails.rb +17 -368
- data/lib/superform/version.rb +1 -1
- data/lib/superform.rb +3 -294
- metadata +25 -9
- data/lib/generators/superform/install/templates/application_form.rb +0 -31
data/lib/superform/rails.rb
CHANGED
@@ -1,373 +1,22 @@
|
|
1
|
+
require "phlex/rails"
|
2
|
+
|
1
3
|
module Superform
|
2
4
|
module Rails
|
3
|
-
#
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
#
|
13
|
-
|
14
|
-
|
15
|
-
# method and the HTTP verb via the `_method_field`.
|
16
|
-
class Form < Component
|
17
|
-
attr_accessor :model
|
18
|
-
|
19
|
-
delegate \
|
20
|
-
:field,
|
21
|
-
:collection,
|
22
|
-
:namespace,
|
23
|
-
:assign,
|
24
|
-
:serialize,
|
25
|
-
to: :@namespace
|
26
|
-
|
27
|
-
# The Field class is designed to be extended to create custom forms. To override,
|
28
|
-
# in your subclass you may have something like this:
|
29
|
-
#
|
30
|
-
# ```ruby
|
31
|
-
# class MyForm < Superform::Rails::Form
|
32
|
-
# class MyLabel < Superform::Rails::Components::LabelComponent
|
33
|
-
# def view_template(&content)
|
34
|
-
# label(form: @field.dom.name, class: "text-bold", &content)
|
35
|
-
# end
|
36
|
-
# end
|
37
|
-
#
|
38
|
-
# class Field < Field
|
39
|
-
# def label(**attributes)
|
40
|
-
# MyLabel.new(self, **attributes)
|
41
|
-
# end
|
42
|
-
# end
|
43
|
-
# end
|
44
|
-
# ```
|
45
|
-
#
|
46
|
-
# Now all calls to `label` will have the `text-bold` class applied to it.
|
47
|
-
class Field < Superform::Field
|
48
|
-
def button(**attributes)
|
49
|
-
Components::ButtonComponent.new(self, attributes: attributes)
|
50
|
-
end
|
51
|
-
|
52
|
-
def input(**attributes)
|
53
|
-
Components::InputComponent.new(self, attributes: attributes)
|
54
|
-
end
|
55
|
-
|
56
|
-
def checkbox(**attributes)
|
57
|
-
Components::CheckboxComponent.new(self, attributes: attributes)
|
58
|
-
end
|
59
|
-
|
60
|
-
def label(**attributes, &)
|
61
|
-
Components::LabelComponent.new(self, attributes: attributes, &)
|
62
|
-
end
|
63
|
-
|
64
|
-
def textarea(**attributes)
|
65
|
-
Components::TextareaComponent.new(self, attributes: attributes)
|
66
|
-
end
|
67
|
-
|
68
|
-
def select(*collection, **attributes, &)
|
69
|
-
Components::SelectField.new(self, attributes: attributes, collection: collection, &)
|
70
|
-
end
|
71
|
-
|
72
|
-
def title
|
73
|
-
key.to_s.titleize
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
def initialize(model, action: nil, method: nil, **attributes)
|
78
|
-
@model = model
|
79
|
-
@action = action
|
80
|
-
@method = method
|
81
|
-
@attributes = attributes
|
82
|
-
@namespace = Namespace.root(key, object: model, field_class: self.class::Field)
|
83
|
-
end
|
84
|
-
|
85
|
-
def around_template(&)
|
86
|
-
form_tag do
|
87
|
-
authenticity_token_field
|
88
|
-
_method_field
|
89
|
-
super
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
def form_tag(&)
|
94
|
-
form action: form_action, method: form_method, **@attributes, &
|
95
|
-
end
|
96
|
-
|
97
|
-
def view_template(&block)
|
98
|
-
yield_content(&block)
|
99
|
-
end
|
100
|
-
|
101
|
-
def submit(value = submit_value, **attributes)
|
102
|
-
input **attributes.merge(
|
103
|
-
name: "commit",
|
104
|
-
type: "submit",
|
105
|
-
value: value
|
106
|
-
)
|
107
|
-
end
|
108
|
-
|
109
|
-
def key
|
110
|
-
@model.model_name.param_key
|
111
|
-
end
|
112
|
-
|
113
|
-
protected
|
114
|
-
def authenticity_token_field
|
115
|
-
input(
|
116
|
-
name: "authenticity_token",
|
117
|
-
type: "hidden",
|
118
|
-
value: helpers.form_authenticity_token
|
119
|
-
)
|
120
|
-
end
|
121
|
-
|
122
|
-
def _method_field
|
123
|
-
input(
|
124
|
-
name: "_method",
|
125
|
-
type: "hidden",
|
126
|
-
value: _method_field_value
|
127
|
-
)
|
128
|
-
end
|
129
|
-
|
130
|
-
def _method_field_value
|
131
|
-
@method || resource_method_field_value
|
132
|
-
end
|
133
|
-
|
134
|
-
def resource_method_field_value
|
135
|
-
@model.persisted? ? "patch" : "post"
|
136
|
-
end
|
137
|
-
|
138
|
-
def submit_value
|
139
|
-
"#{resource_action.to_s.capitalize} #{@model.model_name}"
|
140
|
-
end
|
141
|
-
|
142
|
-
def resource_action
|
143
|
-
@model.persisted? ? :update : :create
|
144
|
-
end
|
145
|
-
|
146
|
-
def form_action
|
147
|
-
@action ||= helpers.url_for(action: resource_action)
|
148
|
-
end
|
149
|
-
|
150
|
-
def form_method
|
151
|
-
@method.to_s.downcase == "get" ? "get" : "post"
|
152
|
-
end
|
153
|
-
end
|
154
|
-
|
155
|
-
module StrongParameters
|
156
|
-
protected
|
157
|
-
# Assigns params to the form and returns the model.
|
158
|
-
def assign(params, to:)
|
159
|
-
form = to
|
160
|
-
# TODO: Figure out how to render this in a way that doesn't concat a string; just throw everything away.
|
161
|
-
render_to_string form
|
162
|
-
form.assign params
|
163
|
-
form.model
|
164
|
-
end
|
165
|
-
end
|
166
|
-
|
167
|
-
# Accept a collection of objects and map them to options suitable for form controls, like `select > options`
|
168
|
-
class OptionMapper
|
169
|
-
include Enumerable
|
170
|
-
|
171
|
-
def initialize(collection)
|
172
|
-
@collection = collection
|
173
|
-
end
|
174
|
-
|
175
|
-
def each(&options)
|
176
|
-
@collection.each do |object|
|
177
|
-
case object
|
178
|
-
in ActiveRecord::Relation => relation
|
179
|
-
active_record_relation_options_enumerable(relation).each(&options)
|
180
|
-
in id, value
|
181
|
-
options.call id, value
|
182
|
-
in value
|
183
|
-
options.call value, value.to_s
|
184
|
-
end
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
|
-
def active_record_relation_options_enumerable(relation)
|
189
|
-
Enumerator.new do |collection|
|
190
|
-
relation.each do |object|
|
191
|
-
attributes = object.attributes
|
192
|
-
id = attributes.delete(relation.primary_key)
|
193
|
-
value = attributes.values.join(" ")
|
194
|
-
collection << [ id, value ]
|
195
|
-
end
|
196
|
-
end
|
197
|
-
end
|
5
|
+
# When this is included in a Rails app, check if `Components::Base` is defined and
|
6
|
+
# inherit from that so we get all the users stuff; otherwise inherit from Phlex::HTML,
|
7
|
+
# which means we won't have all the users methods and overrides.
|
8
|
+
SUPERCLASSES = [
|
9
|
+
"::Components::Base", # Phlex 2.x base class in a Rails project
|
10
|
+
"::ApplicationComponent", # Phlex 1.x base class in a Rails project
|
11
|
+
"::Phlex::HTML", # Couldn't detect a base Phlex Rails class, so use Phlex::HTML
|
12
|
+
]
|
13
|
+
|
14
|
+
# Find the base class for the Rails app.
|
15
|
+
def self.base_class
|
16
|
+
const_get SUPERCLASSES.find { |const| const_defined?(const) }
|
198
17
|
end
|
199
18
|
|
200
|
-
|
201
|
-
|
202
|
-
attr_reader :field, :dom
|
203
|
-
|
204
|
-
delegate :dom, to: :field
|
205
|
-
|
206
|
-
def initialize(field, attributes: {})
|
207
|
-
@field = field
|
208
|
-
@attributes = attributes
|
209
|
-
end
|
210
|
-
|
211
|
-
def field_attributes
|
212
|
-
{}
|
213
|
-
end
|
214
|
-
|
215
|
-
def focus(value = true)
|
216
|
-
@attributes[:autofocus] = value
|
217
|
-
self
|
218
|
-
end
|
219
|
-
|
220
|
-
private
|
221
|
-
|
222
|
-
def attributes
|
223
|
-
field_attributes.merge(@attributes)
|
224
|
-
end
|
225
|
-
end
|
226
|
-
|
227
|
-
class FieldComponent < BaseComponent
|
228
|
-
def field_attributes
|
229
|
-
{ id: dom.id, name: dom.name }
|
230
|
-
end
|
231
|
-
end
|
232
|
-
|
233
|
-
class LabelComponent < BaseComponent
|
234
|
-
def view_template(&content)
|
235
|
-
content ||= Proc.new { field.key.to_s.titleize }
|
236
|
-
label(**attributes, &content)
|
237
|
-
end
|
238
|
-
|
239
|
-
def field_attributes
|
240
|
-
{ for: dom.id }
|
241
|
-
end
|
242
|
-
end
|
243
|
-
|
244
|
-
class ButtonComponent < FieldComponent
|
245
|
-
def view_template(&content)
|
246
|
-
content ||= Proc.new { button_text }
|
247
|
-
button(**attributes, &content)
|
248
|
-
end
|
249
|
-
|
250
|
-
def button_text
|
251
|
-
@attributes.fetch(:value, dom.value).titleize
|
252
|
-
end
|
253
|
-
|
254
|
-
def field_attributes
|
255
|
-
{ id: dom.id, name: dom.name, value: dom.value }
|
256
|
-
end
|
257
|
-
end
|
258
|
-
|
259
|
-
class CheckboxComponent < FieldComponent
|
260
|
-
def view_template(&)
|
261
|
-
# Rails has a hidden and checkbox input to deal with sending back a value
|
262
|
-
# to the server regardless of if the input is checked or not.
|
263
|
-
input(name: dom.name, type: :hidden, value: "0")
|
264
|
-
# The hard coded keys need to be in here so the user can't overrite them.
|
265
|
-
input(type: :checkbox, value: "1", **attributes)
|
266
|
-
end
|
267
|
-
|
268
|
-
def field_attributes
|
269
|
-
{ id: dom.id, name: dom.name, checked: field.value }
|
270
|
-
end
|
271
|
-
end
|
272
|
-
|
273
|
-
class InputComponent < FieldComponent
|
274
|
-
def view_template(&)
|
275
|
-
input(**attributes)
|
276
|
-
end
|
277
|
-
|
278
|
-
def field_attributes
|
279
|
-
{
|
280
|
-
id: dom.id,
|
281
|
-
name: dom.name,
|
282
|
-
type: type,
|
283
|
-
value: value
|
284
|
-
}
|
285
|
-
end
|
286
|
-
|
287
|
-
def has_client_provided_value?
|
288
|
-
case type.to_s
|
289
|
-
when "file", "image"
|
290
|
-
true
|
291
|
-
else
|
292
|
-
false
|
293
|
-
end
|
294
|
-
end
|
295
|
-
|
296
|
-
def value
|
297
|
-
dom.value unless has_client_provided_value?
|
298
|
-
end
|
299
|
-
|
300
|
-
def type
|
301
|
-
@type ||= ActiveSupport::StringInquirer.new(attribute_type || value_type)
|
302
|
-
end
|
303
|
-
|
304
|
-
protected
|
305
|
-
def value_type
|
306
|
-
case field.value
|
307
|
-
when URI
|
308
|
-
"url"
|
309
|
-
when Integer, Float
|
310
|
-
"number"
|
311
|
-
when Date, DateTime
|
312
|
-
"date"
|
313
|
-
when Time
|
314
|
-
"time"
|
315
|
-
else
|
316
|
-
"text"
|
317
|
-
end
|
318
|
-
end
|
319
|
-
|
320
|
-
def attribute_type
|
321
|
-
if type = @attributes[:type] || @attributes["type"]
|
322
|
-
type.to_s
|
323
|
-
end
|
324
|
-
end
|
325
|
-
end
|
326
|
-
|
327
|
-
class TextareaComponent < FieldComponent
|
328
|
-
def view_template(&content)
|
329
|
-
content ||= Proc.new { dom.value }
|
330
|
-
textarea(**attributes, &content)
|
331
|
-
end
|
332
|
-
end
|
333
|
-
|
334
|
-
class SelectField < FieldComponent
|
335
|
-
def initialize(*, collection: [], **, &)
|
336
|
-
super(*, **, &)
|
337
|
-
@collection = collection
|
338
|
-
end
|
339
|
-
|
340
|
-
def view_template(&options)
|
341
|
-
if block_given?
|
342
|
-
select(**attributes, &options)
|
343
|
-
else
|
344
|
-
select(**attributes) { options(*@collection) }
|
345
|
-
end
|
346
|
-
end
|
347
|
-
|
348
|
-
def options(*collection)
|
349
|
-
map_options(collection).each do |key, value|
|
350
|
-
option(selected: field.value == key, value: key) { value }
|
351
|
-
end
|
352
|
-
end
|
353
|
-
|
354
|
-
def blank_option(&)
|
355
|
-
option(selected: field.value.nil?, &)
|
356
|
-
end
|
357
|
-
|
358
|
-
def true_option(&)
|
359
|
-
option(selected: field.value == true, value: true.to_s, &)
|
360
|
-
end
|
361
|
-
|
362
|
-
def false_option(&)
|
363
|
-
option(selected: field.value == false, value: false.to_s, &)
|
364
|
-
end
|
365
|
-
|
366
|
-
protected
|
367
|
-
def map_options(collection)
|
368
|
-
OptionMapper.new(collection)
|
369
|
-
end
|
370
|
-
end
|
371
|
-
end
|
19
|
+
# Set the base class for the rem
|
20
|
+
Component = base_class
|
372
21
|
end
|
373
|
-
end
|
22
|
+
end
|
data/lib/superform/version.rb
CHANGED
data/lib/superform.rb
CHANGED
@@ -3,304 +3,13 @@ require "zeitwerk"
|
|
3
3
|
module Superform
|
4
4
|
Loader = Zeitwerk::Loader.for_gem.tap do |loader|
|
5
5
|
loader.ignore "#{__dir__}/generators"
|
6
|
+
loader.inflector.inflect(
|
7
|
+
'dom' => 'DOM'
|
8
|
+
)
|
6
9
|
loader.setup
|
7
10
|
end
|
8
11
|
|
9
12
|
class Error < StandardError; end
|
10
|
-
|
11
|
-
# Generates DOM IDs, names, etc. for a Field, Namespace, or Node based on
|
12
|
-
# norms that were established by Rails. These can be used outsidef or Rails in
|
13
|
-
# other Ruby web frameworks since it has now dependencies on Rails.
|
14
|
-
class DOM
|
15
|
-
def initialize(field:)
|
16
|
-
@field = field
|
17
|
-
end
|
18
|
-
|
19
|
-
# Converts the value of the field to a String, which is required to work
|
20
|
-
# with Phlex. Assumes that `Object#to_s` emits a format suitable for the web form.
|
21
|
-
def value
|
22
|
-
@field.value.to_s
|
23
|
-
end
|
24
|
-
|
25
|
-
# Walks from the current node to the parent node, grabs the names, and seperates
|
26
|
-
# them with a `_` for a DOM ID. One limitation of this approach is if multiple forms
|
27
|
-
# exist on the same page, the ID may be duplicate.
|
28
|
-
def id
|
29
|
-
lineage.map(&:key).join("_")
|
30
|
-
end
|
31
|
-
|
32
|
-
# The `name` attribute of a node, which is influenced by Rails (not sure where Rails got
|
33
|
-
# it from). All node names, except the parent node, are wrapped in a `[]` and collections
|
34
|
-
# are left empty. For example, `user[addresses][][street]` would be created for a form with
|
35
|
-
# data shaped like `{user: {addresses: [{street: "Sesame Street"}]}}`.
|
36
|
-
def name
|
37
|
-
root, *names = keys
|
38
|
-
names.map { |name| "[#{name}]" }.unshift(root).join
|
39
|
-
end
|
40
|
-
|
41
|
-
# Emit the id, name, and value in an HTML tag-ish that doesnt have an element.
|
42
|
-
def inspect
|
43
|
-
"<id=#{id.inspect} name=#{name.inspect} value=#{value.inspect}/>"
|
44
|
-
end
|
45
|
-
|
46
|
-
private
|
47
|
-
|
48
|
-
def keys
|
49
|
-
lineage.map do |node|
|
50
|
-
# If the parent of a field is a field, the name should be nil.
|
51
|
-
node.key unless node.parent.is_a? Field
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
# One-liner way of walking from the current node all the way up to the parent.
|
56
|
-
def lineage
|
57
|
-
Enumerator.produce(@field, &:parent).take_while(&:itself).reverse
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
|
62
|
-
# Superclass for Namespace and Field classes. Not much to it other than it has a `name`
|
63
|
-
# and `parent` node attribute. Think of it as a tree.
|
64
|
-
class Node
|
65
|
-
attr_reader :key, :parent
|
66
|
-
|
67
|
-
def initialize(key, parent:)
|
68
|
-
@key = key
|
69
|
-
@parent = parent
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
# A Namespace maps and object to values, but doesn't actually have a value itself. For
|
74
|
-
# example, a `User` object or ActiveRecord model could be passed into the `:user` namespace.
|
75
|
-
# To access the values on a Namespace, the `field` can be called for single values.
|
76
|
-
#
|
77
|
-
# Additionally, to access namespaces within a namespace, such as if a `User has_many :addresses` in
|
78
|
-
# ActiveRecord, the `namespace` method can be called which will return another Namespace object and
|
79
|
-
# set the current Namespace as the parent.
|
80
|
-
class Namespace < Node
|
81
|
-
include Enumerable
|
82
|
-
|
83
|
-
attr_reader :object
|
84
|
-
|
85
|
-
def initialize(key, parent:, object: nil, field_class: Field)
|
86
|
-
super(key, parent: parent)
|
87
|
-
@object = object
|
88
|
-
@field_class = field_class
|
89
|
-
@children = Hash.new
|
90
|
-
yield self if block_given?
|
91
|
-
end
|
92
|
-
|
93
|
-
# Creates a `Namespace` child instance with the parent set to the current instance, adds to
|
94
|
-
# the `@children` Hash to ensure duplicate child namespaces aren't created, then calls the
|
95
|
-
# method on the `@object` to get the child object to pass into that namespace.
|
96
|
-
#
|
97
|
-
# For example, if a `User#permission` returns a `Permission` object, we could map that to a
|
98
|
-
# form like this:
|
99
|
-
#
|
100
|
-
# ```ruby
|
101
|
-
# Superform :user, object: User.new do |form|
|
102
|
-
# form.namespace :permission do |permission|
|
103
|
-
# form.field :role
|
104
|
-
# end
|
105
|
-
# end
|
106
|
-
# ```
|
107
|
-
def namespace(key, &block)
|
108
|
-
create_child(key, self.class, object: object_for(key: key), &block)
|
109
|
-
end
|
110
|
-
|
111
|
-
# Maps the `Object#proprety` and `Object#property=` to a field in a web form that can be
|
112
|
-
# read and set by the form. For example, a User form might look like this:
|
113
|
-
#
|
114
|
-
# ```ruby
|
115
|
-
# Superform :user, object: User.new do |form|
|
116
|
-
# form.field :email
|
117
|
-
# form.field :name
|
118
|
-
# end
|
119
|
-
# ```
|
120
|
-
def field(key)
|
121
|
-
create_child(key, @field_class, object: object).tap do |field|
|
122
|
-
yield field if block_given?
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
# Wraps an array of objects in Namespace classes. For example, if `User#addresses` returns
|
127
|
-
# an enumerable or array of `Address` classes:
|
128
|
-
#
|
129
|
-
# ```ruby
|
130
|
-
# Superform :user, object: User.new do |form|
|
131
|
-
# form.field :email
|
132
|
-
# form.field :name
|
133
|
-
# form.collection :addresses do |address|
|
134
|
-
# address.field(:street)
|
135
|
-
# address.field(:state)
|
136
|
-
# address.field(:zip)
|
137
|
-
# end
|
138
|
-
# end
|
139
|
-
# ```
|
140
|
-
# The object within the block is a `Namespace` object that maps each object within the enumerable
|
141
|
-
# to another `Namespace` or `Field`.
|
142
|
-
def collection(key, &)
|
143
|
-
create_child(key, NamespaceCollection, &)
|
144
|
-
end
|
145
|
-
|
146
|
-
# Creates a Hash of Hashes and Arrays that represent the fields and collections of the Superform.
|
147
|
-
# This can be used to safely update ActiveRecord objects without the need for Strong Parameters.
|
148
|
-
# You will want to make sure that all the fields displayed in the form are ones that you're OK updating
|
149
|
-
# from the generated hash.
|
150
|
-
def serialize
|
151
|
-
each_with_object Hash.new do |child, hash|
|
152
|
-
hash[child.key] = child.serialize
|
153
|
-
end
|
154
|
-
end
|
155
|
-
|
156
|
-
# Iterates through the children of the current namespace, which could be `Namespace` or `Field`
|
157
|
-
# objects.
|
158
|
-
def each(&)
|
159
|
-
@children.values.each(&)
|
160
|
-
end
|
161
|
-
|
162
|
-
# Assigns a hash to the current namespace and children namespace.
|
163
|
-
def assign(hash)
|
164
|
-
each do |child|
|
165
|
-
child.assign hash[child.key]
|
166
|
-
end
|
167
|
-
self
|
168
|
-
end
|
169
|
-
|
170
|
-
# Creates a root Namespace, which is essentially a form.
|
171
|
-
def self.root(*, **, &)
|
172
|
-
new(*, parent: nil, **, &)
|
173
|
-
end
|
174
|
-
|
175
|
-
protected
|
176
|
-
|
177
|
-
# Calls the corresponding method on the object for the `key` name, if it exists. For example
|
178
|
-
# if the `key` is `email` on `User`, this method would call `User#email` if the method is
|
179
|
-
# present.
|
180
|
-
#
|
181
|
-
# This method could be overwritten if the mapping between the `@object` and `key` name is not
|
182
|
-
# a method call. For example, a `Hash` would be accessed via `user[:email]` instead of `user.send(:email)`
|
183
|
-
def object_for(key:)
|
184
|
-
@object.send(key) if @object.respond_to? key
|
185
|
-
end
|
186
|
-
|
187
|
-
private
|
188
|
-
|
189
|
-
# Checks if the child exists. If it does then it returns that. If it doesn't, it will
|
190
|
-
# build the child.
|
191
|
-
def create_child(key, child_class, **kwargs, &block)
|
192
|
-
@children.fetch(key) { @children[key] = child_class.new(key, parent: self, **kwargs, &block) }
|
193
|
-
end
|
194
|
-
end
|
195
|
-
|
196
|
-
class Field < Node
|
197
|
-
attr_reader :dom
|
198
|
-
|
199
|
-
def initialize(key, parent:, object: nil, value: nil)
|
200
|
-
super key, parent: parent
|
201
|
-
@object = object
|
202
|
-
@value = value
|
203
|
-
@dom = DOM.new(field: self)
|
204
|
-
end
|
205
|
-
|
206
|
-
def value
|
207
|
-
if @object and @object.respond_to? @key
|
208
|
-
@object.send @key
|
209
|
-
else
|
210
|
-
@value
|
211
|
-
end
|
212
|
-
end
|
213
|
-
alias :serialize :value
|
214
|
-
|
215
|
-
def assign(value)
|
216
|
-
if @object and @object.respond_to? "#{@key}="
|
217
|
-
@object.send "#{@key}=", value
|
218
|
-
else
|
219
|
-
@value = value
|
220
|
-
end
|
221
|
-
end
|
222
|
-
alias :value= :assign
|
223
|
-
|
224
|
-
# Wraps a field that's an array of values with a bunch of fields
|
225
|
-
# that are indexed with the array's index.
|
226
|
-
def collection(&)
|
227
|
-
@collection ||= FieldCollection.new(field: self, &)
|
228
|
-
end
|
229
|
-
end
|
230
|
-
|
231
|
-
class FieldCollection
|
232
|
-
include Enumerable
|
233
|
-
|
234
|
-
def initialize(field:, &)
|
235
|
-
@field = field
|
236
|
-
@index = 0
|
237
|
-
each(&) if block_given?
|
238
|
-
end
|
239
|
-
|
240
|
-
def each(&)
|
241
|
-
values.each do |value|
|
242
|
-
yield build_field(value: value)
|
243
|
-
end
|
244
|
-
end
|
245
|
-
|
246
|
-
def field
|
247
|
-
build_field
|
248
|
-
end
|
249
|
-
|
250
|
-
def values
|
251
|
-
Array(@field.value)
|
252
|
-
end
|
253
|
-
|
254
|
-
private
|
255
|
-
|
256
|
-
def build_field(**)
|
257
|
-
@field.class.new(@index += 1, parent: @field, **)
|
258
|
-
end
|
259
|
-
end
|
260
|
-
|
261
|
-
class NamespaceCollection < Node
|
262
|
-
include Enumerable
|
263
|
-
|
264
|
-
def initialize(key, parent:, &template)
|
265
|
-
super(key, parent: parent)
|
266
|
-
@template = template
|
267
|
-
@namespaces = enumerate(parent_collection)
|
268
|
-
end
|
269
|
-
|
270
|
-
def serialize
|
271
|
-
map(&:serialize)
|
272
|
-
end
|
273
|
-
|
274
|
-
def assign(array)
|
275
|
-
# The problem with zip-ing the array is if I need to add new
|
276
|
-
# elements to it and wrap it in the namespace.
|
277
|
-
zip(array) do |namespace, hash|
|
278
|
-
namespace.assign hash
|
279
|
-
end
|
280
|
-
end
|
281
|
-
|
282
|
-
def each(&)
|
283
|
-
@namespaces.each(&)
|
284
|
-
end
|
285
|
-
|
286
|
-
private
|
287
|
-
|
288
|
-
def enumerate(enumerator)
|
289
|
-
Enumerator.new do |y|
|
290
|
-
enumerator.each.with_index do |object, key|
|
291
|
-
y << build_namespace(key, object: object)
|
292
|
-
end
|
293
|
-
end
|
294
|
-
end
|
295
|
-
|
296
|
-
def build_namespace(index, **)
|
297
|
-
parent.class.new(index, parent: self, **, &@template)
|
298
|
-
end
|
299
|
-
|
300
|
-
def parent_collection
|
301
|
-
@parent.object.send @key
|
302
|
-
end
|
303
|
-
end
|
304
13
|
end
|
305
14
|
|
306
15
|
def Superform(...)
|