superform 0.5.1 → 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.
@@ -0,0 +1,36 @@
1
+ module Superform
2
+ module Rails
3
+ # Accept a collection of objects and map them to options suitable for form controls, like `select > options`
4
+ class OptionMapper
5
+ include Enumerable
6
+
7
+ def initialize(collection)
8
+ @collection = collection
9
+ end
10
+
11
+ def each(&options)
12
+ @collection.each do |object|
13
+ case object
14
+ in ActiveRecord::Relation => relation
15
+ active_record_relation_options_enumerable(relation).each(&options)
16
+ in id, value
17
+ options.call id, value
18
+ in value
19
+ options.call value, value.to_s
20
+ end
21
+ end
22
+ end
23
+
24
+ def active_record_relation_options_enumerable(relation)
25
+ Enumerator.new do |collection|
26
+ relation.each do |object|
27
+ attributes = object.attributes
28
+ id = attributes.delete(relation.primary_key)
29
+ value = attributes.values.join(" ")
30
+ collection << [ id, value ]
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,73 @@
1
+ module Superform
2
+ module Rails
3
+ module StrongParameters
4
+ protected
5
+ # Assigns permitted params to the given form and returns the model.
6
+ # Usage in a controller when you want to build/validate without saving:
7
+ #
8
+ # def preview
9
+ # @post = Post.new
10
+ # post = permit PostForm.new(@post)
11
+ # # post now has attributes from params, but is not persisted
12
+ # render :preview
13
+ # end
14
+ def permit(form)
15
+ form_params = params.require(form.key)
16
+ assign(form_params, to: form).model
17
+ end
18
+
19
+ # Saves the form's underlying model after assigning permitted params.
20
+ # Typical Rails controller usage (create):
21
+ #
22
+ # def create
23
+ # @post = Post.new
24
+ # if save PostForm.new(@post)
25
+ # redirect_to @post
26
+ # else
27
+ # render :new, status: :unprocessable_entity
28
+ # end
29
+ # end
30
+ #
31
+ # Typical Rails controller usage (update):
32
+ #
33
+ # def update
34
+ # @post = Post.find(params[:id])
35
+ # if save PostForm.new(@post)
36
+ # redirect_to @post
37
+ # else
38
+ # render :edit, status: :unprocessable_entity
39
+ # end
40
+ # end
41
+ def save(form)
42
+ permit(form).save
43
+ end
44
+
45
+ # Bang version that raises on validation failure.
46
+ # Useful when you prefer exceptions or are in a transaction:
47
+ #
48
+ # def create
49
+ # @post = Post.new
50
+ # save! PostForm.new(@post)
51
+ # redirect_to @post
52
+ # rescue ActiveRecord::RecordInvalid
53
+ # render :new, status: :unprocessable_entity
54
+ # end
55
+ def save!(form)
56
+ permit(form).save!
57
+ end
58
+
59
+ # Assigns params to the form and returns the form.
60
+ def assign(params, to:)
61
+ to.tap do |form|
62
+ # This output of this string goes nowhere since it likely
63
+ # won't be used. I'm not sure if I'm right about this though,
64
+ # If I'm wrong, then I think I need to encapsulate this module
65
+ # into a class that can store the rendered HTML that can be
66
+ # rendered later.
67
+ render_to_string form
68
+ form.assign params
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -1,373 +1,22 @@
1
+ require "phlex/rails"
2
+
1
3
  module Superform
2
4
  module Rails
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
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
- module Components
201
- class BaseComponent < Component
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Superform
4
- VERSION = "0.5.1"
4
+ VERSION = "0.6.0"
5
5
  end
metadata CHANGED
@@ -1,34 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: superform
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brad Gessler
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-04-24 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: phlex-rails
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - ">="
17
- - !ruby/object:Gem::Version
18
- version: '1.0'
19
- - - "<"
16
+ - - "~>"
20
17
  - !ruby/object:Gem::Version
21
- version: '3.0'
18
+ version: '2.0'
22
19
  type: :runtime
23
20
  prerelease: false
24
21
  version_requirements: !ruby/object:Gem::Requirement
25
22
  requirements:
26
- - - ">="
27
- - !ruby/object:Gem::Version
28
- version: '1.0'
29
- - - "<"
23
+ - - "~>"
30
24
  - !ruby/object:Gem::Version
31
- version: '3.0'
25
+ version: '2.0'
32
26
  - !ruby/object:Gem::Dependency
33
27
  name: zeitwerk
34
28
  requirement: !ruby/object:Gem::Requirement
@@ -59,17 +53,30 @@ files:
59
53
  - LICENSE.txt
60
54
  - README.md
61
55
  - Rakefile
56
+ - SPEC_STYLE_GUIDE.md
62
57
  - lib/generators/superform/install/USAGE
63
58
  - lib/generators/superform/install/install_generator.rb
64
- - lib/generators/superform/install/templates/application_form.rb
59
+ - lib/generators/superform/install/templates/base.rb
65
60
  - lib/superform.rb
66
61
  - lib/superform/dom.rb
67
62
  - lib/superform/field.rb
68
63
  - lib/superform/field_collection.rb
64
+ - lib/superform/form.rb
69
65
  - lib/superform/namespace.rb
70
66
  - lib/superform/namespace_collection.rb
71
67
  - lib/superform/node.rb
72
68
  - lib/superform/rails.rb
69
+ - lib/superform/rails/components/base.rb
70
+ - lib/superform/rails/components/button.rb
71
+ - lib/superform/rails/components/checkbox.rb
72
+ - lib/superform/rails/components/field.rb
73
+ - lib/superform/rails/components/input.rb
74
+ - lib/superform/rails/components/label.rb
75
+ - lib/superform/rails/components/select.rb
76
+ - lib/superform/rails/components/textarea.rb
77
+ - lib/superform/rails/form.rb
78
+ - lib/superform/rails/option_mapper.rb
79
+ - lib/superform/rails/strong_parameters.rb
73
80
  - lib/superform/version.rb
74
81
  - sig/superform.rbs
75
82
  homepage: https://github.com/rubymonolith/superform
@@ -94,7 +101,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
101
  - !ruby/object:Gem::Version
95
102
  version: '0'
96
103
  requirements: []
97
- rubygems_version: 3.6.2
104
+ rubygems_version: 3.6.7
98
105
  specification_version: 4
99
106
  summary: Build forms in Rails
100
107
  test_files: []
@@ -1,31 +0,0 @@
1
- class ApplicationForm < Superform::Rails::Form
2
- include Phlex::Rails::Helpers::Pluralize
3
-
4
- def row(component)
5
- div do
6
- render component.field.label(style: "display: block;")
7
- render component
8
- end
9
- end
10
-
11
- def around_template(&)
12
- super do
13
- error_messages
14
- yield
15
- submit
16
- end
17
- end
18
-
19
- def error_messages
20
- if model.errors.any?
21
- div(style: "color: red;") do
22
- h2 { "#{pluralize model.errors.count, "error"} prohibited this post from being saved:" }
23
- ul do
24
- model.errors.each do |error|
25
- li { error.full_message }
26
- end
27
- end
28
- end
29
- end
30
- end
31
- end