superform 0.4.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 764fb4731b8ae1977a1e05ce0410302e5f29f3c008c4e0326f9b3f852118cb37
4
- data.tar.gz: 6d7887d4b7b3bbf2af8ebfe7e675a155061e4982231d3f52caefd7a5e4c4c85d
3
+ metadata.gz: 5daa39c7a090d229f2175c5214e69ae17c586738ff7b64c9c8c4d7745f52e08f
4
+ data.tar.gz: 7d5b2fa66f1230f5f7efdbdc2a97fef97fb1ad5c069dc1331a685cfd4cee474b
5
5
  SHA512:
6
- metadata.gz: edb3acf775edcaacb269b0aff7200d6bfd22d2c5701eb7dd0c96149bbc515acf33b77f586809ac574520a4330c1ed1acbe304e8c54d60a2b219e28b72179321b
7
- data.tar.gz: e2b69ed000da6eeddeb3a47f664abfe052bbd85ae0e5649d23131b134d5d68acc9032fd4660260207540724c5fa10b85d6c6b4609484849e7c480a9fbd90bae1
6
+ metadata.gz: f8b014e3dceb150d9e17e294794370a967f8bec8630a37219e20c3fbb3d4fbdac89dbd5a4beb91829f4c3e74155d607fee303e71600f16d6d5a8cbb3c100ebc1
7
+ data.tar.gz: debd7cf51bdea3de105412ceece2bea44f074acb713eeffc786ca1f6c56e7323f54cec2b8a12b8b36172e3c3b4b74a071738d2978bee5e15a63bf36692f7dde7
data/Gemfile.lock CHANGED
@@ -1,8 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- superform (0.4.0)
4
+ superform (0.4.1)
5
5
  phlex-rails (~> 1.0)
6
+ zeitwerk (~> 2.6)
6
7
 
7
8
  GEM
8
9
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -36,6 +36,7 @@ class Posts::Form < ApplicationForm
36
36
  def template(&)
37
37
  row field(:title).input
38
38
  row field(:body).textarea
39
+ row field(:blog).select Blog.select(:id, :title)
39
40
  end
40
41
  end
41
42
  ```
@@ -106,6 +107,64 @@ Then render it from Erb.
106
107
 
107
108
  Much better!
108
109
 
110
+ ## Form field guide
111
+
112
+ Superform tries to strike a balance between "being as close to HTML forms as possible" and not requiring a lot of boilerplate to create forms. This example is contrived, but it shows all the different ways you can render a form.
113
+
114
+ In practice, many of the calls below you'd put inside of a method. This cuts down on the number of `render` calls in your HTML code and further reduces boilerplate.
115
+
116
+ ```ruby
117
+ # Everything below is intentionally verbose!
118
+ class SignupForm < ApplicationForm
119
+ def template
120
+ # The most basic type of input, which will be autofocused.
121
+ render field(:name).input.focus
122
+
123
+ # Input field with a lot more options on it.
124
+ render field(:email).input(type: :email, placeholder: "We will sell this to third parties", required: true)
125
+
126
+ # You can put fields in a block if that's your thing.
127
+ render field(:reason) do |f|
128
+ div do
129
+ f.label { "Why should we care about you?" }
130
+ f.textarea(row: 3, col: 80)
131
+ end
132
+ end
133
+
134
+ # Let's get crazy with Selects. They can accept values as simple as 2 element arrays.
135
+ div do
136
+ render field(:contact).label { "Would you like us to spam you to death?" }
137
+ render field(:contact).select(
138
+ [true, "Yes"], # <option value="true">Yes</option>
139
+ [false, "No"], # <option value="false">No</option>
140
+ "Hell no", # <option value="Hell no">Hell no</option>
141
+ nil # <option></option>
142
+ )
143
+ end
144
+
145
+ div do
146
+ render field(:source).label { "How did you hear about us?" }
147
+ render field(:source).select do |s|
148
+ # Pretend WebSources is an ActiveRecord scope with a "Social" category that has "Facebook, X, etc"
149
+ # and a "Search" category with "AltaVista, Yahoo, etc."
150
+ WebSources.select(:id, :name).group_by(:category) do |category, sources|
151
+ s.optgroup(label: category) do
152
+ s.options(sources)
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ div do
159
+ render field(:agreement).label { "Check this box if you agree to give us your first born child" }
160
+ render field(:agreement).input(type: :checkbox, value: true)
161
+ end
162
+
163
+ render button { "Submit" }
164
+ end
165
+ end
166
+ ```
167
+
109
168
  ## Extending Superforms
110
169
 
111
170
  The best part? If you have forms with a completely different look and feel, you can extend the forms just like you would a Ruby class:
@@ -140,11 +199,11 @@ class Admin::Users::Form < AdminForm
140
199
  end
141
200
  ```
