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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +131 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +128 -27
- data/README.md +183 -54
- 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/field.rb +82 -0
- data/lib/superform/form.rb +34 -0
- data/lib/superform/namespace.rb +25 -15
- data/lib/superform/namespace_collection.rb +7 -4
- 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
- metadata +21 -14
- 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,36 +34,148 @@ 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
|
+
```
|
53
|
+
|
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.
|
30
57
|
|
31
|
-
|
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 view_template
|
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
|
176
|
+
# ./app/components/form.rb
|
177
|
+
class Components::Form < Superform::Rails::Form
|
178
|
+
class MyInput < Superform::Rails::Components::Input
|
59
179
|
def view_template(&)
|
60
180
|
div class: "form-field" do
|
61
181
|
input(**attributes)
|
@@ -66,7 +186,7 @@ class ApplicationForm < Superform::Rails::Form
|
|
66
186
|
# Redefining the base Field class lets us override every field component.
|
67
187
|
class Field < Superform::Rails::Form::Field
|
68
188
|
def input(**attributes)
|
69
|
-
|
189
|
+
MyInput.new(self, attributes: attributes)
|
70
190
|
end
|
71
191
|
end
|
72
192
|
|
@@ -89,7 +209,7 @@ That looks like a LOT of code, and it is, but look at how easy it is to create f
|
|
89
209
|
|
90
210
|
```ruby
|
91
211
|
# ./app/views/users/form.rb
|
92
|
-
class Users::Form <
|
212
|
+
class Users::Form < Components::Form
|
93
213
|
def view_template(&)
|
94
214
|
labeled field(:name).input
|
95
215
|
labeled field(:email).input(type: :email)
|
@@ -119,19 +239,19 @@ class AccountForm < Superform::Rails::Form
|
|
119
239
|
# Account#owner returns a single object
|
120
240
|
namespace :owner do |owner|
|
121
241
|
# Renders input with the name `account[owner][name]`
|
122
|
-
|
242
|
+
owner.Field(:name).text
|
123
243
|
# Renders input with the name `account[owner][email]`
|
124
|
-
|
244
|
+
owner.Field(:email).email
|
125
245
|
end
|
126
246
|
|
127
247
|
# Account#members returns a collection of objects
|
128
248
|
collection(:members).each do |member|
|
129
249
|
# Renders input with the name `account[members][0][name]`,
|
130
250
|
# `account[members][1][name]`, ...
|
131
|
-
|
251
|
+
member.Field(:name).input
|
132
252
|
# Renders input with the name `account[members][0][email]`,
|
133
253
|
# `account[members][1][email]`, ...
|
134
|
-
|
254
|
+
member.Field(:email).input(type: :email)
|
135
255
|
|
136
256
|
# Member#permissions returns an array of values like
|
137
257
|
# ["read", "write", "delete"].
|
@@ -183,7 +303,7 @@ class UserForm < Superform::Rails::Form
|
|
183
303
|
def view_template
|
184
304
|
render field(:email).input
|
185
305
|
end
|
186
|
-
|
306
|
+
|
187
307
|
def key
|
188
308
|
"user"
|
189
309
|
end
|
@@ -204,13 +324,13 @@ In practice, many of the calls below you'd put inside of a method. This cuts dow
|
|
204
324
|
|
205
325
|
```ruby
|
206
326
|
# Everything below is intentionally verbose!
|
207
|
-
class SignupForm <
|
327
|
+
class SignupForm < Components::Form
|
208
328
|
def view_template
|
209
329
|
# The most basic type of input, which will be autofocused.
|
210
|
-
|
330
|
+
Field(:name).input.focus
|
211
331
|
|
212
332
|
# Input field with a lot more options on it.
|
213
|
-
|
333
|
+
Field(:email).input(type: :email, placeholder: "We will sell this to third parties", required: true)
|
214
334
|
|
215
335
|
# You can put fields in a block if that's your thing.
|
216
336
|
field(:reason) do |f|
|
@@ -222,8 +342,8 @@ class SignupForm < ApplicationForm
|
|
222
342
|
|
223
343
|
# Let's get crazy with Selects. They can accept values as simple as 2 element arrays.
|
224
344
|
div do
|
225
|
-
|
226
|
-
|
345
|
+
Field(:contact).label { "Would you like us to spam you to death?" }
|
346
|
+
Field(:contact).select(
|
227
347
|
[true, "Yes"], # <option value="true">Yes</option>
|
228
348
|
[false, "No"], # <option value="false">No</option>
|
229
349
|
"Hell no", # <option value="Hell no">Hell no</option>
|
@@ -232,8 +352,8 @@ class SignupForm < ApplicationForm
|
|
232
352
|
end
|
233
353
|
|
234
354
|
div do
|
235
|
-
|
236
|
-
|
355
|
+
Field(:source).label { "How did you hear about us?" }
|
356
|
+
Field(:source).select do |s|
|
237
357
|
# Renders a blank option.
|
238
358
|
s.blank_option
|
239
359
|
# Pretend WebSources is an ActiveRecord scope with a "Social" category that has "Facebook, X, etc"
|
@@ -247,8 +367,8 @@ class SignupForm < ApplicationForm
|
|
247
367
|
end
|
248
368
|
|
249
369
|
div do
|
250
|
-
|
251
|
-
|
370
|
+
Field(:agreement).label { "Check this box if you agree to give us your first born child" }
|
371
|
+
Field(:agreement).checkbox(checked: true)
|
252
372
|
end
|
253
373
|
|
254
374
|
render button { "Submit" }
|
@@ -260,12 +380,12 @@ end
|
|
260
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:
|
261
381
|
|
262
382
|
```ruby
|
263
|
-
class User::ImageForm <
|
383
|
+
class User::ImageForm < Components::Form
|
264
384
|
def view_template
|
265
385
|
# render label
|
266
|
-
|
386
|
+
Field(:image).label { "Choose file" }
|
267
387
|
# render file input with accept attribute for png and jpeg images
|
268
|
-
|
388
|
+
Field(:image).input(type: "file", accept: "image/png, image/jpeg")
|
269
389
|
end
|
270
390
|
end
|
271
391
|
|
@@ -280,8 +400,8 @@ render User::ImageForm.new(@usermodel, enctype: "multipart/form-data")
|
|
280
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:
|
281
401
|
|
282
402
|
```ruby
|
283
|
-
class AdminForm <
|
284
|
-
class AdminInput <
|
403
|
+
class AdminForm < Components::Form
|
404
|
+
class AdminInput < Components::Base
|
285
405
|
def view_template(&)
|
286
406
|
input(**attributes)
|
287
407
|
small { admin_tool_tip_for field.key }
|
@@ -309,41 +429,50 @@ class Admin::Users::Form < AdminForm
|
|
309
429
|
end
|
310
430
|
```
|
311
431
|
|
312
|
-
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!
|
313
433
|
|
314
434
|
## Automatic strong parameters
|
315
435
|
|
316
|
-
|
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:
|
317
437
|
|
318
438
|
```ruby
|
319
439
|
class PostsController < ApplicationController
|
320
440
|
include Superform::Rails::StrongParameters
|
321
441
|
|
442
|
+
# Standard Rails CRUD with automatic strong parameters
|
322
443
|
def create
|
323
|
-
@post =
|
324
|
-
|
325
|
-
|
326
|
-
# Success path
|
444
|
+
@post = Post.new
|
445
|
+
if save Posts::Form.new(@post)
|
446
|
+
redirect_to @post, notice: 'Post created successfully.'
|
327
447
|
else
|
328
|
-
|
448
|
+
render :new, status: :unprocessable_entity
|
329
449
|
end
|
330
450
|
end
|
331
451
|
|
332
452
|
def update
|
333
453
|
@post = Post.find(params[:id])
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
if @post.save
|
338
|
-
# Success path
|
454
|
+
if save Posts::Form.new(@post)
|
455
|
+
redirect_to @post, notice: 'Post updated successfully.'
|
339
456
|
else
|
340
|
-
|
457
|
+
render :edit, status: :unprocessable_entity
|
341
458
|
end
|
342
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
|
343
467
|
end
|
344
468
|
```
|
345
469
|
|
346
|
-
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
|
347
476
|
|
348
477
|
## Comparisons
|
349
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
|