Skip to content

Commit 06b4170

Browse files
committed
Rewrite field guide with realistic job posting form
Replace the contrived SignupForm kitchen-sink example with a coherent JobPostingForm that demonstrates every field type in a realistic Rails context. Use HTML5 convenience methods (.email, .file) instead of raw input(type:). Add Rails vs Superform side-by-side comparison showing how much ceremony Rails requires for checkbox groups.
1 parent 0ebf93a commit 06b4170

1 file changed

Lines changed: 99 additions & 97 deletions

File tree

README.md

Lines changed: 99 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -313,106 +313,86 @@ render UserForm.new(Admin::User.new)
313313

314314
## Form field guide
315315

316-
Superform tries to strike a balance between "being as close to HTML forms as possible" and not requiring a lot of boilerplate to create forms. This example is contrived, but it shows all the different ways you can render a form.
317-
318-
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.
316+
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.
319317

320318
```ruby
321-
# Everything below is intentionally verbose!
322-
class SignupForm < Components::Form
319+
class JobPostingForm < Components::Form
323320
def view_template
324-
# The most basic type of input, which will be autofocused.
325-
Field(:name).input.focus
321+
# Text input, autofocused.
322+
Field(:title).input.focus
326323

327-
# Input field with a lot more options on it.
328-
Field(:email).input(type: :email, placeholder: "We will sell this to third parties", required: true)
324+
# HTML5 input helpers pass attributes straight through.
325+
Field(:contact_email).email(placeholder: "hiring@company.com", required: true)
329326

330-
# You can put fields in a block if that's your thing.
331-
field(:reason) do |f|
327+
# Block form for custom layout around a field.
328+
field(:description) do |f|
332329
div do
333-
render f.label { "Why should we care about you?" }
334-
render f.textarea(row: 3, col: 80)
330+
render f.label { "Describe the role" }
331+
render f.textarea(rows: 6, cols: 80)
335332
end
336333
end
337334

338-
# Selects accept options as positional arguments. Each option can be:
339-
# - A 2-element array: [value, label] renders <option value="value">label</option>
340-
# - A single value: "text" renders <option value="text">text</option>
341-
# - A hash: {1 => "Admin", 2 => "Editor"} maps ids to labels
342-
# - nil: renders an empty <option></option>
335+
# Select options can be [value, label] pairs, single values, hashes, or nil
336+
# for a blank option. Pass nil first to prepend a blank <option></option>.
343337
div do
344-
Field(:contact).label { "Would you like us to spam you to death?" }
345-
Field(:contact).select(
346-
[true, "Yes"], # <option value="true">Yes</option>
347-
[false, "No"], # <option value="false">No</option>
348-
"Hell no", # <option value="Hell no">Hell no</option>
349-
nil # <option></option>
338+
Field(:experience_level).label
339+
Field(:experience_level).select(
340+
nil,
341+
["junior", "Junior"],
342+
["mid", "Mid-level"],
343+
["senior", "Senior"],
344+
["lead", "Lead"]
350345
)
351346
end
352347

348+
# Block form gives full control — optgroups, blank options, etc.
353349
div do
354-
Field(:source).label { "How did you hear about us?" }
355-
Field(:source).select do |s|
356-
# Renders a blank option.
357-
s.blank_option
358-
# Pretend WebSources is an ActiveRecord scope with a "Social" category that has "Facebook, X, etc"
359-
# and a "Search" category with "AltaVista, Yahoo, etc."
360-
WebSources.select(:id, :name).group_by(:category) do |category, sources|
361-
s.optgroup(label: category) do
362-
s.options(sources)
350+
Field(:category_id).label { "Category" }
351+
Field(:category_id).select do |s|
352+
s.blank_option { "Select a category..." }
353+
Category.grouped_by_department.each do |department, categories|
354+
s.optgroup(label: department) do
355+
s.options(categories)
363356
end
364357
end
365358
end
366359
end
367360

368-
# Pass nil as first argument to add a blank option at the start
369-
div do
370-
Field(:country).label { "Select your country" }
371-
Field(:country).select(nil, [1, "USA"], [2, "Canada"], [3, "Mexico"])
372-
end
373-
374-
# Multiple select with multiple: true
375-
# - Adds the HTML 'multiple' attribute
376-
# - Appends [] to the field name (role_ids becomes role_ids[])
377-
# - Includes a hidden input to handle empty submissions
361+
# Multiple select for has_many-through or array columns.
362+
# Adds the HTML multiple attribute, appends [] to the field name,
363+
# and includes a hidden input to handle empty submissions.
378364
div do
379-
Field(:role_ids).label { "Select roles" }
380-
Field(:role_ids).select(
381-
[[1, "Admin"], [2, "Editor"], [3, "Viewer"]],
365+
Field(:skill_ids).label { "Required skills" }
366+
Field(:skill_ids).select(
367+
Skill.select(:id, :name),
382368
multiple: true
383369
)
384370
end
385371

386-
# Combine multiple: true with nil for blank option
372+
# ActiveRecord relations work as select options too.
373+
# OptionMapper uses the primary key as value and joins remaining
374+
# attributes for the label.
387375
div do
388-
Field(:tag_ids).label { "Select tags (optional)" }
389-
Field(:tag_ids).select(
390-
nil, [1, "Ruby"], [2, "Rails"], [3, "Phlex"],
391-
multiple: true
392-
)
376+
Field(:hiring_manager_id).label { "Hiring manager" }
377+
Field(:hiring_manager_id).select(User.select(:id, :first_name, :last_name))
393378
end
394379

395-
# Select options can also be ActiveRecord relations
396-
# The relation is passed as a single argument (not splatted)
397-
# OptionMapper extracts the primary key and joins other attributes for the label
380+
# Boolean checkbox — renders a hidden "0" input so unchecked state
381+
# is submitted, just like Rails.
398382
div do
399-
Field(:author_id).label { "Select author" }
400-
# For User.select(:id, :name), renders <option value="1">Alice</option>
401-
# where id=1 is the primary key and "Alice" is the name attribute
402-
Field(:author_id).select(User.select(:id, :name))
383+
Field(:remote_friendly).label { "This position is remote-friendly" }
384+
Field(:remote_friendly).checkbox
403385
end
404386

405-
div do
406-
Field(:agreement).label { "Check this box if you agree to give us your first born child" }
407-
Field(:agreement).checkbox(checked: true)
408-
end
409-
410-
# Radio groups: use radios(...) with the same option formats as select.
411-
# Each iteration yields a Choice with .radio, .label, .value, .text.
412-
# Ids are index-based: "plan_id_0", "plan_id_1", etc.
387+
# Radio groups. Accepts all the same option formats as select:
388+
# arrays, single values, hashes, or ActiveRecord relations.
413389
fieldset do
414-
legend { "Plan" }
415-
field(:plan_id).radios([1, "Basic"], [2, "Pro"], [3, "Enterprise"]).each do |choice|
390+
legend { "Employment type" }
391+
field(:employment_type).radios(
392+
"full_time" => "Full-time",
393+
"part_time" => "Part-time",
394+
"contract" => "Contract"
395+
).each do |choice|
416396
render choice.label {
417397
render choice.radio
418398
whitespace
@@ -421,11 +401,16 @@ class SignupForm < Components::Form
421401
end
422402
end
423403

424-
# Checkbox groups: use checkboxes(...) with the same option formats.
425-
# Handles name[] and checked state automatically.
404+
# Checkbox groups. Same option formats, same Choice API.
405+
# Handles name[], checked state, and unique ids automatically.
426406
fieldset do
427-
legend { "Roles" }
428-
field(:role_ids).checkboxes([1, "Admin"], [2, "Editor"], [3, "Viewer"]).each do |choice|
407+
legend { "Benefits" }
408+
field(:benefit_ids).checkboxes(
409+
[1, "Health insurance"],
410+
[2, "Dental & vision"],
411+
[3, "401(k)"],
412+
[4, "Stock options"]
413+
).each do |choice|
429414
render choice.label {
430415
render choice.checkbox
431416
whitespace
@@ -434,35 +419,18 @@ class SignupForm < Components::Form
434419
end
435420
end
436421

437-
# Options also accept hashes: radios(1 => "Basic", 2 => "Pro")
438-
# or ActiveRecord relations: radios(Plan.select(:id, :name))
439-
440-
# Low-level API: field(:gender).radio("male") and
441-
# field(:role_ids).checkbox(value: 1) are still available
442-
# for full control over iteration and ids.
422+
# File upload (remember to set enctype on the form).
423+
div do
424+
Field(:job_description_pdf).label { "Upload job description" }
425+
Field(:job_description_pdf).file(accept: ".pdf,.doc,.docx")
426+
end
443427

444-
render button { "Submit" }
428+
render button { "Post Job" }
445429
end
446430
end
447431
```
448432