142
201
 
143
- Since Superforms are just Ruby objects, you can organize them however you want. You can keep your view component classes embedded in your Superform file if you prefer for everythign to be in one place, keep the forms in the `app/views/forms/*.rb` folder and the components in `app/views/forms/**/*_component.rb`, use Ruby's `include` and `extend` features to modify different form classes, or put them in a gem and share them with an entire organization or open source community. It's just Ruby code!
202
+ Since Superforms are just Ruby objects, you can organize them however you want. You can keep your view component classes embedded in your Superform file if you prefer for everything to be in one place, keep the forms in the `app/views/forms/*.rb` folder and the components in `app/views/forms/**/*_component.rb`, use Ruby's `include` and `extend` features to modify different form classes, or put them in a gem and share them with an entire organization or open source community. It's just Ruby code!
144
203
 
145
204
  ## Automatic strong parameters
146
205
 
147
- Guess what? Superform eliminates then need for Strong Parameters in Rails by assigning the values of the `params` hash _through_ your form via the `assign` method. Here's what it looks like.
206
+ Guess what? Superform eliminates the need for Strong Parameters in Rails by assigning the values of the `params` hash _through_ your form via the `assign` method. Here's what it looks like.
148
207
 
149
208
  ```ruby
150
209
  class PostsController < ApplicationController
@@ -1,6 +1,19 @@
1
1
  module Superform
2
2
  module Rails
3
- class Form < Phlex::HTML
3
+ # The `ApplicationComponent` is the superclass for all components in your application.
4
+ Component = ::ApplicationComponent
5
+
6
+ # A Phlex::HTML view module that accepts a model and sets a `Superform::Namespace`
7
+ # with the `Object#model_name` as the key and maps the object to form fields
8
+ # and namespaces.
9
+ #
10
+ # The `Form::Field` is a class that's meant to be extended so you can customize the `Form` inputs
11
+ # to your applications needs. Defaults for the `input`, `button`, `label`, and `textarea` tags
12
+ # are provided.
13
+ #
14
+ # The `Form` component also handles Rails authenticity tokens via the `authenticity_toklen_field`
15
+ # method and the HTTP verb via the `_method_field`.
16
+ class Form < Component
4
17
  attr_reader :model
5
18
 
6
19
  delegate \
@@ -12,6 +25,26 @@ module Superform
12
25
  :serialize,
13
26
  to: :@namespace
14
27
 
28
+ # The Field class is designed to be extended to create custom forms. To override,
29
+ # in your subclass you may have something like this:
30
+ #
31
+ # ```ruby
32
+ # class MyForm
33
+ # class MyLabel < LabelComponent
34
+ # def template(&content)
35
+ # label(form: @field.dom.name, class: "text-bold", &content)
36
+ # end
37
+ # end
38
+ #
39
+ # class Field < Field
40
+ # def label(**attributes)
41
+ # MyLabel.new(self, attributes: **attributes)
42
+ # end
43
+ # end
44
+ # end
45
+ # ```
46
+ #
47
+ # Now all calls to `label` will have the `text-bold` class applied to it.
15
48
  class Field < Superform::Field
16
49
  def button(**attributes)
17
50
  Components::ButtonComponent.new(self, attributes: attributes)
