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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5525e912faf72d0525d0aa35d7ec2ef22ced3a4e8b8cf7d99483850f4bfe5074
4
- data.tar.gz: f0dfde4e0c1ec94e036addce561fee83039627e65236872922525b361abb34d2
3
+ metadata.gz: b9c458186315ee97fe98e0b3c304ca674967ee6e2c9f797aadec175a629fa3ef
4
+ data.tar.gz: 65a76515d2f07965a9c5beff64861ef1ea02b46f6ee498304cf665a36990ecb9
5
5
  SHA512:
6
- metadata.gz: a513255c99aee3da7992521e0d56db5ac14a7cb1d5ed52ad918124a3722b07adcab9490a35587f5c0386659e8173404e58d541d1cb7a277634c9d2929955bbb9
7
- data.tar.gz: df12f03bb89cd6d76c058ad8100ef9b75bf6dce91dc6c0f146bbddf596502e659279b7c58ff2a9db6641fa12d1ac8eaace4e1096dc604b956501b9e6960d83b7
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.0)
5
- phlex-rails (~> 1.0)
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.0-arm64-darwin)
50
- ffi (1.17.0-x86_64-linux-gnu)
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.16.7-arm64-darwin)
83
+ nokogiri (1.18.3-arm64-darwin)
84
84
  racc (~> 1.4)
85
- nokogiri (1.16.7-x86_64-linux)
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 template(&)
37
- row field(:title).input
38
- row field(:body).textarea
39
- row field(:blog).select Blog.select(:id, :title)
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 template(&)
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 template(&)
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 template
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 template
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 template
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 template
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
- render field(:reason) do |f|
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 template
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 template(&)
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 template(&)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Superform
4
- VERSION = "0.5.0"
4
+ VERSION = "0.5.1"
5
5
  end
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.0
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: 2024-10-15 00:00:00.000000000 Z
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.5.9
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: []