superform 0.6.0 → 0.7.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 +100 -3
- data/CLAUDE.md +46 -0
- data/Gemfile.lock +6 -1
- data/README.md +208 -75
- data/examples/basic_form.rb +21 -0
- data/examples/checkbox_form.rb +28 -0
- data/examples/date_time_form.rb +18 -0
- data/examples/example_form.rb +10 -0
- data/examples/select_form.rb +26 -0
- data/examples/special_inputs_form.rb +23 -0
- data/examples/textarea_form.rb +15 -0
- data/lib/generators/superform/install/templates/base.rb +33 -7
- data/lib/superform/dom.rb +13 -1
- data/lib/superform/field.rb +14 -9
- data/lib/superform/rails/choices/choice.rb +39 -0
- data/lib/superform/rails/choices/mapper.rb +41 -0
- data/lib/superform/rails/choices.rb +6 -0
- data/lib/superform/rails/components/base.rb +9 -2
- data/lib/superform/rails/components/checkbox.rb +34 -7
- data/lib/superform/rails/components/checkboxes.rb +38 -0
- data/lib/superform/rails/components/datalist.rb +34 -0
- data/lib/superform/rails/components/input.rb +1 -1
- data/lib/superform/rails/components/label.rb +1 -1
- data/lib/superform/rails/components/radio.rb +21 -0
- data/lib/superform/rails/components/radios.rb +38 -0
- data/lib/superform/rails/components/select.rb +52 -8
- data/lib/superform/rails/field.rb +184 -0
- data/lib/superform/rails/form.rb +18 -129
- data/lib/superform/version.rb +1 -1
- data/server/components/breadcrumb.rb +11 -0
- data/server/components/form_card.rb +13 -0
- data/server/components/layout.rb +23 -0
- data/server/controllers/forms_controller.rb +94 -0
- data/server/models/example.rb +31 -0
- data/server/public/styles.css +282 -0
- data/server/rails.rb +56 -0
- data/superform.gemspec +37 -0
- metadata +28 -5
- data/lib/superform/rails/option_mapper.rb +0 -36
data/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
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
|
-
* **Concise field helpers.** `
|
|
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
12
|
|
|
@@ -16,9 +16,9 @@ Superform is a complete reimagining of Rails forms, built on solid Ruby foundati
|
|
|
16
16
|
|
|
17
17
|
## Video course
|
|
18
18
|
|
|
19
|
-
Support this project and [become a Superform pro](https://beautifulruby.com/phlex/forms/
|
|
19
|
+
Support this project and [become a Superform pro](https://beautifulruby.com/phlex/forms/introduction) by ordering the [Phlex on Rails video course](https://beautifulruby.com/phlex).
|
|
20
20
|
|
|
21
|
-
[](https://beautifulruby.com/phlex/forms/introduction)
|
|
22
22
|
|
|
23
23
|
## Installation
|
|
24
24
|
|
|
@@ -61,7 +61,7 @@ You probably want to use the same form for creating and editing resources. In Su
|
|
|
61
61
|
|
|
62
62
|
```ruby
|
|
63
63
|
# app/views/posts/form.rb
|
|
64
|
-
class Posts::Form < Components::Form
|
|
64
|
+
class Views::Posts::Form < Components::Form
|
|
65
65
|
def view_template
|
|
66
66
|
Field(:title).text
|
|
67
67
|
Field(:body).textarea(rows: 10)
|
|
@@ -77,7 +77,7 @@ Then render this in your views:
|
|
|
77
77
|
```erb
|
|
78
78
|
<!-- app/views/posts/new.html.erb -->
|
|
79
79
|
<h1>New Post</h1>
|
|
80
|
-
<%= render Posts::Form.new @post %>
|
|
80
|
+
<%= render Views::Posts::Form.new @post %>
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
Cool, but you're about to score a huge benefit from extracting forms into their own Ruby classes with automatic strong parameters.
|
|
@@ -92,21 +92,14 @@ class PostsController < ApplicationController
|
|
|
92
92
|
|
|
93
93
|
def create
|
|
94
94
|
@post = Post.new
|
|
95
|
-
if save Posts::Form.new(@post)
|
|
95
|
+
if save Views::Posts::Form.new(@post)
|
|
96
96
|
redirect_to @post, notice: 'Post created!'
|
|
97
97
|
else
|
|
98
98
|
render :new, status: :unprocessable_entity
|
|
99
99
|
end
|
|
100
100
|
end
|
|
101
101
|
|
|
102
|
-
|
|
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
|
|
102
|
+
# ... other actions ...
|
|
110
103
|
end
|
|
111
104
|
```
|
|
112
105
|
|
|
@@ -145,7 +138,7 @@ end
|
|
|
145
138
|
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
139
|
|
|
147
140
|
```ruby
|
|
148
|
-
class Posts::Form < Components::Form
|
|
141
|
+
class Views::Posts::Form < Components::Form
|
|
149
142
|
def view_template
|
|
150
143
|
div(class: "form-section") do
|
|
151
144
|
h2 { "Post Details" }
|
|
@@ -172,6 +165,8 @@ This gives you complete control over markup, styling, and component composition
|
|
|
172
165
|
|
|
173
166
|
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.
|
|
174
167
|
|
|
168
|
+
When building custom components, follow this convention: **positional or named keyword arguments** are for component configuration (non-HTML concerns like `value:`, `options:`, `multiple:`), and **`**kwargs`** are for HTML attributes that pass through to the rendered element. This keeps the boundary between component logic and HTML clean.
|
|
169
|
+
|
|
175
170
|
```ruby
|
|
176
171
|
# ./app/components/form.rb
|
|
177
172
|
class Components::Form < Superform::Rails::Form
|
|
@@ -186,7 +181,7 @@ class Components::Form < Superform::Rails::Form
|
|
|
186
181
|
# Redefining the base Field class lets us override every field component.
|
|
187
182
|
class Field < Superform::Rails::Form::Field
|
|
188
183
|
def input(**attributes)
|
|
189
|
-
MyInput.new(self, attributes
|
|
184
|
+
MyInput.new(self, **attributes)
|
|
190
185
|
end
|
|
191
186
|
end
|
|
192
187
|
|
|
@@ -211,8 +206,8 @@ That looks like a LOT of code, and it is, but look at how easy it is to create f
|
|
|
211
206
|
# ./app/views/users/form.rb
|
|
212
207
|
class Users::Form < Components::Form
|
|
213
208
|
def view_template(&)
|
|
214
|
-
labeled
|
|
215
|
-
labeled
|
|
209
|
+
labeled Field(:name).input
|
|
210
|
+
labeled Field(:email).input(type: :email)
|
|
216
211
|
|
|
217
212
|
submit "Sign up"
|
|
218
213
|
end
|
|
@@ -227,6 +222,40 @@ Then render it from Erb.
|
|
|
227
222
|
|
|
228
223
|
Much better!
|
|
229
224
|
|
|
225
|
+
### Customizing radios and checkboxes
|
|
226
|
+
|
|
227
|
+
`radios` and `checkboxes` render sensible defaults out of the box, but you can subclass them to match your design system. Override `view_template` to change the default markup — the block form still works for one-off customizations.
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
class Components::Form < Superform::Rails::Form
|
|
231
|
+
class MyRadios < Superform::Rails::Components::Radios
|
|
232
|
+
def view_template(&block)
|
|
233
|
+
choices.each do |choice|
|
|
234
|
+
if block
|
|
235
|
+
yield choice
|
|
236
|
+
else
|
|
237
|
+
div(class: "radio-option") do
|
|
238
|
+
render choice.build_input(class: "radio-input")
|
|
239
|
+
label(for: DOM.join(dom.id, choice.index), class: "radio-label") do
|
|
240
|
+
plain choice.text
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
class Field < Field
|
|
249
|
+
def radios(*options, **attributes, &block)
|
|
250
|
+
options = enum_options if options.empty?
|
|
251
|
+
MyRadios.new(field, options:, **attributes, &block)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Now every `Field(:status).radios` in your app gets the custom markup. Individual forms can still pass a block for one-off layouts.
|
|
258
|
+
|
|
230
259
|
## Namespaces & Collections
|
|
231
260
|
|
|
232
261
|
Superform uses a different syntax for namespacing and collections than Rails, which can be a bit confusing since the same terminology is used but the application is slightly different.
|
|
@@ -259,7 +288,7 @@ class AccountForm < Superform::Rails::Form
|
|
|
259
288
|
# Renders input with the name `account[members][0][permissions][]`,
|
|
260
289
|
# `account[members][1][permissions][]`, ...
|
|
261
290
|
render permission.label do
|
|
262
|
-
plain
|
|
291
|
+
plain permission.value.humanize
|
|
263
292
|
render permission.checkbox
|
|
264
293
|
end
|
|
265
294
|
end
|
|
@@ -285,7 +314,7 @@ By default Superform namespaces a form based on the ActiveModel model name param
|
|
|
285
314
|
```ruby
|
|
286
315
|
class UserForm < Superform::Rails::Form
|
|
287
316
|
def view_template
|
|
288
|
-
render
|
|
317
|
+
render Field(:email).input
|
|
289
318
|
end
|
|
290
319
|
end
|
|
291
320
|
|
|
@@ -301,7 +330,7 @@ To customize the form namespace, like an ActiveRecord model nested within a modu
|
|
|
301
330
|
```ruby
|
|
302
331
|
class UserForm < Superform::Rails::Form
|
|
303
332
|
def view_template
|
|
304
|
-
render
|
|
333
|
+
render Field(:email).input
|
|
305
334
|
end
|
|
306
335
|
|
|
307
336
|
def key
|
|
@@ -318,82 +347,142 @@ render UserForm.new(Admin::User.new)
|
|
|
318
347
|
|
|
319
348
|
## Form field guide
|
|
320
349
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
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.
|
|
350
|
+
This example builds a realistic job posting form that demonstrates every field type Superform supports. In practice you'd extract helpers to cut down on `render` calls, but this keeps things explicit so you can see exactly what's happening.
|
|
324
351
|
|
|
325
352
|
```ruby
|
|
326
|
-
|
|
327
|
-
class SignupForm < Components::Form
|
|
353
|
+
class JobPostingForm < Components::Form
|
|
328
354
|
def view_template
|
|
329
|
-
#
|
|
330
|
-
Field(:
|
|
355
|
+
# Text input, autofocused.
|
|
356
|
+
Field(:title).input.focus
|
|
331
357
|
|
|
332
|
-
#
|
|
333
|
-
Field(:
|
|
358
|
+
# HTML5 input helpers pass attributes straight through.
|
|
359
|
+
Field(:contact_email).email(placeholder: "hiring@company.com", required: true)
|
|
334
360
|
|
|
335
|
-
#
|
|
336
|
-
field(:
|
|
361
|
+
# Block form for custom layout around a field.
|
|
362
|
+
field(:description) do |f|
|
|
337
363
|
div do
|
|
338
|
-
render f.label { "
|
|
339
|
-
render f.textarea(
|
|
364
|
+
render f.label { "Describe the role" }
|
|
365
|
+
render f.textarea(rows: 6, cols: 80)
|
|
340
366
|
end
|
|
341
367
|
end
|
|
342
368
|
|
|
343
|
-
#
|
|
369
|
+
# Select options can be [value, label] pairs, single values, hashes, or nil
|
|
370
|
+
# for a blank option. Pass nil first to prepend a blank <option></option>.
|
|
344
371
|
div do
|
|
345
|
-
Field(:
|
|
346
|
-
Field(:
|
|
347
|
-
|
|
348
|
-
[
|
|
349
|
-
"
|
|
350
|
-
|
|
372
|
+
Field(:experience_level).label
|
|
373
|
+
Field(:experience_level).select(
|
|
374
|
+
nil,
|
|
375
|
+
["junior", "Junior"],
|
|
376
|
+
["mid", "Mid-level"],
|
|
377
|
+
["senior", "Senior"],
|
|
378
|
+
["lead", "Lead"]
|
|
351
379
|
)
|
|
352
380
|
end
|
|
353
381
|
|
|
382
|
+
# Block form gives full control — optgroups, blank options, etc.
|
|
354
383
|
div do
|
|
355
|
-
Field(:
|
|
356
|
-
Field(:
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
WebSources.select(:id, :name).group_by(:category) do |category, sources|
|
|
362
|
-
s.optgroup(label: category) do
|
|
363
|
-
s.options(sources)
|
|
384
|
+
Field(:category_id).label { "Category" }
|
|
385
|
+
Field(:category_id).select do |s|
|
|
386
|
+
s.blank_option { "Select a category..." }
|
|
387
|
+
Category.grouped_by_department.each do |department, categories|
|
|
388
|
+
s.optgroup(label: department) do
|
|
389
|
+
s.options(categories)
|
|
364
390
|
end
|
|
365
391
|
end
|
|
366
392
|
end
|
|
367
393
|
end
|
|
368
394
|
|
|
395
|
+
# Multiple select for has_many-through or array columns.
|
|
396
|
+
# Adds the HTML multiple attribute, appends [] to the field name,
|
|
397
|
+
# and includes a hidden input to handle empty submissions.
|
|
369
398
|
div do
|
|
370
|
-
Field(:
|
|
371
|
-
Field(:
|
|
399
|
+
Field(:skill_ids).label { "Required skills" }
|
|
400
|
+
Field(:skill_ids).select(
|
|
401
|
+
Skill.select(:id, :name),
|
|
402
|
+
multiple: true
|
|
403
|
+
)
|
|
372
404
|
end
|
|
373
405
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
406
|
+
# ActiveRecord relations work as select options too.
|
|
407
|
+
# Choices::Mapper uses the primary key as value and joins remaining
|
|
408
|
+
# attributes for the label.
|
|
409
|
+
div do
|
|
410
|
+
Field(:hiring_manager_id).label { "Hiring manager" }
|
|
411
|
+
Field(:hiring_manager_id).select(User.select(:id, :first_name, :last_name))
|
|
412
|
+
end
|
|
378
413
|
|
|
379
|
-
|
|
380
|
-
|
|
414
|
+
# Boolean checkbox — renders a hidden "0" input so unchecked state
|
|
415
|
+
# is submitted, just like Rails.
|
|
416
|
+
div do
|
|
417
|
+
Field(:remote_friendly).label { "This position is remote-friendly" }
|
|
418
|
+
Field(:remote_friendly).checkbox
|
|
419
|
+
end
|
|
381
420
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
#
|
|
388
|
-
|
|
421
|
+
# Radio group auto-detected from a Rails enum.
|
|
422
|
+
# Given: enum :employment_type, full_time: 0, part_time: 1, contract: 2
|
|
423
|
+
# One-liner — renders <label><input type="radio"> Text</label> per choice.
|
|
424
|
+
Field(:employment_type).radios
|
|
425
|
+
|
|
426
|
+
# Block form — full control over each choice's markup.
|
|
427
|
+
fieldset do
|
|
428
|
+
legend { "Employment type" }
|
|
429
|
+
Field(:employment_type).radios do |choice|
|
|
430
|
+
choice.label {
|
|
431
|
+
choice.input
|
|
432
|
+
whitespace
|
|
433
|
+
plain choice.text # "Full time", "Part time", "Contract"
|
|
434
|
+
}
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# You can also pass explicit options to override enum detection.
|
|
439
|
+
# Accepts arrays, single values, hashes, or ActiveRecord relations.
|
|
440
|
+
# Field(:employment_type).radios("full_time" => "Full-time", ...)
|
|
441
|
+
|
|
442
|
+
# Checkbox groups. Same option formats, same Choice API.
|
|
443
|
+
# Handles name[], checked state, and unique ids automatically.
|
|
444
|
+
# One-liner:
|
|
445
|
+
Field(:benefit_ids).checkboxes(
|
|
446
|
+
[1, "Health insurance"],
|
|
447
|
+
[2, "Dental & vision"],
|
|
448
|
+
[3, "401(k)"],
|
|
449
|
+
[4, "Stock options"]
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
# Block form:
|
|
453
|
+
fieldset do
|
|
454
|
+
legend { "Benefits" }
|
|
455
|
+
Field(:benefit_ids).checkboxes(
|
|
456
|
+
[1, "Health insurance"],
|
|
457
|
+
[2, "Dental & vision"],
|
|
458
|
+
[3, "401(k)"],
|
|
459
|
+
[4, "Stock options"]
|
|
460
|
+
) do |choice|
|
|
461
|
+
choice.label {
|
|
462
|
+
choice.input
|
|
463
|
+
whitespace
|
|
464
|
+
plain choice.text
|
|
465
|
+
}
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# Datalist — free-text input with autocomplete suggestions.
|
|
470
|
+
# No JavaScript required. The browser handles filtering natively.
|
|
471
|
+
Field(:time_zone).datalist(*ActiveSupport::TimeZone.all.map(&:name))
|
|
472
|
+
|
|
473
|
+
# File upload (remember to set enctype on the form).
|
|
474
|
+
div do
|
|
475
|
+
Field(:job_description_pdf).label { "Upload job description" }
|
|
476
|
+
Field(:job_description_pdf).file(accept: ".pdf,.doc,.docx")
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
render button { "Post Job" }
|
|
389
480
|
end
|
|
390
481
|
end
|
|
391
|
-
|
|
392
|
-
# IMPORTANT
|
|
393
|
-
# When rendering the form remember to init the User::ImageForm like that
|
|
394
|
-
render User::ImageForm.new(@usermodel, enctype: "multipart/form-data")
|
|
395
482
|
```
|
|
396
483
|
|
|
484
|
+
Render it with `<%= render JobPostingForm.new(@job_posting) %>`. For file uploads, pass `enctype: "multipart/form-data"` to the form constructor.
|
|
485
|
+
|
|
397
486
|
|
|
398
487
|
## Extending Superforms
|
|
399
488
|
|
|
@@ -410,7 +499,7 @@ class AdminForm < Components::Form
|
|
|
410
499
|
|
|
411
500
|
class Field < Field
|
|
412
501
|
def tooltip_input(**attributes)
|
|
413
|
-
AdminInput.new(self, attributes
|
|
502
|
+
AdminInput.new(self, **attributes)
|
|
414
503
|
end
|
|
415
504
|
end
|
|
416
505
|
end
|
|
@@ -421,8 +510,8 @@ Then, just like you did in your Erb, you create the form:
|
|
|
421
510
|
```ruby
|
|
422
511
|
class Admin::Users::Form < AdminForm
|
|
423
512
|
def view_template(&)
|
|
424
|
-
labeled
|
|
425
|
-
labeled
|
|
513
|
+
labeled Field(:name).tooltip_input
|
|
514
|
+
labeled Field(:email).tooltip_input(type: :email)
|
|
426
515
|
|
|
427
516
|
submit "Save"
|
|
428
517
|
end
|
|
@@ -438,11 +527,12 @@ Superform eliminates the need to manually define strong parameters. Just include
|
|
|
438
527
|
```ruby
|
|
439
528
|
class PostsController < ApplicationController
|
|
440
529
|
include Superform::Rails::StrongParameters
|
|
530
|
+
include Views::Posts
|
|
441
531
|
|
|
442
532
|
# Standard Rails CRUD with automatic strong parameters
|
|
443
533
|
def create
|
|
444
534
|
@post = Post.new
|
|
445
|
-
if save
|
|
535
|
+
if save Form.new(@post)
|
|
446
536
|
redirect_to @post, notice: 'Post created successfully.'
|
|
447
537
|
else
|
|
448
538
|
render :new, status: :unprocessable_entity
|
|
@@ -451,7 +541,7 @@ class PostsController < ApplicationController
|
|
|
451
541
|
|
|
452
542
|
def update
|
|
453
543
|
@post = Post.find(params[:id])
|
|
454
|
-
if save
|
|
544
|
+
if save! Form.new(@post)
|
|
455
545
|
redirect_to @post, notice: 'Post updated successfully.'
|
|
456
546
|
else
|
|
457
547
|
render :edit, status: :unprocessable_entity
|
|
@@ -461,7 +551,7 @@ class PostsController < ApplicationController
|
|
|
461
551
|
# For cases where you want to assign params without saving
|
|
462
552
|
def preview
|
|
463
553
|
@post = Post.new
|
|
464
|
-
permit
|
|
554
|
+
permit Form.new(@post) # Assigns params but doesn't save
|
|
465
555
|
render :preview
|
|
466
556
|
end
|
|
467
557
|
end
|
|
@@ -486,6 +576,43 @@ Rails ships with a lot of great options to make forms. Many of these inspired Su
|
|
|
486
576
|
|
|
487
577
|
Rails form helpers have lasted for almost 20 years and are super solid, but things get tricky when your application starts to take on different styles of forms. To manage it all you have to cobble together helper methods, partials, and templates. Additionally, the structure of the form then has to be expressed to the controller as strong params, forcing you to repeat yourself.
|
|
488
578
|
|
|
579
|
+
Here's a checkbox group in Rails vs Superform. In Rails you need to manually wire up the field name with `[]`, track checked state against the model, generate unique ids, and point each label's `for` attribute at the right input:
|
|
580
|
+
|
|
581
|
+
```erb
|
|
582
|
+
<%# Rails: checkbox group for a has_many :benefits association %>
|
|
583
|
+
<fieldset>
|
|
584
|
+
<legend>Benefits</legend>
|
|
585
|
+
<% Benefit.all.each do |benefit| %>
|
|
586
|
+
<%= form.check_box :benefit_ids,
|
|
587
|
+
{ multiple: true,
|
|
588
|
+
checked: form.object.benefit_ids.include?(benefit.id),
|
|
589
|
+
id: "job_posting_benefit_ids_#{benefit.id}" },
|
|
590
|
+
benefit.id, nil %>
|
|
591
|
+
<%= form.label :benefit_ids, benefit.name,
|
|
592
|
+
for: "job_posting_benefit_ids_#{benefit.id}" %>
|
|
593
|
+
<% end %>
|
|
594
|
+
<% end %>
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
```ruby
|
|
598
|
+
# Superform — one-liner
|
|
599
|
+
Field(:benefit_ids).checkboxes(Benefit.select(:id, :name))
|
|
600
|
+
|
|
601
|
+
# Or with custom markup
|
|
602
|
+
fieldset do
|
|
603
|
+
legend { "Benefits" }
|
|
604
|
+
Field(:benefit_ids).checkboxes(Benefit.select(:id, :name)) do |choice|
|
|
605
|
+
choice.label {
|
|
606
|
+
choice.input
|
|
607
|
+
whitespace
|
|
608
|
+
plain choice.text
|
|
609
|
+
}
|
|
610
|
+
end
|
|
611
|
+
end
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
Superform handles the field name (`benefit_ids[]`), checked state, unique ids, and label targeting automatically. The same pattern works for radio groups with `radios(...)`.
|
|
615
|
+
|
|
489
616
|
With Superform, you build the entire form with Ruby code, so you avoid the Erb gymnastics and helper method soup that it takes in Rails to scale up forms in an organization.
|
|
490
617
|
|
|
491
618
|
### Simple Form
|
|
@@ -514,6 +641,12 @@ https://www.ruby-toolbox.com/projects/formtastic
|
|
|
514
641
|
|
|
515
642
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
516
643
|
|
|
644
|
+
To preview example forms in your browser, run:
|
|
645
|
+
|
|
646
|
+
$ bin/preview
|
|
647
|
+
|
|
648
|
+
This boots a local Rails server at http://localhost:3000 that displays all forms in the `examples/` directory. The server supports hot-reloading, so you can edit forms and refresh the page to see changes.
|
|
649
|
+
|
|
517
650
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
518
651
|
|
|
519
652
|
## Contributing
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class BasicForm < ExampleForm
|
|
2
|
+
def self.description = <<~HTML
|
|
3
|
+
<p>Common input types: <code>text</code>, <code>email</code>, <code>password</code>, and <code>number</code>.</p>
|
|
4
|
+
HTML
|
|
5
|
+
|
|
6
|
+
def view_template
|
|
7
|
+
render field(:name).label
|
|
8
|
+
render field(:name).text(placeholder: "Your name")
|
|
9
|
+
|
|
10
|
+
render field(:email).label
|
|
11
|
+
render field(:email).email(placeholder: "you@example.com")
|
|
12
|
+
|
|
13
|
+
render field(:password).label
|
|
14
|
+
render field(:password).password
|
|
15
|
+
|
|
16
|
+
render field(:age).label
|
|
17
|
+
render field(:age).number(min: 0, max: 150)
|
|
18
|
+
|
|
19
|
+
render submit("Save")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class CheckboxForm < ExampleForm
|
|
2
|
+
def self.description = <<~HTML
|
|
3
|
+
<p><code>checkbox</code> inputs with labels inside a fieldset.</p>
|
|
4
|
+
HTML
|
|
5
|
+
|
|
6
|
+
def view_template
|
|
7
|
+
fieldset do
|
|
8
|
+
legend { "Preferences" }
|
|
9
|
+
|
|
10
|
+
label do
|
|
11
|
+
render field(:terms_accepted).checkbox
|
|
12
|
+
plain " I accept the terms"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
label do
|
|
16
|
+
render field(:subscribe).checkbox
|
|
17
|
+
plain " Subscribe to newsletter"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
label do
|
|
21
|
+
render field(:featured).checkbox
|
|
22
|
+
plain " Feature this item"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
render submit("Continue")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class DateTimeForm < ExampleForm
|
|
2
|
+
def self.description = <<~HTML
|
|
3
|
+
<p><code>date</code>, <code>time</code>, and <code>datetime</code> input types.</p>
|
|
4
|
+
HTML
|
|
5
|
+
|
|
6
|
+
def view_template
|
|
7
|
+
render field(:birth_date).label
|
|
8
|
+
render field(:birth_date).date
|
|
9
|
+
|
|
10
|
+
render field(:appointment_time).label
|
|
11
|
+
render field(:appointment_time).time
|
|
12
|
+
|
|
13
|
+
render field(:event_datetime).label
|
|
14
|
+
render field(:event_datetime).datetime
|
|
15
|
+
|
|
16
|
+
render submit("Schedule")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class SelectForm < ExampleForm
|
|
2
|
+
def self.description = <<~HTML
|
|
3
|
+
<p><code>select</code> dropdowns with various option formats.</p>
|
|
4
|
+
HTML
|
|
5
|
+
|
|
6
|
+
COUNTRIES = [
|
|
7
|
+
["United States", "us"],
|
|
8
|
+
["Canada", "ca"],
|
|
9
|
+
["United Kingdom", "uk"],
|
|
10
|
+
["Germany", "de"],
|
|
11
|
+
["Japan", "jp"]
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
def view_template
|
|
15
|
+
render field(:country).label
|
|
16
|
+
render field(:country).select(options: COUNTRIES, include_blank: "Choose...")
|
|
17
|
+
|
|
18
|
+
render field(:priority).label
|
|
19
|
+
render field(:priority).select(options: %w[Low Medium High])
|
|
20
|
+
|
|
21
|
+
render field(:quantity).label
|
|
22
|
+
render field(:quantity).select(options: (1..10).to_a)
|
|
23
|
+
|
|
24
|
+
render submit("Submit")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class SpecialInputsForm < ExampleForm
|
|
2
|
+
def self.name_text = "Special Inputs"
|
|
3
|
+
|
|
4
|
+
def self.description = <<~HTML
|
|
5
|
+
<p>HTML5 input types: <code>color</code>, <code>range</code>, <code>search</code>, and <code>file</code>.</p>
|
|
6
|
+
HTML
|
|
7
|
+
|
|
8
|
+
def view_template
|
|
9
|
+
render field(:favorite_color).label
|
|
10
|
+
render field(:favorite_color).color
|
|
11
|
+
|
|
12
|
+
render field(:volume).label
|
|
13
|
+
render field(:volume).range(min: 0, max: 100, step: 10)
|
|
14
|
+
|
|
15
|
+
render field(:search_query).label
|
|
16
|
+
render field(:search_query).search(placeholder: "Search...")
|
|
17
|
+
|
|
18
|
+
render field(:avatar).label
|
|
19
|
+
render field(:avatar).file(accept: "image/*")
|
|
20
|
+
|
|
21
|
+
render submit("Apply")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class TextareaForm < ExampleForm
|
|
2
|
+
def self.description = <<~HTML
|
|
3
|
+
<p>Text input and <code>textarea</code> for longer content.</p>
|
|
4
|
+
HTML
|
|
5
|
+
|
|
6
|
+
def view_template
|
|
7
|
+
render field(:title).label
|
|
8
|
+
render field(:title).text(placeholder: "Post title")
|
|
9
|
+
|
|
10
|
+
render field(:body).label
|
|
11
|
+
render field(:body).textarea(rows: 6, placeholder: "Write your content...")
|
|
12
|
+
|
|
13
|
+
render submit("Publish")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -1,22 +1,40 @@
|
|
|
1
1
|
module Components
|
|
2
2
|
class Form < Superform::Rails::Form
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
end
|
|
3
|
+
# Extend the Field class to add your own custom helpers.
|
|
4
|
+
class Field < self::Field
|
|
5
|
+
# # Overide base form helpers for small modifications, like injecting
|
|
6
|
+
# # default classes or styles.
|
|
7
|
+
# def input(class: nil, **)
|
|
8
|
+
# super(class: ["border p-2", grab(class:)], **)
|
|
9
|
+
# end
|
|
10
|
+
#
|
|
11
|
+
# # Create custom field helpers that may be accessed via `fied(:email).my_input`
|
|
12
|
+
# def required_email(**)
|
|
13
|
+
# input(type: "email", required: true, **)
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# # Return your own component if you're doing more complicated things.
|
|
17
|
+
# def autocomplete(**attributes)
|
|
18
|
+
# Components::Autocomplete.new(field, **attributes)
|
|
19
|
+
# end
|
|
10
20
|
end
|
|
11
21
|
|
|
12
22
|
def around_template(&)
|
|
13
23
|
super do
|
|
24
|
+
# Renders error messages if there are any validation errors on the model
|
|
14
25
|
error_messages
|
|
26
|
+
# Renders the contents of the form from `view_template` or the block passed
|
|
27
|
+
# the #render method.
|
|
15
28
|
yield if block_given?
|
|
29
|
+
# Renders the submit button for the form.
|
|
16
30
|
submit
|
|
17
31
|
end
|
|
18
32
|
end
|
|
19
33
|
|
|
34
|
+
# This is needed for the `error_messages`
|
|
35
|
+
include Phlex::Rails::Helpers::Pluralize
|
|
36
|
+
|
|
37
|
+
# Displays error messages for the form's model if there are any validation errors.
|
|
20
38
|
def error_messages
|
|
21
39
|
if model.errors.any?
|
|
22
40
|
div(style: "color: red;") do
|
|
@@ -29,5 +47,13 @@ module Components
|
|
|
29
47
|
end
|
|
30
48
|
end
|
|
31
49
|
end
|
|
50
|
+
|
|
51
|
+
# Wraps a form field and its label in a div for layout purposes.
|
|
52
|
+
def row(component)
|
|
53
|
+
div do
|
|
54
|
+
render component.field.label(style: "display: block;")
|
|
55
|
+
render component
|
|
56
|
+
end
|
|
57
|
+
end
|
|
32
58
|
end
|
|
33
59
|
end
|