@@ -29,6 +62,10 @@ module Superform
29
62
  Components::TextareaComponent.new(self, attributes: attributes)
30
63
  end
31
64
 
65
+ def select(*collection, **attributes, &)
66
+ Components::SelectField.new(self, attributes: attributes, collection: collection, &)
67
+ end
68
+
32
69
  def title
33
70
  key.to_s.titleize
34
71
  end
@@ -42,19 +79,23 @@ module Superform
42
79
  end
43
80
 
44
81
  def around_template(&)
45
- form action: form_action, method: form_method do
82
+ form_tag do
46
83
  authenticity_token_field
47
84
  _method_field
48
85
  super
49
86
  end
50
87
  end
51
88
 
89
+ def form_tag(&)
90
+ form action: form_action, method: form_method, &
91
+ end
92
+
52
93
  def template(&block)
53
94
  yield_content(&block)
54
95
  end
55
96
 
56
- def submit(value = submit_value)
57
- input(
97
+ def submit(value = submit_value, **attributes)
98
+ input **attributes.merge(
58
99
  name: "commit",
59
100
  type: "submit",
60
101
  value: value
@@ -62,42 +103,41 @@ module Superform
62
103
  end
63
104
 
64
105
  protected
106
+ def authenticity_token_field
107
+ input(
108
+ name: "authenticity_token",
109
+ type: "hidden",
110
+ value: helpers.form_authenticity_token
111
+ )
112
+ end
65
113
 
66
- def authenticity_token_field
67
- input(
68
- name: "authenticity_token",
69
- type: "hidden",
70
- value: helpers.form_authenticity_token
71
- )
72
- end
73
-
74
- def _method_field
75
- input(
76
- name: "_method",
77
- type: "hidden",
78
- value: _method_field_value
79
- )
80
- end
114
+ def _method_field
115
+ input(
116
+ name: "_method",
117
+ type: "hidden",
118
+ value: _method_field_value
119
+ )
120
+ end
81
121
 
82
- def _method_field_value
83
- @method || @model.persisted? ? "patch" : "post"
84
- end
122
+ def _method_field_value
123
+ @method || @model.persisted? ? "patch" : "post"
124
+ end
85
125
 
86
- def submit_value
87
- "#{resource_action.to_s.capitalize} #{@model.model_name}"
88
- end
126
+ def submit_value
127
+ "#{resource_action.to_s.capitalize} #{@model.model_name}"
128
+ end
89
129
 
90
- def resource_action
91
- @model.persisted? ? :update : :create
92
- end
130
+ def resource_action
131
+ @model.persisted? ? :update : :create
132
+ end
93
133
 
94
- def form_action
95
- @action ||= helpers.url_for(action: resource_action)
96
- end
134
+ def form_action
135
+ @action ||= helpers.url_for(action: resource_action)
136
+ end
97
137
 
98
- def form_method
99
- @method.to_s.downcase == "get" ? "get" : "post"
100
- end
138
+ def form_method
139
+ @method.to_s.downcase == "get" ? "get" : "post"
140
+ end
101
141
  end
102
142
 
103
143
  module StrongParameters
@@ -112,8 +152,41 @@ module Superform
112
152
  end
113
153
  end
114
154
 
155
+ # Accept a collection of objects and map them to options suitable for form controls, like `select > options`
156
+ class OptionMapper
157
+ include Enumerable
158
+
159
+ def initialize(collection)
160
+ @collection = collection
161
+ end
162
+
163
+ def each(&options)
164
+ @collection.each do |object|
165
+ case object
166
+ in ActiveRecord::Relation => relation
167
+ active_record_relation_options_enumerable(relation).each(&options)
168
+ in id, value
169
+ options.call id, value
170
+ in value
171
+ options.call value, value.to_s
172
+ end
173
+ end
174
+ end
175
+
176
+ def active_record_relation_options_enumerable(relation)
177
+ Enumerator.new do |collection|
178
+ relation.each do |object|
179
+ attributes = object.attributes
180
+ id = attributes.delete(relation.primary_key)
181
+ value = attributes.values.join(" ")
182
+ collection << [ id, value ]
183
+ end
184
+ end
185
+ end
186
+ end
187
+
115
188
  module Components
116
- class FieldComponent < ApplicationComponent
189
+ class BaseComponent < Component
117
190
  attr_reader :field, :dom
118
191
 
119
192
  delegate :dom, to: :field
@@ -139,9 +212,16 @@ module Superform
139
212
  end
140
213
  end
141
214
 
142
- class LabelComponent < FieldComponent
143
- def template(&)
144
- label(**attributes) { field.key.to_s.titleize }
215
+ class FieldComponent < BaseComponent
216
+ def field_attributes
217
+ { id: dom.id, name: dom.name }
218
+ end
219
+ end
220
+
221
+ class LabelComponent < BaseComponent
222
+ def template(&content)
223
+ content ||= Proc.new { field.key.to_s.titleize }
224
+ label(**attributes, &content)
145
225
  end
146
226
 
147
227
  def field_attributes
@@ -150,8 +230,9 @@ module Superform
150
230
  end
151
231
 
152
232
  class ButtonComponent < FieldComponent
153
- def template(&block)
154
- button(**attributes) { button_text }
233
+ def template(&content)
234
+ content ||= Proc.new { button_text }
235
+ button(**attributes, &content)
155
236
  end
156
237
 
157
238
  def button_text
@@ -189,13 +270,48 @@ module Superform
189
270
  end
190
271
 
191
272
  class TextareaComponent < FieldComponent
192
- def template(&)
193
- textarea(**attributes) { dom.value }
273
+ def template(&content)
274
+ content ||= Proc.new { dom.value }
275
+ textarea(**attributes, &content)
194
276
  end
277
+ end
195
278
 
196
- def field_attributes
197
- { id: dom.id, name: dom.name }
279
+ class SelectField < FieldComponent
280
+ def initialize(*, collection: [], **, &)
281
+ super(*, **, &)
282
+ @collection = collection
198
283
  end
284
+
285
+ def template(&options)
286
+ if block_given?
287
+ select(**attributes, &options)
288
+ else
289
+ select(**attributes) { options(*@collection) }
290
+ end
291
+ end
292
+
293
+ def options(*collection)
294
+ map_options(collection).each do |key, value|
295
+ option(selected: field.value == key, value: key) { value }
296
+ end
297
+ end
298
+
299
+ def blank_option(&)
300
+ option(selected: field.value.nil?, &)
301
+ end
302
+
303
+ def true_option(&)
304
+ option(selected: field.value == true, value: true.to_s, &)
305
+ end
306
+
307
+ def false_option(&)
308
+ option(selected: field.value == false, value: false.to_s, &)
309
+ end
310
+
311
+ protected
312
+ def map_options(collection)
313
+ OptionMapper.new(collection)
314
+ end
199
315
  end
200
316
  end
201
317
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Superform
4
- VERSION = "0.4.0"
4
+ VERSION = "0.4.1"
5
5
  end
data/lib/superform.rb CHANGED
@@ -1,26 +1,44 @@
1
+ require "zeitwerk"
2
+
1
3
  module Superform
2
- class Error < StandardError; end
4
+ Loader = Zeitwerk::Loader.for_gem.tap do |loader|
5
+ loader.ignore "#{__dir__}/generators"
6
+ loader.setup
7
+ end
3
8
 
4
- autoload :Rails, "superform/rails"
9
+ class Error < StandardError; end
5
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.
6
14
  class DOM
7
15
  def initialize(field:)
8
16
  @field = field
9
17
  end
10
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.
11
21
  def value
12
22
  @field.value.to_s
13
23
  end
14
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.
15
28
  def id
16
29
  lineage.map(&:key).join("_")
17
30
  end
18
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"}]}}`.
19
36
  def name
20
37
  root, *names = keys
21
38
  names.map { |name| "[#{name}]" }.unshift(root).join
22
39
  end
23
40
 
41
+ # Emit the id, name, and value in an HTML tag-ish that doesnt have an element.
24
42
  def inspect
25
43
  "<id=#{id.inspect} name=#{name.inspect} value=#{value.inspect}/>"
26
44
  end
@@ -34,11 +52,15 @@ module Superform
34
52
  end
35
53
  end
36
54
 
55
+ # One-liner way of walking from the current node all the way up to the parent.
37
56
  def lineage
38
57
  Enumerator.produce(@field, &:parent).take_while(&:itself).reverse
39
58
  end
40
59
  end
41
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.
42
64
  class Node
43
65
  attr_reader :key, :parent
44
66
 
@@ -48,6 +70,13 @@ module Superform
48
70
  end
49
71
  end
50
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.
51
80
  class Namespace < Node
52
81
  include Enumerable
53
82
 
@@ -61,28 +90,76 @@ module Superform
61
90
  yield self if block_given?
62
91
  end
63
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
+ # ```
64
107
  def namespace(key, &block)
65
108
  create_child(key, self.class, object: object_for(key: key), &block)
66
109
  end
67
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
+ # ```
68
120
  def field(key)
69
- create_child(key, @field_class, object: object)
70
- end
71
-
72
- def collection(key, &block)
73
- create_child(key, NamespaceCollection, &block)
121
+ create_child(key, @field_class, object: object).tap do |field|
122
+ yield field if block_given?
123
+ end
74
124
  end
75
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.
76
150
  def serialize
77
151
  each_with_object Hash.new do |child, hash|
78
152
  hash[child.key] = child.serialize
79
153
  end
80
154
  end
81
155
 
156
+ # Iterates through the children of the current namespace, which could be `Namespace` or `Field`
157
+ # objects.
82
158
  def each(&)
83
159
  @children.values.each(&)
84
160
  end
85
161
 
162
+ # Assigns a hash to the current namespace and children namespace.
86
163
  def assign(hash)
87
164
  each do |child|
88
165
  child.assign hash[child.key]
@@ -90,22 +167,30 @@ module Superform
90
167
  self
91
168
  end
92
169
 
93
- def self.root(*args, **kwargs, &block)
94
- new(*args, parent: nil, **kwargs, &block)
95
- end
96
- private
97
-
98
- def create_child(key, child_class, **options, &block)
99
- fetch(key) { child_class.new(key, parent: self, **options, &block) }
170
+ # Creates a root Namespace, which is essentially a form.
171
+ def self.root(*, **, &)
172
+ new(*, parent: nil, **, &)
100
173
  end
101
174
 
102
- def fetch(key, &build)
103
- @children[key] ||= build.call
104
- end
175
+ protected
105
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)`
106
183
  def object_for(key:)
107
184
  @object.send(key) if @object.respond_to? key
108
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, **, &)
192
+ @children.fetch(key) { @children[key] = child_class.new(key, parent: self, **, &) }
193
+ end
109
194
  end
110
195
 
111
196
  class Field < Node
@@ -168,8 +253,8 @@ module Superform
168
253
 
169
254
  private
170
255
 
171
- def build_field(**kwargs)
172
- @field.class.new(@index += 1, parent: @field, **kwargs)
256
+ def build_field(**)
257
+ @field.class.new(@index += 1, parent: @field, **)
173
258
  end
174
259
  end
175
260
 
@@ -208,8 +293,8 @@ module Superform
208
293
  end
209
294
  end
210
295
 
211
- def build_namespace(index, **kwargs)
212
- parent.class.new(index, parent: self, **kwargs, &@template)
296
+ def build_namespace(index, **)
297
+ parent.class.new(index, parent: self, **, &@template)
213
298
  end
214
299
 
215
300
  def parent_collection
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: superform
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brad Gessler
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-07-22 00:00:00.000000000 Z
11
+ date: 2023-11-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: phlex-rails
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: zeitwerk
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.6'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.6'
27
41
  description: A better way to customize and build forms for your Rails application
28
42
  email:
29
43
  - bradgessler@gmail.com