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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +100 -3
  3. data/CLAUDE.md +46 -0
  4. data/Gemfile.lock +6 -1
  5. data/README.md +208 -75
  6. data/examples/basic_form.rb +21 -0
  7. data/examples/checkbox_form.rb +28 -0
  8. data/examples/date_time_form.rb +18 -0
  9. data/examples/example_form.rb +10 -0
  10. data/examples/select_form.rb +26 -0
  11. data/examples/special_inputs_form.rb +23 -0
  12. data/examples/textarea_form.rb +15 -0
  13. data/lib/generators/superform/install/templates/base.rb +33 -7
  14. data/lib/superform/dom.rb +13 -1
  15. data/lib/superform/field.rb +14 -9
  16. data/lib/superform/rails/choices/choice.rb +39 -0
  17. data/lib/superform/rails/choices/mapper.rb +41 -0
  18. data/lib/superform/rails/choices.rb +6 -0
  19. data/lib/superform/rails/components/base.rb +9 -2
  20. data/lib/superform/rails/components/checkbox.rb +34 -7
  21. data/lib/superform/rails/components/checkboxes.rb +38 -0
  22. data/lib/superform/rails/components/datalist.rb +34 -0
  23. data/lib/superform/rails/components/input.rb +1 -1
  24. data/lib/superform/rails/components/label.rb +1 -1
  25. data/lib/superform/rails/components/radio.rb +21 -0
  26. data/lib/superform/rails/components/radios.rb +38 -0
  27. data/lib/superform/rails/components/select.rb +52 -8
  28. data/lib/superform/rails/field.rb +184 -0
  29. data/lib/superform/rails/form.rb +18 -129
  30. data/lib/superform/version.rb +1 -1
  31. data/server/components/breadcrumb.rb +11 -0
  32. data/server/components/form_card.rb +13 -0
  33. data/server/components/layout.rb +23 -0
  34. data/server/controllers/forms_controller.rb +94 -0
  35. data/server/models/example.rb +31 -0
  36. data/server/public/styles.css +282 -0
  37. data/server/rails.rb +56 -0
  38. data/superform.gemspec +37 -0
  39. metadata +28 -5
  40. 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.** `field(:publish_at).date`, `field(:email).email`, `field(:price).number` — intuitive methods that generate the right input types with proper validation.
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/overview) by ordering the [Phlex on Rails video course](https://beautifulruby.com/phlex).
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://immutable.terminalwire.com/NgTt6nzO1aEnExV8j6ODuKt2iZpY74ZF8ecpUSCp4A0tXA0ErpJIS4cdMX0tQQKOWwZSl65jWnpzpgCLJThhhWtZJGr42XKt7WIi.png)](https://beautifulruby.com/phlex/forms/overview)
21
+ [![](https://immutable.terminalwire.com/hmM9jvv7yF89frBUfjikUfRmdUsTVZ8YvXc7OnnYoERXfLJLzDcj5dFM7qdfMG2bqQLuw633Zt1gl3O7z0zKmH6k8QmifN7z0kJo.png)](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
- 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
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: 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 field(:name).input
215
- labeled field(:email).input(type: :email)
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 permisson.value.humanize
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 field(:email).input
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 field(:email).input
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
- 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.
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
- # Everything below is intentionally verbose!
327
- class SignupForm < Components::Form
353
+ class JobPostingForm < Components::Form
328
354
  def view_template
329
- # The most basic type of input, which will be autofocused.
330
- Field(:name).input.focus
355
+ # Text input, autofocused.
356
+ Field(:title).input.focus
331
357
 
332
- # Input field with a lot more options on it.
333
- Field(:email).input(type: :email, placeholder: "We will sell this to third parties", required: true)
358
+ # HTML5 input helpers pass attributes straight through.
359
+ Field(:contact_email).email(placeholder: "hiring@company.com", required: true)
334
360
 
335
- # You can put fields in a block if that's your thing.
336
- field(:reason) do |f|
361
+ # Block form for custom layout around a field.
362
+ field(:description) do |f|
337
363
  div do
338
- render f.label { "Why should we care about you?" }
339
- render f.textarea(row: 3, col: 80)
364
+ render f.label { "Describe the role" }
365
+ render f.textarea(rows: 6, cols: 80)
340
366
  end
341
367
  end
342
368
 
343
- # Let's get crazy with Selects. They can accept values as simple as 2 element arrays.
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(:contact).label { "Would you like us to spam you to death?" }
346
- Field(:contact).select(
347
- [true, "Yes"], # <option value="true">Yes</option>
348
- [false, "No"], # <option value="false">No</option>
349
- "Hell no", # <option value="Hell no">Hell no</option>
350
- nil # <option></option>
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(:source).label { "How did you hear about us?" }
356
- Field(:source).select do |s|
357
- # Renders a blank option.
358
- s.blank_option
359
- # Pretend WebSources is an ActiveRecord scope with a "Social" category that has "Facebook, X, etc"
360
- # and a "Search" category with "AltaVista, Yahoo, etc."
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(:agreement).label { "Check this box if you agree to give us your first born child" }
371
- Field(:agreement).checkbox(checked: true)
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
- render button { "Submit" }
375
- end
376
- end
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
- ### Upload fields
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:
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
- ```ruby
383
- class User::ImageForm < Components::Form
384
- def view_template
385
- # render label
386
- Field(:image).label { "Choose file" }
387
- # render file input with accept attribute for png and jpeg images
388
- Field(:image).input(type: "file", accept: "image/png, image/jpeg")
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: 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 field(:name).tooltip_input
425
- labeled field(:email).tooltip_input(type: :email)
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 Posts::Form.new(@post)
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 Posts::Form.new(@post)
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 Posts::Form.new(@post) # Assigns params but doesn't save
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,10 @@
1
+ # Base class for example forms
2
+ class ExampleForm < Superform::Rails::Form
3
+ def self.name_text
4
+ name.gsub(/Form$/, "").gsub(/([a-z])([A-Z])/, '\1 \2')
5
+ end
6
+
7
+ def self.description
8
+ ""
9
+ end
10
+ 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
- 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
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