449-
### Upload fields
450-
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:
451-
452-
```ruby
453-
class User::ImageForm < Components::Form
454-
def view_template
455-
# render label
456-
Field(:image).label { "Choose file" }
457-
# render file input with accept attribute for png and jpeg images
458-
Field(:image).input(type: "file", accept: "image/png, image/jpeg")
459-
end
460-
end
461-
462-
# IMPORTANT
463-
# When rendering the form remember to init the User::ImageForm like that
464-
render User::ImageForm.new(@usermodel, enctype: "multipart/form-data")
465-
```
433+
Render it with `<%= render JobPostingForm.new(@job_posting) %>`. For file uploads, pass `enctype: "multipart/form-data"` to the form constructor.
466434

467435

468436
## Extending Superforms
@@ -557,6 +525,40 @@ Rails ships with a lot of great options to make forms. Many of these inspired Su
557525

558526
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.
559527

528+
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:
529+
530+
```erb
531+
<%# Rails: checkbox group for a has_many :benefits association %>
532+
<fieldset>
533+
<legend>Benefits</legend>
534+
<% Benefit.all.each do |benefit| %>
535+
<%= form.check_box :benefit_ids,
536+
{ multiple: true,
537+
checked: form.object.benefit_ids.include?(benefit.id),
538+
id: "job_posting_benefit_ids_#{benefit.id}" },
539+
benefit.id, nil %>
540+
<%= form.label :benefit_ids, benefit.name,
541+
for: "job_posting_benefit_ids_#{benefit.id}" %>
542+
<% end %>
543+
<% end %>
544+
```
545+
546+
```ruby
547+
# Superform
548+
fieldset do
549+
legend { "Benefits" }
550+
field(:benefit_ids).checkboxes(Benefit.select(:id, :name)).each do |choice|
551+
render choice.label {
552+
render choice.checkbox
553+
whitespace
554+
plain choice.text
555+
}
556+
end
557+
end
558+
```
559+
560+
Superform handles the field name (`benefit_ids[]`), checked state, unique ids, and label targeting automatically. The same pattern works for radio groups with `radios(...)`.
561+
560562
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.
561563

562564
### Simple Form

0 commit comments

Comments
 (0)