superform 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +131 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +131 -29
- data/README.md +200 -66
- data/SPEC_STYLE_GUIDE.md +146 -0
- data/lib/generators/superform/install/USAGE +6 -2
- data/lib/generators/superform/install/install_generator.rb +11 -22
- data/lib/generators/superform/install/templates/base.rb +33 -0
- data/lib/superform/dom.rb +51 -0
- data/lib/superform/field.rb +121 -0
- data/lib/superform/field_collection.rb +33 -0
- data/lib/superform/form.rb +34 -0
- data/lib/superform/namespace.rb +134 -0
- data/lib/superform/namespace_collection.rb +51 -0
- data/lib/superform/node.rb +12 -0
- data/lib/superform/rails/components/base.rb +31 -0
- data/lib/superform/rails/components/button.rb +20 -0
- data/lib/superform/rails/components/checkbox.rb +19 -0
- data/lib/superform/rails/components/field.rb +11 -0
- data/lib/superform/rails/components/input.rb +59 -0
- data/lib/superform/rails/components/label.rb +20 -0
- data/lib/superform/rails/components/select.rb +43 -0
- data/lib/superform/rails/components/textarea.rb +12 -0
- data/lib/superform/rails/form.rb +240 -0
- data/lib/superform/rails/option_mapper.rb +36 -0
- data/lib/superform/rails/strong_parameters.rb +73 -0
- data/lib/superform/rails.rb +17 -368
- data/lib/superform/version.rb +1 -1
- data/lib/superform.rb +3 -294
- metadata +25 -9
- data/lib/generators/superform/install/templates/application_form.rb +0 -31
data/README.md
CHANGED
@@ -1,17 +1,25 @@
|
|
1
1
|
# Superform
|
2
2
|
|
3
|
-
|
3
|
+
**The best Rails form library.** Whether you're using ERB, HAML, or Phlex, Superform makes building forms delightful.
|
4
4
|
|
5
|
-
* **
|
5
|
+
* **No more strong parameters headaches.** Add a field to your form and it automatically gets permitted. Never again wonder why your new field isn't saving. Superform handles parameter security for you.
|
6
6
|
|
7
|
-
* **
|
7
|
+
* **Works beautifully with ERB.** Start using Superform in your existing Rails app without changing a single ERB template. All the power, zero migration pain.
|
8
8
|
|
9
|
-
* **
|
9
|
+
* **Concise field helpers.** `field(:publish_at).date`, `field(:email).email`, `field(:price).number` — intuitive methods that generate the right input types with proper validation.
|
10
10
|
|
11
|
-
|
11
|
+
* **RESTful controller helpers** Superform's `save` and `save!` methods work exactly like ActiveRecord, making controller code predictable and Rails-like.
|
12
|
+
|
13
|
+
Superform is a complete reimagining of Rails forms, built on solid Ruby foundations with modern component architecture under the hood.
|
12
14
|
|
13
15
|
[](https://codeclimate.com/github/rubymonolith/superform/maintainability) [](https://github.com/rubymonolith/superform/actions/workflows/main.yml)
|
14
16
|
|
17
|
+
## Video course
|
18
|
+
|
19
|
+
Support this project and [become a Superform pro](https://beautifulruby.com/phlex/forms/overview) by ordering the [Phlex on Rails video course](https://beautifulruby.com/phlex).
|
20
|
+
|
21
|
+
[](https://beautifulruby.com/phlex/forms/overview)
|
22
|
+
|
15
23
|
## Installation
|
16
24
|
|
17
25
|
Add to the Rails application's Gemfile by executing:
|
@@ -26,49 +34,164 @@ This will install both Phlex Rails and Superform.
|
|
26
34
|
|
27
35
|
## Usage
|
28
36
|
|
29
|
-
|
37
|
+
### Start with inline forms in your ERB templates
|
38
|
+
|
39
|
+
Superform works instantly in your existing Rails ERB templates. Here's what a form for a blog post might look like:
|
40
|
+
|
41
|
+
```erb
|
42
|
+
<!-- app/views/posts/new.html.erb -->
|
43
|
+
<h1>New Post</h1>
|
44
|
+
|
45
|
+
<%= render Components::Form.new @post do
|
46
|
+
it.Field(:title).text
|
47
|
+
it.Field(:body).textarea
|
48
|
+
it.Field(:publish_at).date
|
49
|
+
it.Field(:featured).checkbox
|
50
|
+
it.submit "Create Post"
|
51
|
+
end %>
|
52
|
+
```
|
30
53
|
|
31
|
-
|
54
|
+
The form automatically generates proper Rails form tags, includes CSRF tokens, and handles validation errors.
|
55
|
+
|
56
|
+
Notice anything missing? Superform doesn't need `<% %>` tags around every single line, unlike all other Rails form helpers.
|
57
|
+
|
58
|
+
### Extract inline forms to dedicated classes to use in other views
|
59
|
+
|
60
|
+
You probably want to use the same form for creating and editing resources. In Superform, you extract forms into their own Ruby classes right along with your views.
|
32
61
|
|
33
62
|
```ruby
|
34
|
-
#
|
35
|
-
class Posts::Form <
|
36
|
-
def
|
37
|
-
|
38
|
-
|
39
|
-
|
63
|
+
# app/views/posts/form.rb
|
64
|
+
class Posts::Form < Components::Form
|
65
|
+
def view_template
|
66
|
+
Field(:title).text
|
67
|
+
Field(:body).textarea(rows: 10)
|
68
|
+
Field(:publish_at).date
|
69
|
+
Field(:featured).checkbox
|
70
|
+
submit
|
40
71
|
end
|
41
72
|
end
|
42
73
|
```
|
43
74
|
|
44
|
-
Then render
|
75
|
+
Then render this in your views:
|
45
76
|
|
46
77
|
```erb
|
47
|
-
|
78
|
+
<!-- app/views/posts/new.html.erb -->
|
79
|
+
<h1>New Post</h1>
|
48
80
|
<%= render Posts::Form.new @post %>
|
49
81
|
```
|
50
82
|
|
83
|
+
Cool, but you're about to score a huge benefit from extracting forms into their own Ruby classes with automatic strong parameters.
|
84
|
+
|
85
|
+
### Automatically permit strong parameters with form classes
|
86
|
+
|
87
|
+
Include `Superform::Rails::StrongParameters` in your controllers for automatic parameter handling:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
class PostsController < ApplicationController
|
91
|
+
include Superform::Rails::StrongParameters
|
92
|
+
|
93
|
+
def create
|
94
|
+
@post = Post.new
|
95
|
+
if save Posts::Form.new(@post)
|
96
|
+
redirect_to @post, notice: 'Post created!'
|
97
|
+
else
|
98
|
+
render :new, status: :unprocessable_entity
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def update
|
103
|
+
@post = Post.find(params[:id])
|
104
|
+
if save Posts::Form.new(@post)
|
105
|
+
redirect_to @post, notice: 'Post updated!'
|
106
|
+
else
|
107
|
+
render :edit, status: :unprocessable_entity
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
```
|
112
|
+
|
113
|
+
The `save` method automatically:
|
114
|
+
- Permits only the parameters defined in your form
|
115
|
+
- Assigns them to your model
|
116
|
+
- Attempts to save the model
|
117
|
+
- Returns `true` if successful, `false` if validation fails
|
118
|
+
|
119
|
+
Use `save!` for the bang version that raises exceptions on validation failure or `permit` if you want to assign parameters to a model without saving it.
|
120
|
+
|
121
|
+
### Concise HTML5 form helpers
|
122
|
+
|
123
|
+
Superform includes helpers for all HTML5 input types:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
class UserForm < Components::Form
|
127
|
+
def view_template
|
128
|
+
Field(:email).email # type="email"
|
129
|
+
Field(:password).password # type="password"
|
130
|
+
Field(:website).url # type="url"
|
131
|
+
Field(:phone).tel # type="tel"
|
132
|
+
Field(:age).number(min: 18) # type="number"
|
133
|
+
Field(:birthday).date # type="date"
|
134
|
+
Field(:appointment).datetime # type="datetime-local"
|
135
|
+
Field(:favorite_color).color # type="color"
|
136
|
+
Field(:bio).textarea(rows: 5)
|
137
|
+
Field(:terms).checkbox
|
138
|
+
submit
|
139
|
+
end
|
140
|
+
end
|
141
|
+
```
|
142
|
+
|
143
|
+
### Works great with Phlex
|
144
|
+
|
145
|
+
Superform was built from the ground-up using Phlex components, which means you'll feel right at home using it with your existing Phlex views and components.
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
class Posts::Form < Components::Form
|
149
|
+
def view_template
|
150
|
+
div(class: "form-section") do
|
151
|
+
h2 { "Post Details" }
|
152
|
+
Field(:title).text(class: "form-control")
|
153
|
+
Field(:body).textarea(class: "form-control", rows: 10)
|
154
|
+
end
|
155
|
+
|
156
|
+
div(class: "form-section") do
|
157
|
+
h2 { "Publishing" }
|
158
|
+
Field(:publish_at).date(class: "form-control")
|
159
|
+
Field(:featured).checkbox(class: "form-check-input")
|
160
|
+
end
|
161
|
+
|
162
|
+
div(class: "form-actions") do
|
163
|
+
submit "Save Post", class: "btn btn-primary"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
```
|
168
|
+
|
169
|
+
This gives you complete control over markup, styling, and component composition while maintaining all the strong parameter and validation benefits.
|
170
|
+
|
51
171
|
## Customization
|
52
172
|
|
53
|
-
Superforms are built out of [Phlex components](https://www.phlex.fun/html/components/). The method names
|
173
|
+
Superforms are built out of [Phlex components](https://www.phlex.fun/html/components/). The method names correspond with the HTML tag, its arguments are attributes, and the blocks are the contents of the tag.
|
54
174
|
|
55
175
|
```ruby
|
56
|
-
# ./app/
|
57
|
-
class
|
58
|
-
class
|
59
|
-
def
|
176
|
+
# ./app/components/form.rb
|
177
|
+
class Components::Form < Superform::Rails::Form
|
178
|
+
class MyInput < Superform::Rails::Components::Input
|
179
|
+
def view_template(&)
|
60
180
|
div class: "form-field" do
|
61
181
|
input(**attributes)
|
62
182
|
end
|
63
183
|
end
|
64
184
|
end
|
65
185
|
|
186
|
+
# Redefining the base Field class lets us override every field component.
|
66
187
|
class Field < Superform::Rails::Form::Field
|
67
188
|
def input(**attributes)
|
68
|
-
|
189
|
+
MyInput.new(self, attributes: attributes)
|
69
190
|
end
|
70
191
|
end
|
71
192
|
|
193
|
+
# Here we make a simple helper to make our syntax shorter. Given a field it
|
194
|
+
# will also render its label.
|
72
195
|
def labeled(component)
|
73
196
|
div class: "form-row" do
|
74
197
|
render component.field.label
|
@@ -86,8 +209,8 @@ That looks like a LOT of code, and it is, but look at how easy it is to create f
|
|
86
209
|
|
87
210
|
```ruby
|
88
211
|
# ./app/views/users/form.rb
|
89
|
-
class Users::Form <
|
90
|
-
def
|
212
|
+
class Users::Form < Components::Form
|
213
|
+
def view_template(&)
|
91
214
|
labeled field(:name).input
|
92
215
|
labeled field(:email).input(type: :email)
|
93
216
|
|
@@ -112,23 +235,23 @@ Consider a form for an account that lets people edit the names and email of the
|
|
112
235
|
|
113
236
|
```ruby
|
114
237
|
class AccountForm < Superform::Rails::Form
|
115
|
-
def
|
238
|
+
def view_template
|
116
239
|
# Account#owner returns a single object
|
117
240
|
namespace :owner do |owner|
|
118
241
|
# Renders input with the name `account[owner][name]`
|
119
|
-
|
242
|
+
owner.Field(:name).text
|
120
243
|
# Renders input with the name `account[owner][email]`
|
121
|
-
|
244
|
+
owner.Field(:email).email
|
122
245
|
end
|
123
246
|
|
124
247
|
# Account#members returns a collection of objects
|
125
248
|
collection(:members).each do |member|
|
126
249
|
# Renders input with the name `account[members][0][name]`,
|
127
250
|
# `account[members][1][name]`, ...
|
128
|
-
|
251
|
+
member.Field(:name).input
|
129
252
|
# Renders input with the name `account[members][0][email]`,
|
130
253
|
# `account[members][1][email]`, ...
|
131
|
-
|
254
|
+
member.Field(:email).input(type: :email)
|
132
255
|
|
133
256
|
# Member#permissions returns an array of values like
|
134
257
|
# ["read", "write", "delete"].
|
@@ -161,7 +284,7 @@ By default Superform namespaces a form based on the ActiveModel model name param
|
|
161
284
|
|
162
285
|
```ruby
|
163
286
|
class UserForm < Superform::Rails::Form
|
164
|
-
def
|
287
|
+
def view_template
|
165
288
|
render field(:email).input
|
166
289
|
end
|
167
290
|
end
|
@@ -177,10 +300,10 @@ To customize the form namespace, like an ActiveRecord model nested within a modu
|
|
177
300
|
|
178
301
|
```ruby
|
179
302
|
class UserForm < Superform::Rails::Form
|
180
|
-
def
|
303
|
+
def view_template
|
181
304
|
render field(:email).input
|
182
305
|
end
|
183
|
-
|
306
|
+
|
184
307
|
def key
|
185
308
|
"user"
|
186
309
|
end
|
@@ -201,26 +324,26 @@ In practice, many of the calls below you'd put inside of a method. This cuts dow
|
|
201
324
|
|
202
325
|
```ruby
|
203
326
|
# Everything below is intentionally verbose!
|
204
|
-
class SignupForm <
|
205
|
-
def
|
327
|
+
class SignupForm < Components::Form
|
328
|
+
def view_template
|
206
329
|
# The most basic type of input, which will be autofocused.
|
207
|
-
|
330
|
+
Field(:name).input.focus
|
208
331
|
|
209
332
|
# Input field with a lot more options on it.
|
210
|
-
|
333
|
+
Field(:email).input(type: :email, placeholder: "We will sell this to third parties", required: true)
|
211
334
|
|
212
335
|
# You can put fields in a block if that's your thing.
|
213
|
-
|
336
|
+
field(:reason) do |f|
|
214
337
|
div do
|
215
|
-
f.label { "Why should we care about you?" }
|
216
|
-
f.textarea(row: 3, col: 80)
|
338
|
+
render f.label { "Why should we care about you?" }
|
339
|
+
render f.textarea(row: 3, col: 80)
|
217
340
|
end
|
218
341
|
end
|
219
342
|
|
220
343
|
# Let's get crazy with Selects. They can accept values as simple as 2 element arrays.
|
221
344
|
div do
|
222
|
-
|
223
|
-
|
345
|
+
Field(:contact).label { "Would you like us to spam you to death?" }
|
346
|
+
Field(:contact).select(
|
224
347
|
[true, "Yes"], # <option value="true">Yes</option>
|
225
348
|
[false, "No"], # <option value="false">No</option>
|
226
349
|
"Hell no", # <option value="Hell no">Hell no</option>
|
@@ -229,8 +352,10 @@ class SignupForm < ApplicationForm
|
|
229
352
|
end
|
230
353
|
|
231
354
|
div do
|
232
|
-
|
233
|
-
|
355
|
+
Field(:source).label { "How did you hear about us?" }
|
356
|
+
Field(:source).select do |s|
|
357
|
+
# Renders a blank option.
|
358
|
+
s.blank_option
|
234
359
|
# Pretend WebSources is an ActiveRecord scope with a "Social" category that has "Facebook, X, etc"
|
235
360
|
# and a "Search" category with "AltaVista, Yahoo, etc."
|
236
361
|
WebSources.select(:id, :name).group_by(:category) do |category, sources|
|
@@ -242,8 +367,8 @@ class SignupForm < ApplicationForm
|
|
242
367
|
end
|
243
368
|
|
244
369
|
div do
|
245
|
-
|
246
|
-
|
370
|
+
Field(:agreement).label { "Check this box if you agree to give us your first born child" }
|
371
|
+
Field(:agreement).checkbox(checked: true)
|
247
372
|
end
|
248
373
|
|
249
374
|
render button { "Submit" }
|
@@ -255,12 +380,12 @@ end
|
|
255
380
|
If you want to add file upload fields to your form you will need to initialize your form with the `enctype` attribute set to `multipart/form-data` as shown in the following example code:
|
256
381
|
|
257
382
|
```ruby
|
258
|
-
class User::ImageForm <
|
259
|
-
def
|
383
|
+
class User::ImageForm < Components::Form
|
384
|
+
def view_template
|
260
385
|
# render label
|
261
|
-
|
386
|
+
Field(:image).label { "Choose file" }
|
262
387
|
# render file input with accept attribute for png and jpeg images
|
263
|
-
|
388
|
+
Field(:image).input(type: "file", accept: "image/png, image/jpeg")
|
264
389
|
end
|
265
390
|
end
|
266
391
|
|
@@ -275,9 +400,9 @@ render User::ImageForm.new(@usermodel, enctype: "multipart/form-data")
|
|
275
400
|
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:
|
276
401
|
|
277
402
|
```ruby
|
278
|
-
class AdminForm <
|
279
|
-
class AdminInput <
|
280
|
-
def
|
403
|
+
class AdminForm < Components::Form
|
404
|
+
class AdminInput < Components::Base
|
405
|
+
def view_template(&)
|
281
406
|
input(**attributes)
|
282
407
|
small { admin_tool_tip_for field.key }
|
283
408
|
end
|
@@ -295,7 +420,7 @@ Then, just like you did in your Erb, you create the form:
|
|
295
420
|
|
296
421
|
```ruby
|
297
422
|
class Admin::Users::Form < AdminForm
|
298
|
-
def
|
423
|
+
def view_template(&)
|
299
424
|
labeled field(:name).tooltip_input
|
300
425
|
labeled field(:email).tooltip_input(type: :email)
|
301
426
|
|
@@ -304,41 +429,50 @@ class Admin::Users::Form < AdminForm
|
|
304
429
|
end
|
305
430
|
```
|
306
431
|
|
307
|
-
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/
|
432
|
+
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/components/forms/*.rb` folder and the components in `app/components/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!
|
308
433
|
|
309
434
|
## Automatic strong parameters
|
310
435
|
|
311
|
-
|
436
|
+
Superform eliminates the need to manually define strong parameters. Just include `Superform::Rails::StrongParameters` in your controllers and use the `save`, `save!`, and `permit` methods:
|
312
437
|
|
313
438
|
```ruby
|
314
439
|
class PostsController < ApplicationController
|
315
440
|
include Superform::Rails::StrongParameters
|
316
441
|
|
442
|
+
# Standard Rails CRUD with automatic strong parameters
|
317
443
|
def create
|
318
|
-
@post =
|
319
|
-
|
320
|
-
|
321
|
-
# Success path
|
444
|
+
@post = Post.new
|
445
|
+
if save Posts::Form.new(@post)
|
446
|
+
redirect_to @post, notice: 'Post created successfully.'
|
322
447
|
else
|
323
|
-
|
448
|
+
render :new, status: :unprocessable_entity
|
324
449
|
end
|
325
450
|
end
|
326
451
|
|
327
452
|
def update
|
328
453
|
@post = Post.find(params[:id])
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
if @post.save
|
333
|
-
# Success path
|
454
|
+
if save Posts::Form.new(@post)
|
455
|
+
redirect_to @post, notice: 'Post updated successfully.'
|
334
456
|
else
|
335
|
-
|
457
|
+
render :edit, status: :unprocessable_entity
|
336
458
|
end
|
337
459
|
end
|
460
|
+
|
461
|
+
# For cases where you want to assign params without saving
|
462
|
+
def preview
|
463
|
+
@post = Post.new
|
464
|
+
permit Posts::Form.new(@post) # Assigns params but doesn't save
|
465
|
+
render :preview
|
466
|
+
end
|
338
467
|
end
|
339
468
|
```
|
340
469
|
|
341
|
-
How
|
470
|
+
**How it works:** Superform automatically permits only the parameters that correspond to fields defined in your form. Attempts to mass-assign other parameters are safely ignored, protecting against parameter pollution attacks.
|
471
|
+
|
472
|
+
**Available methods:**
|
473
|
+
- `save(form)` - Assigns permitted params and saves the model, returns `true`/`false`
|
474
|
+
- `save!(form)` - Same as `save` but raises exception on validation failure
|
475
|
+
- `permit(form)` - Assigns permitted params without saving, returns the model
|
342
476
|
|
343
477
|
## Comparisons
|
344
478
|
|
data/SPEC_STYLE_GUIDE.md
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
# Spec Style Guide
|
2
|
+
|
3
|
+
This document outlines the preferred testing patterns for Superform. Follow these patterns to maintain consistency and readability across the test suite.
|
4
|
+
|
5
|
+
## Generator Testing
|
6
|
+
|
7
|
+
### ✅ Preferred Pattern
|
8
|
+
|
9
|
+
Use integration-style testing that actually runs the generator:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
RSpec.describe SomeGenerator, type: :generator do
|
13
|
+
let(:destination_root) { Dir.mktmpdir }
|
14
|
+
let(:generator) { described_class.new([], {}, { destination_root: destination_root }) }
|
15
|
+
|
16
|
+
before do
|
17
|
+
FileUtils.mkdir_p(destination_root)
|
18
|
+
allow(Rails).to receive(:root).and_return(Pathname.new(destination_root))
|
19
|
+
end
|
20
|
+
|
21
|
+
after do
|
22
|
+
FileUtils.rm_rf(destination_root) if File.exist?(destination_root)
|
23
|
+
end
|
24
|
+
|
25
|
+
context "when dependencies are met" do
|
26
|
+
before do
|
27
|
+
allow(generator).to receive(:gem_in_bundle?).with("some-gem").and_return(true)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "creates the expected file" do
|
31
|
+
generator.invoke_all
|
32
|
+
expect(File.exist?(File.join(destination_root, "path/to/file.rb"))).to be true
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "generated file" do
|
36
|
+
subject { File.read(File.join(destination_root, "path/to/file.rb")) }
|
37
|
+
|
38
|
+
before { generator.invoke_all }
|
39
|
+
|
40
|
+
it { is_expected.to include("class SomeClass") }
|
41
|
+
it { is_expected.to include("def some_method") }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
### ❌ Avoid This Pattern
|
48
|
+
|
49
|
+
Don't unit test individual generator methods in isolation:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
# DON'T DO THIS
|
53
|
+
it "creates file with correct content" do
|
54
|
+
generator.create_some_file
|
55
|
+
|
56
|
+
content = File.read(file_path)
|
57
|
+
expect(content).to include("class SomeClass")
|
58
|
+
expect(content).to include("def some_method")
|
59
|
+
expect(content).to include("def another_method")
|
60
|
+
# ... more expectations
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
## File Content Testing
|
65
|
+
|
66
|
+
### ✅ Use Subject Blocks
|
67
|
+
|
68
|
+
When testing generated file content, use `subject` blocks with `is_expected` matchers:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
describe "generated file" do
|
72
|
+
subject { File.read(file_path) }
|
73
|
+
|
74
|
+
before { run_generator_or_setup }
|
75
|
+
|
76
|
+
it { is_expected.to include("essential content") }
|
77
|
+
it { is_expected.to include("other essential content") }
|
78
|
+
end
|
79
|
+
```
|
80
|
+
|
81
|
+
### ❌ Don't Repeat File Reading
|
82
|
+
|
83
|
+
Avoid reading the same file multiple times in different tests:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
# DON'T DO THIS
|
87
|
+
it "includes class definition" do
|
88
|
+
content = File.read(file_path)
|
89
|
+
expect(content).to include("class SomeClass")
|
90
|
+
end
|
91
|
+
|
92
|
+
it "includes method definition" do
|
93
|
+
content = File.read(file_path) # Reading same file again
|
94
|
+
expect(content).to include("def some_method")
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
## Test Focus
|
99
|
+
|
100
|
+
### ✅ Test What Matters
|
101
|
+
|
102
|
+
Focus on essential functionality, not implementation details:
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
# Test the important stuff
|
106
|
+
it { is_expected.to include("class Base < Superform::Rails::Form") }
|
107
|
+
it { is_expected.to include("def row(component)") }
|
108
|
+
```
|
109
|
+
|
110
|
+
### ❌ Don't Over-Test
|
111
|
+
|
112
|
+
Avoid brittle, line-by-line assertions:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
# DON'T DO THIS - too brittle
|
116
|
+
expect(lines[0]).to eq("module Components")
|
117
|
+
expect(lines[1]).to eq(" module Forms")
|
118
|
+
expect(lines[2]).to eq(" class Base < Superform::Rails::Form")
|
119
|
+
```
|
120
|
+
|
121
|
+
## Test Structure
|
122
|
+
|
123
|
+
### ✅ Good Test Organization
|
124
|
+
|
125
|
+
- Use contexts to group related scenarios
|
126
|
+
- Use descriptive test names
|
127
|
+
- Keep tests focused and minimal
|
128
|
+
- Use `before` blocks to set up common state
|
129
|
+
|
130
|
+
### ✅ Integration Over Unit
|
131
|
+
|
132
|
+
- Test generators by actually running them
|
133
|
+
- Test the user experience, not internal methods
|
134
|
+
- Mock external dependencies, not internal logic
|
135
|
+
|
136
|
+
## Key Principles
|
137
|
+
|
138
|
+
1. **Integration over Unit**: Test how components work together, not in isolation
|
139
|
+
2. **User Experience**: Test what users actually do and see
|
140
|
+
3. **Essential over Exhaustive**: Test what matters, not every edge case
|
141
|
+
4. **Readable over Clever**: Clear, simple tests are better than complex ones
|
142
|
+
5. **DRY but Clear**: Eliminate repetition without sacrificing readability
|
143
|
+
|
144
|
+
## Example: Good Generator Spec
|
145
|
+
|
146
|
+
See `spec/generators/superform/install_generator_spec.rb` for a complete example of these patterns in action.
|
@@ -1,8 +1,12 @@
|
|
1
1
|
Description:
|
2
|
-
Installs
|
2
|
+
Installs Superform with proper component structure
|
3
3
|
|
4
4
|
Example:
|
5
5
|
bin/rails generate superform:install
|
6
6
|
|
7
7
|
This will create:
|
8
|
-
app/
|
8
|
+
app/components/form.rb
|
9
|
+
|
10
|
+
Prerequisites:
|
11
|
+
phlex-rails must be installed. If not installed, run:
|
12
|
+
bundle add phlex-rails
|
@@ -3,31 +3,20 @@ require "bundler"
|
|
3
3
|
class Superform::InstallGenerator < Rails::Generators::Base
|
4
4
|
source_root File.expand_path("templates", __dir__)
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
gem "phlex-rails"
|
12
|
-
generate "phlex:install"
|
13
|
-
end
|
14
|
-
|
15
|
-
def autoload_components
|
16
|
-
return unless APPLICATION_CONFIGURATION_PATH.exist?
|
17
|
-
|
18
|
-
inject_into_class(
|
19
|
-
APPLICATION_CONFIGURATION_PATH,
|
20
|
-
"Application",
|
21
|
-
%( config.autoload_paths << "\#{root}/app/views/forms"\n)
|
22
|
-
)
|
6
|
+
def check_phlex_rails_dependency
|
7
|
+
unless gem_in_bundle?("phlex-rails")
|
8
|
+
say "ERROR: phlex-rails is not installed. Please run 'bundle add phlex-rails' first.", :red
|
9
|
+
exit 1
|
10
|
+
end
|
23
11
|
end
|
24
12
|
|
25
13
|
def create_application_form
|
26
|
-
template "
|
14
|
+
template "base.rb", Rails.root.join("app/components/form.rb")
|
27
15
|
end
|
28
16
|
|
29
17
|
private
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
end
|
18
|
+
|
19
|
+
def gem_in_bundle?(gem_name)
|
20
|
+
Bundler.load.specs.any? { |spec| spec.name == gem_name }
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Components
|
2
|
+
class Form < Superform::Rails::Form
|
3
|
+
include Phlex::Rails::Helpers::Pluralize
|
4
|
+
|
5
|
+
def row(component)
|
6
|
+
div do
|
7
|
+
render component.field.label(style: "display: block;")
|
8
|
+
render component
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def around_template(&)
|
13
|
+
super do
|
14
|
+
error_messages
|
15
|
+
yield if block_given?
|
16
|
+
submit
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def error_messages
|
21
|
+
if model.errors.any?
|
22
|
+
div(style: "color: red;") do
|
23
|
+
h2 { "#{pluralize model.errors.count, "error"} prohibited this post from being saved:" }
|
24
|
+
ul do
|
25
|
+
model.errors.each do |error|
|
26
|
+
li { error.full_message }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|