superform 0.5.0 → 0.5.1
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/Gemfile.lock +7 -6
- data/README.md +21 -16
- data/lib/superform/dom.rb +51 -0
- data/lib/superform/field.rb +39 -0
- data/lib/superform/field_collection.rb +33 -0
- data/lib/superform/namespace.rb +124 -0
- data/lib/superform/namespace_collection.rb +48 -0
- data/lib/superform/node.rb +12 -0
- data/lib/superform/version.rb +1 -1
- data/lib/superform.rb +3 -294
- metadata +17 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b9c458186315ee97fe98e0b3c304ca674967ee6e2c9f797aadec175a629fa3ef
|
4
|
+
data.tar.gz: 65a76515d2f07965a9c5beff64861ef1ea02b46f6ee498304cf665a36990ecb9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9150a51203dd7bc4ca7dcdd3b629cde2c5b0eaf37446c0ffd9a9697201de0af24b041c57c3e4ecf268d6f40f1c9f424760b058bcd31480d4af515f9e39d07de9
|
7
|
+
data.tar.gz: 45c5794a688e16daa4818068136163bfa68ae86ba61e1797960c8d2eff3d8cb88d5cbd69752569163ffc183a75e944ddcade6cabf49a3992734457b94752b70d
|
data/Gemfile.lock
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
superform (0.5.
|
5
|
-
phlex-rails (
|
4
|
+
superform (0.5.1)
|
5
|
+
phlex-rails (>= 1.0, < 3.0)
|
6
6
|
zeitwerk (~> 2.6)
|
7
7
|
|
8
8
|
GEM
|
@@ -46,8 +46,8 @@ GEM
|
|
46
46
|
diff-lcs (1.5.1)
|
47
47
|
drb (2.2.1)
|
48
48
|
erubi (1.13.0)
|
49
|
-
ffi (1.17.
|
50
|
-
ffi (1.17.
|
49
|
+
ffi (1.17.1-arm64-darwin)
|
50
|
+
ffi (1.17.1-x86_64-linux-gnu)
|
51
51
|
formatador (1.1.0)
|
52
52
|
guard (2.18.1)
|
53
53
|
formatador (>= 0.2.4)
|
@@ -80,9 +80,9 @@ GEM
|
|
80
80
|
method_source (1.1.0)
|
81
81
|
minitest (5.25.1)
|
82
82
|
nenv (0.3.0)
|
83
|
-
nokogiri (1.
|
83
|
+
nokogiri (1.18.3-arm64-darwin)
|
84
84
|
racc (~> 1.4)
|
85
|
-
nokogiri (1.
|
85
|
+
nokogiri (1.18.3-x86_64-linux-gnu)
|
86
86
|
racc (~> 1.4)
|
87
87
|
notiffany (0.1.3)
|
88
88
|
nenv (~> 0.1)
|
@@ -154,6 +154,7 @@ GEM
|
|
154
154
|
PLATFORMS
|
155
155
|
arm64-darwin-22
|
156
156
|
arm64-darwin-23
|
157
|
+
arm64-darwin-24
|
157
158
|
x86_64-linux
|
158
159
|
|
159
160
|
DEPENDENCIES
|
data/README.md
CHANGED
@@ -33,10 +33,10 @@ After installing, create a form in `app/views/*/form.rb`. For example, a form fo
|
|
33
33
|
```ruby
|
34
34
|
# ./app/views/posts/form.rb
|
35
35
|
class Posts::Form < ApplicationForm
|
36
|
-
def
|
37
|
-
|
38
|
-
|
39
|
-
|
36
|
+
def view_template(&)
|
37
|
+
labeled field(:title).input
|
38
|
+
labeled field(:body).textarea
|
39
|
+
labeled field(:blog).select Blog.select(:id, :title)
|
40
40
|
end
|
41
41
|
end
|
42
42
|
```
|
@@ -56,19 +56,22 @@ Superforms are built out of [Phlex components](https://www.phlex.fun/html/compon
|
|
56
56
|
# ./app/views/forms/application_form.rb
|
57
57
|
class ApplicationForm < Superform::Rails::Form
|
58
58
|
class MyInputComponent < Superform::Rails::Components::InputComponent
|
59
|
-
def
|
59
|
+
def view_template(&)
|
60
60
|
div class: "form-field" do
|
61
61
|
input(**attributes)
|
62
62
|
end
|
63
63
|
end
|
64
64
|
end
|
65
65
|
|
66
|
+
# Redefining the base Field class lets us override every field component.
|
66
67
|
class Field < Superform::Rails::Form::Field
|
67
68
|
def input(**attributes)
|
68
69
|
MyInputComponent.new(self, attributes: attributes)
|
69
70
|
end
|
70
71
|
end
|
71
72
|
|
73
|
+
# Here we make a simple helper to make our syntax shorter. Given a field it
|
74
|
+
# will also render its label.
|
72
75
|
def labeled(component)
|
73
76
|
div class: "form-row" do
|
74
77
|
render component.field.label
|
@@ -87,7 +90,7 @@ That looks like a LOT of code, and it is, but look at how easy it is to create f
|
|
87
90
|
```ruby
|
88
91
|
# ./app/views/users/form.rb
|
89
92
|
class Users::Form < ApplicationForm
|
90
|
-
def
|
93
|
+
def view_template(&)
|
91
94
|
labeled field(:name).input
|
92
95
|
labeled field(:email).input(type: :email)
|
93
96
|
|
@@ -112,7 +115,7 @@ Consider a form for an account that lets people edit the names and email of the
|
|
112
115
|
|
113
116
|
```ruby
|
114
117
|
class AccountForm < Superform::Rails::Form
|
115
|
-
def
|
118
|
+
def view_template
|
116
119
|
# Account#owner returns a single object
|
117
120
|
namespace :owner do |owner|
|
118
121
|
# Renders input with the name `account[owner][name]`
|
@@ -161,7 +164,7 @@ By default Superform namespaces a form based on the ActiveModel model name param
|
|
161
164
|
|
162
165
|
```ruby
|
163
166
|
class UserForm < Superform::Rails::Form
|
164
|
-
def
|
167
|
+
def view_template
|
165
168
|
render field(:email).input
|
166
169
|
end
|
167
170
|
end
|
@@ -177,7 +180,7 @@ To customize the form namespace, like an ActiveRecord model nested within a modu
|
|
177
180
|
|
178
181
|
```ruby
|
179
182
|
class UserForm < Superform::Rails::Form
|
180
|
-
def
|
183
|
+
def view_template
|
181
184
|
render field(:email).input
|
182
185
|
end
|
183
186
|
|
@@ -202,7 +205,7 @@ In practice, many of the calls below you'd put inside of a method. This cuts dow
|
|
202
205
|
```ruby
|
203
206
|
# Everything below is intentionally verbose!
|
204
207
|
class SignupForm < ApplicationForm
|
205
|
-
def
|
208
|
+
def view_template
|
206
209
|
# The most basic type of input, which will be autofocused.
|
207
210
|
render field(:name).input.focus
|
208
211
|
|
@@ -210,10 +213,10 @@ class SignupForm < ApplicationForm
|
|
210
213
|
render field(:email).input(type: :email, placeholder: "We will sell this to third parties", required: true)
|
211
214
|
|
212
215
|
# You can put fields in a block if that's your thing.
|
213
|
-
|
216
|
+
field(:reason) do |f|
|
214
217
|
div do
|
215
|
-
f.label { "Why should we care about you?" }
|
216
|
-
f.textarea(row: 3, col: 80)
|
218
|
+
render f.label { "Why should we care about you?" }
|
219
|
+
render f.textarea(row: 3, col: 80)
|
217
220
|
end
|
218
221
|
end
|
219
222
|
|
@@ -231,6 +234,8 @@ class SignupForm < ApplicationForm
|
|
231
234
|
div do
|
232
235
|
render field(:source).label { "How did you hear about us?" }
|
233
236
|
render field(:source).select do |s|
|
237
|
+
# Renders a blank option.
|
238
|
+
s.blank_option
|
234
239
|
# Pretend WebSources is an ActiveRecord scope with a "Social" category that has "Facebook, X, etc"
|
235
240
|
# and a "Search" category with "AltaVista, Yahoo, etc."
|
236
241
|
WebSources.select(:id, :name).group_by(:category) do |category, sources|
|
@@ -256,7 +261,7 @@ If you want to add file upload fields to your form you will need to initialize y
|
|
256
261
|
|
257
262
|
```ruby
|
258
263
|
class User::ImageForm < ApplicationForm
|
259
|
-
def
|
264
|
+
def view_template
|
260
265
|
# render label
|
261
266
|
render field(:image).label { "Choose file" }
|
262
267
|
# render file input with accept attribute for png and jpeg images
|
@@ -277,7 +282,7 @@ The best part? If you have forms with a completely different look and feel, you
|
|
277
282
|
```ruby
|
278
283
|
class AdminForm < ApplicationForm
|
279
284
|
class AdminInput < ApplicationComponent
|
280
|
-
def
|
285
|
+
def view_template(&)
|
281
286
|
input(**attributes)
|
282
287
|
small { admin_tool_tip_for field.key }
|
283
288
|
end
|
@@ -295,7 +300,7 @@ Then, just like you did in your Erb, you create the form:
|
|
295
300
|
|
296
301
|
```ruby
|
297
302
|
class Admin::Users::Form < AdminForm
|
298
|
-
def
|
303
|
+
def view_template(&)
|
299
304
|
labeled field(:name).tooltip_input
|
300
305
|
labeled field(:email).tooltip_input(type: :email)
|
301
306
|
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Superform
|
2
|
+
# Generates DOM IDs, names, etc. for a Field, Namespace, or Node based on
|
3
|
+
# norms that were established by Rails. These can be used outsidef or Rails in
|
4
|
+
# other Ruby web frameworks since it has now dependencies on Rails.
|
5
|
+
class DOM
|
6
|
+
def initialize(field:)
|
7
|
+
@field = field
|
8
|
+
end
|
9
|
+
|
10
|
+
# Converts the value of the field to a String, which is required to work
|
11
|
+
# with Phlex. Assumes that `Object#to_s` emits a format suitable for the web form.
|
12
|
+
def value
|
13
|
+
@field.value.to_s
|
14
|
+
end
|
15
|
+
|
16
|
+
# Walks from the current node to the parent node, grabs the names, and seperates
|
17
|
+
# them with a `_` for a DOM ID. One limitation of this approach is if multiple forms
|
18
|
+
# exist on the same page, the ID may be duplicate.
|
19
|
+
def id
|
20
|
+
lineage.map(&:key).join("_")
|
21
|
+
end
|
22
|
+
|
23
|
+
# The `name` attribute of a node, which is influenced by Rails (not sure where Rails got
|
24
|
+
# it from). All node names, except the parent node, are wrapped in a `[]` and collections
|
25
|
+
# are left empty. For example, `user[addresses][][street]` would be created for a form with
|
26
|
+
# data shaped like `{user: {addresses: [{street: "Sesame Street"}]}}`.
|
27
|
+
def name
|
28
|
+
root, *names = keys
|
29
|
+
names.map { |name| "[#{name}]" }.unshift(root).join
|
30
|
+
end
|
31
|
+
|
32
|
+
# Emit the id, name, and value in an HTML tag-ish that doesnt have an element.
|
33
|
+
def inspect
|
34
|
+
"<id=#{id.inspect} name=#{name.inspect} value=#{value.inspect}/>"
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def keys
|
40
|
+
lineage.map do |node|
|
41
|
+
# If the parent of a field is a field, the name should be nil.
|
42
|
+
node.key unless node.parent.is_a? Field
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# One-liner way of walking from the current node all the way up to the parent.
|
47
|
+
def lineage
|
48
|
+
Enumerator.produce(@field, &:parent).take_while(&:itself).reverse
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Superform
|
2
|
+
# A Field represents the data associated with a form element. This class provides
|
3
|
+
# methods for accessing and modifying the field's value. HTML concerns are all
|
4
|
+
# delegated to the DOM object.
|
5
|
+
class Field < Node
|
6
|
+
attr_reader :dom
|
7
|
+
|
8
|
+
def initialize(key, parent:, object: nil, value: nil)
|
9
|
+
super key, parent: parent
|
10
|
+
@object = object
|
11
|
+
@value = value
|
12
|
+
@dom = Superform::DOM.new(field: self)
|
13
|
+
end
|
14
|
+
|
15
|
+
def value
|
16
|
+
if @object and @object.respond_to? @key
|
17
|
+
@object.send @key
|
18
|
+
else
|
19
|
+
@value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
alias :serialize :value
|
23
|
+
|
24
|
+
def assign(value)
|
25
|
+
if @object and @object.respond_to? "#{@key}="
|
26
|
+
@object.send "#{@key}=", value
|
27
|
+
else
|
28
|
+
@value = value
|
29
|
+
end
|
30
|
+
end
|
31
|
+
alias :value= :assign
|
32
|
+
|
33
|
+
# Wraps a field that's an array of values with a bunch of fields
|
34
|
+
# that are indexed with the array's index.
|
35
|
+
def collection(&)
|
36
|
+
@collection ||= FieldCollection.new(field: self, &)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Superform
|
2
|
+
# A FieldCollection represents values that are collections of literals. For example, a Note
|
3
|
+
# ActiveRecord object might have a collection of tags that's an array of string literals.
|
4
|
+
class FieldCollection
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def initialize(field:, &)
|
8
|
+
@field = field
|
9
|
+
@index = 0
|
10
|
+
each(&) if block_given?
|
11
|
+
end
|
12
|
+
|
13
|
+
def each(&)
|
14
|
+
values.each do |value|
|
15
|
+
yield build_field(value: value)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def field
|
20
|
+
build_field
|
21
|
+
end
|
22
|
+
|
23
|
+
def values
|
24
|
+
Array(@field.value)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def build_field(**)
|
30
|
+
@field.class.new(@index += 1, parent: @field, **)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module Superform
|
2
|
+
# A Namespace maps and object to values, but doesn't actually have a value itself. For
|
3
|
+
# example, a `User` object or ActiveRecord model could be passed into the `:user` namespace.
|
4
|
+
# To access the values on a Namespace, the `field` can be called for single values.
|
5
|
+
#
|
6
|
+
# Additionally, to access namespaces within a namespace, such as if a `User has_many :addresses` in
|
7
|
+
# ActiveRecord, the `namespace` method can be called which will return another Namespace object and
|
8
|
+
# set the current Namespace as the parent.
|
9
|
+
class Namespace < Node
|
10
|
+
include Enumerable
|
11
|
+
|
12
|
+
attr_reader :object
|
13
|
+
|
14
|
+
def initialize(key, parent:, object: nil, field_class: Field)
|
15
|
+
super(key, parent: parent)
|
16
|
+
@object = object
|
17
|
+
@field_class = field_class
|
18
|
+
@children = Hash.new
|
19
|
+
yield self if block_given?
|
20
|
+
end
|
21
|
+
|
22
|
+
# Creates a `Namespace` child instance with the parent set to the current instance, adds to
|
23
|
+
# the `@children` Hash to ensure duplicate child namespaces aren't created, then calls the
|
24
|
+
# method on the `@object` to get the child object to pass into that namespace.
|
25
|
+
#
|
26
|
+
# For example, if a `User#permission` returns a `Permission` object, we could map that to a
|
27
|
+
# form like this:
|
28
|
+
#
|
29
|
+
# ```ruby
|
30
|
+
# Superform :user, object: User.new do |form|
|
31
|
+
# form.namespace :permission do |permission|
|
32
|
+
# form.field :role
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
# ```
|
36
|
+
def namespace(key, &block)
|
37
|
+
create_child(key, self.class, object: object_for(key: key), &block)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Maps the `Object#proprety` and `Object#property=` to a field in a web form that can be
|
41
|
+
# read and set by the form. For example, a User form might look like this:
|
42
|
+
#
|
43
|
+
# ```ruby
|
44
|
+
# Superform :user, object: User.new do |form|
|
45
|
+
# form.field :email
|
46
|
+
# form.field :name
|
47
|
+
# end
|
48
|
+
# ```
|
49
|
+
def field(key)
|
50
|
+
create_child(key, @field_class, object: object).tap do |field|
|
51
|
+
yield field if block_given?
|
52
|
+
end
|
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
|
+
# Superform :user, object: User.new do |form|
|
60
|
+
# form.field :email
|
61
|
+
# form.field :name
|
62
|
+
# form.collection :addresses do |address|
|
63
|
+
# address.field(:street)
|
64
|
+
# address.field(:state)
|
65
|
+
# address.field(:zip)
|
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 collection(key, &)
|
72
|
+
create_child(key, NamespaceCollection, &)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Creates a Hash of Hashes and Arrays that represent the fields and collections of the Superform.
|
76
|
+
# This can be used to safely update ActiveRecord objects without the need for Strong Parameters.
|
77
|
+
# You will want to make sure that all the fields displayed in the form are ones that you're OK updating
|
78
|
+
# from the generated hash.
|
79
|
+
def serialize
|
80
|
+
each_with_object Hash.new do |child, hash|
|
81
|
+
hash[child.key] = child.serialize
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Iterates through the children of the current namespace, which could be `Namespace` or `Field`
|
86
|
+
# objects.
|
87
|
+
def each(&)
|
88
|
+
@children.values.each(&)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Assigns a hash to the current namespace and children namespace.
|
92
|
+
def assign(hash)
|
93
|
+
each do |child|
|
94
|
+
child.assign hash[child.key]
|
95
|
+
end
|
96
|
+
self
|
97
|
+
end
|
98
|
+
|
99
|
+
# Creates a root Namespace, which is essentially a form.
|
100
|
+
def self.root(*, **, &)
|
101
|
+
new(*, parent: nil, **, &)
|
102
|
+
end
|
103
|
+
|
104
|
+
protected
|
105
|
+
|
106
|
+
# Calls the corresponding method on the object for the `key` name, if it exists. For example
|
107
|
+
# if the `key` is `email` on `User`, this method would call `User#email` if the method is
|
108
|
+
# present.
|
109
|
+
#
|
110
|
+
# This method could be overwritten if the mapping between the `@object` and `key` name is not
|
111
|
+
# a method call. For example, a `Hash` would be accessed via `user[:email]` instead of `user.send(:email)`
|
112
|
+
def object_for(key:)
|
113
|
+
@object.send(key) if @object.respond_to? key
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
# Checks if the child exists. If it does then it returns that. If it doesn't, it will
|
119
|
+
# build the child.
|
120
|
+
def create_child(key, child_class, **kwargs, &block)
|
121
|
+
@children.fetch(key) { @children[key] = child_class.new(key, parent: self, **kwargs, &block) }
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Superform
|
2
|
+
# A NamespaceCollection represents values that are collections of namespaces. For example, a User
|
3
|
+
# ActiveRecord object might have many Addresses. Each individual address is then delegated out
|
4
|
+
# to a Namespace object.
|
5
|
+
class NamespaceCollection < Node
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
def initialize(key, parent:, &template)
|
9
|
+
super(key, parent: parent)
|
10
|
+
@template = template
|
11
|
+
@namespaces = enumerate(parent_collection)
|
12
|
+
end
|
13
|
+
|
14
|
+
def serialize
|
15
|
+
map(&:serialize)
|
16
|
+
end
|
17
|
+
|
18
|
+
def assign(array)
|
19
|
+
# The problem with zip-ing the array is if I need to add new
|
20
|
+
# elements to it and wrap it in the namespace.
|
21
|
+
zip(array) do |namespace, hash|
|
22
|
+
namespace.assign hash
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def each(&)
|
27
|
+
@namespaces.each(&)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def enumerate(enumerator)
|
33
|
+
Enumerator.new do |y|
|
34
|
+
enumerator.each.with_index do |object, key|
|
35
|
+
y << build_namespace(key, object: object)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def build_namespace(index, **)
|
41
|
+
parent.class.new(index, parent: self, **, &@template)
|
42
|
+
end
|
43
|
+
|
44
|
+
def parent_collection
|
45
|
+
@parent.object.send @key
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Superform
|
2
|
+
# Superclass for Namespace and Field classes. Not much to it other than it has a `name`
|
3
|
+
# and `parent` node attribute. Think of it as a tree.
|
4
|
+
class Node
|
5
|
+
attr_reader :key, :parent
|
6
|
+
|
7
|
+
def initialize(key, parent:)
|
8
|
+
@key = key
|
9
|
+
@parent = parent
|
10
|
+
end
|
11
|
+
end
|
12
|
+
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(...)
|
metadata
CHANGED
@@ -1,29 +1,34 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: superform
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.5.
|
4
|
+
version: 0.5.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brad Gessler
|
8
|
-
autorequire:
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-04-24 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: phlex-rails
|
15
14
|
requirement: !ruby/object:Gem::Requirement
|
16
15
|
requirements:
|
17
|
-
- - "
|
16
|
+
- - ">="
|
18
17
|
- !ruby/object:Gem::Version
|
19
18
|
version: '1.0'
|
19
|
+
- - "<"
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '3.0'
|
20
22
|
type: :runtime
|
21
23
|
prerelease: false
|
22
24
|
version_requirements: !ruby/object:Gem::Requirement
|
23
25
|
requirements:
|
24
|
-
- - "
|
26
|
+
- - ">="
|
25
27
|
- !ruby/object:Gem::Version
|
26
28
|
version: '1.0'
|
29
|
+
- - "<"
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: '3.0'
|
27
32
|
- !ruby/object:Gem::Dependency
|
28
33
|
name: zeitwerk
|
29
34
|
requirement: !ruby/object:Gem::Requirement
|
@@ -58,6 +63,12 @@ files:
|
|
58
63
|
- lib/generators/superform/install/install_generator.rb
|
59
64
|
- lib/generators/superform/install/templates/application_form.rb
|
60
65
|
- lib/superform.rb
|
66
|
+
- lib/superform/dom.rb
|
67
|
+
- lib/superform/field.rb
|
68
|
+
- lib/superform/field_collection.rb
|
69
|
+
- lib/superform/namespace.rb
|
70
|
+
- lib/superform/namespace_collection.rb
|
71
|
+
- lib/superform/node.rb
|
61
72
|
- lib/superform/rails.rb
|
62
73
|
- lib/superform/version.rb
|
63
74
|
- sig/superform.rbs
|
@@ -69,7 +80,6 @@ metadata:
|
|
69
80
|
homepage_uri: https://github.com/rubymonolith/superform
|
70
81
|
source_code_uri: https://github.com/rubymonolith/superform
|
71
82
|
changelog_uri: https://github.com/rubymonolith/superform
|
72
|
-
post_install_message:
|
73
83
|
rdoc_options: []
|
74
84
|
require_paths:
|
75
85
|
- lib
|
@@ -84,8 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
94
|
- !ruby/object:Gem::Version
|
85
95
|
version: '0'
|
86
96
|
requirements: []
|
87
|
-
rubygems_version: 3.
|
88
|
-
signing_key:
|
97
|
+
rubygems_version: 3.6.2
|
89
98
|
specification_version: 4
|
90
99
|
summary: Build forms in Rails
|
91
100
|
test_files: []
|