action_form 0.1.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.
data/README.md ADDED
@@ -0,0 +1,1169 @@
1
+ # ActionForm
2
+
3
+ [![Maintainability](https://qlty.sh/gh/andriy-baran/projects/easy_params/maintainability.svg)](https://qlty.sh/gh/andriy-baran/projects/easy_params)
4
+ [![Code Coverage](https://qlty.sh/gh/andriy-baran/projects/easy_params/coverage.svg)](https://qlty.sh/gh/andriy-baran/projects/easy_params)
5
+
6
+ This library allows you to build complex forms in Ruby with a simple DSL. It provides:
7
+
8
+ - A clean, declarative syntax for defining form fields and validations
9
+ - Support for nested forms
10
+ - Automatic form rendering with customizable HTML/CSS
11
+ - Built-in error handling and validation
12
+ - Integration with Rails and other Ruby web frameworks
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem 'action_form'
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ ```bash
25
+ $ bundle install
26
+ ```
27
+
28
+ Or install it yourself as:
29
+
30
+ ```bash
31
+ $ gem install action_form
32
+ ```
33
+
34
+ ### Requirements
35
+
36
+ - Ruby >= 2.7.0
37
+ - Rails >= 6.0.0 (for Rails integration)
38
+
39
+ ## Concept
40
+
41
+ ActionForm is built around a modular architecture that separates form definition, data handling, and rendering concerns.
42
+
43
+ ### Core Architecture
44
+
45
+ **ActionForm::Base** is the main form class that inherits from `Phlex::HTML` for rendering. It combines three key modules:
46
+
47
+ - **Elements DSL**: Provides methods like `element`, `subform`, and `many` to define form structure using block syntax, where each element can be configured with input types, labels, and validation options
48
+ - **Rendering**: Converts form elements into HTML using Phlex, handles nested forms, error display, and provides JavaScript for dynamic form interactions
49
+ - **Schema DSL**: Defines how form data is structured and validated using [EasyParams](https://github.com/andriy-baran/easy_params). It generates parameter classes that can process submitted form data and restore the form state when validation fails
50
+
51
+ ### How It Works
52
+
53
+ 1. **Form Definition**: You define your form using a declarative DSL with `element`, `subform`, and `many` methods
54
+ 2. **Element Creation**: Each element definition creates a class that inherits from `ActionForm::Element`. The element name must correspond to a method or attribute on the object passed to the form (e.g., `element :name` expects the object to have a `name` method)
55
+ 3. **Instance Building**: When the form is instantiated, it iterates through each defined element and creates an instance. Each element instance is bound to the object and can access its current values, errors, and HTML attributes
56
+ 4. **Rendering**: The form renders itself using Phlex, with each element containing all the data needed to render a complete form control (input type, current value, label text, HTML attributes, validation errors, and select options)
57
+ 5. **Parameter Handling**: The form automatically generates [EasyParams](https://github.com/andriy-baran/easy_params) classes that mirror the form structure, providing type coercion, validation, and strong parameter handling for form submissions. Each element's `output` configuration determines how submitted data is processed
58
+
59
+ ### Key Features
60
+
61
+ - **Declarative DSL**: Define forms with simple, readable syntax
62
+ - **Nested Forms**: Support for complex nested structures with `subform` and `many`
63
+ - **Dynamic Collections**: JavaScript-powered add/remove functionality for many relationships
64
+ - **Flexible Rendering**: Each element can be configured with custom input types, labels, and HTML attributes
65
+ - **Error Integration**: Built-in support for displaying validation errors
66
+ - **Rails Integration**: Seamless integration with Rails forms and parameter handling
67
+
68
+ ### Data Flow
69
+
70
+ ActionForm follows a bidirectional data flow pattern that handles both form display and form submission:
71
+
72
+
73
+ #### **Phase 1: Form Display**
74
+ 1. **Object/Model**: Your Ruby object (User model, ActiveRecord instance, or plain Ruby object) containing data to display
75
+ 2. **Form Definition**: ActionForm class defined using the DSL (`element`, `subform`, `many` methods)
76
+ 3. **Element Instances**: Each form element becomes an instance bound to the object, with access to current values, errors, and HTML attributes
77
+ 4. **HTML Rendering**: Final HTML output rendered using Phlex, ready for the browser
78
+
79
+ #### **Phase 2: Form Submission**
80
+ 1. **User Input**: Data submitted through the form by the user
81
+ 2. **Parameter Validation**: ActionForm's auto-generated [EasyParams](https://github.com/andriy-baran/easy_params) classes validate and coerce submitted data
82
+ 3. **Form Processing**: Your application logic processes the validated data (database saves, business logic, etc.)
83
+ 4. **Response**: Result sent back to user (success page, error display, redirect, etc.)
84
+
85
+ #### **Key Benefits:**
86
+ - **Single Source of Truth**: The same form definition handles both displaying existing data and processing new data
87
+ - **Automatic Parameter Handling**: [EasyParams](https://github.com/andriy-baran/easy_params) classes are automatically generated to mirror your form structure
88
+ - **Error Integration**: Failed validations can re-render the form with submitted data and error messages
89
+ - **Nested Support**: Both phases support complex nested structures through `subform` and `many` relationships
90
+
91
+ ## Usage
92
+
93
+ ActionForm follows a **Declare/Plan/Execute** pattern that separates form definition from data handling and rendering:
94
+
95
+ 1. **Declare**: Define your form structure using the DSL (`element`, `subform`, `many`)
96
+ 2. **Plan**: ActionForm creates element instances bound to your object's actual values
97
+ 3. **Execute**: Each element renders itself with the appropriate HTML, labels, and validation
98
+
99
+ ### Form elements declaration
100
+
101
+ ActionForm provides a declarative DSL for defining form elements. Each form class inherits from `ActionForm::Base` and uses three main methods to define form structure:
102
+
103
+ #### **Basic Elements**
104
+
105
+ Use `element` to define individual form fields:
106
+
107
+ ```ruby
108
+ class UserForm < ActionForm::Base
109
+ element :name do
110
+ input type: :text, class: "form-control"
111
+ output type: :string, presence: true
112
+ label text: "Full Name", class: "form-label"
113
+ end
114
+
115
+ element :email do
116
+ input type: :email, placeholder: "user@example.com"
117
+ output type: :string, presence: true, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
118
+ end
119
+
120
+ element :age do
121
+ input type: :number, min: 0, max: 120
122
+ output type: :integer, presence: true
123
+ end
124
+ end
125
+ ```
126
+
127
+ #### **Available Input Types**
128
+
129
+ - **HTML input types**: `:text`, `:email`, `:password`, `:number`, `:tel`, `:url`, etc.
130
+ - **Selection inputs**: `:select`
131
+ - **Other inputs**: `:textarea`, `:hidden`
132
+
133
+ #### **Available Output Types**
134
+
135
+ ActionForm uses [EasyParams](https://github.com/andriy-baran/easy_params) for parameter validation and type coercion:
136
+
137
+ - **Basic types**: `:string`, `:integer`, `:float`, `:bool`, `:date`, `:datetime`
138
+ - **Collections**: `:array` (with `of:` option for element type)
139
+ - **Validation options**: `presence: true`, `format: regex`, `inclusion: { in: [...] }`
140
+
141
+ #### **Element Configuration Methods**
142
+
143
+ ```ruby
144
+ element :field_name do
145
+ # Input configuration
146
+ input type: :text, class: "form-control", placeholder: "Enter value"
147
+
148
+ # Output/validation configuration
149
+ output type: :string, presence: true, format: /\A\d+\z/
150
+
151
+ # Label configuration
152
+ label text: "Custom Label", class: "form-label"
153
+
154
+ # Select options (for select, radio, checkbox)
155
+ options [["value1", "Label 1"], ["value2", "Label 2"]]
156
+
157
+ # Elements tagging
158
+ tags column: "1"
159
+ end
160
+ ```
161
+
162
+ #### **Nested Forms**
163
+
164
+ Use `subform` for single nested objects:
165
+
166
+ ```ruby
167
+ class UserForm < ActionForm::Base
168
+ subform :profile do
169
+ element :bio do
170
+ input type: :textarea, rows: 4
171
+ output type: :string
172
+ end
173
+
174
+ element :avatar do
175
+ input type: :file
176
+ output type: :string
177
+ end
178
+ end
179
+ end
180
+ ```
181
+
182
+ Use `many` for collections of nested objects. Note that `many` requires a `subform` block inside it:
183
+
184
+ ```ruby
185
+ class UserForm < ActionForm::Base
186
+ many :addresses do
187
+ subform do
188
+ element :street do
189
+ input type: :text
190
+ output type: :string, presence: true
191
+ end
192
+
193
+ element :city do
194
+ input type: :text
195
+ output type: :string, presence: true
196
+ end
197
+ end
198
+ end
199
+ end
200
+ ```
201
+
202
+ #### **Complete Example**
203
+
204
+ ```ruby
205
+ class UserForm < ActionForm::Base
206
+ element :name do
207
+ input type: :text, class: "form-control"
208
+ output type: :string, presence: true
209
+ label text: "Full Name"
210
+ end
211
+
212
+ element :email do
213
+ input type: :email, class: "form-control"
214
+ output type: :string, presence: true
215
+ end
216
+
217
+ element :role do
218
+ input type: :select, class: "form-control"
219
+ output type: :string, presence: true
220
+ options [["admin", "Administrator"], ["user", "User"]]
221
+ end
222
+
223
+ element :interests do
224
+ input type: :checkbox
225
+ output type: :array, of: :string
226
+ options [["tech", "Technology"], ["sports", "Sports"], ["music", "Music"]]
227
+ end
228
+
229
+ subform :profile do
230
+ element :bio do
231
+ input type: :textarea, rows: 4
232
+ output type: :string
233
+ end
234
+ end
235
+
236
+ many :addresses do
237
+ subform do
238
+ element :street do
239
+ input type: :text
240
+ output type: :string, presence: true
241
+ end
242
+
243
+ element :city do
244
+ input type: :text
245
+ output type: :string, presence: true
246
+ end
247
+ end
248
+ end
249
+ end
250
+ ```
251
+
252
+ ### Tagging system
253
+
254
+ ActionForm includes a flexible tagging system that allows you to add custom metadata to form elements and control rendering behavior. Tags serve multiple purposes:
255
+
256
+ #### **Purpose of Tags**
257
+
258
+ 1. **Rendering Control**: Tags control how elements are rendered (e.g., showing error messages, template rendering)
259
+ 2. **Custom Metadata**: Store custom data that can be accessed during rendering
260
+ 3. **Element Classification**: Mark elements with specific characteristics for conditional logic
261
+
262
+ #### **Automatic Tags**
263
+
264
+ ActionForm automatically adds several tags based on element configuration:
265
+
266
+ ```ruby
267
+ element :email do
268
+ input type: :email
269
+ output type: :string
270
+ options [["admin", "Admin"], ["user", "User"]] # Adds options: true tag
271
+ end
272
+
273
+ # Automatic tags added:
274
+ # - input: :email (from input type)
275
+ # - output: :string (from output type)
276
+ # - options: true (from options method)
277
+ # - errors: true/false (based on validation errors)
278
+ ```
279
+
280
+ #### **Custom Tags**
281
+
282
+ Add custom tags using the `tags` method:
283
+
284
+ ```ruby
285
+ element :password do
286
+ input type: :password
287
+ output type: :string, presence: true
288
+
289
+ # Custom tags
290
+ tags row: "3",
291
+ column: "4",
292
+ background: "gray"
293
+ end
294
+ ```
295
+
296
+ #### **Tag Usage in Rendering**
297
+
298
+ Tags are used throughout the rendering process:
299
+
300
+ ```ruby
301
+ # Error display (automatic)
302
+ render_inline_errors(element) if element.tags[:errors]
303
+
304
+ # Custom rendering logic
305
+ def render_element(element)
306
+ if element.tags[:row] == "3"
307
+ div(class: "high-priority") { super }
308
+ else
309
+ super
310
+ end
311
+ end
312
+ ```
313
+
314
+ #### **Nested Form Tags**
315
+
316
+ Tags are automatically propagated in nested forms:
317
+
318
+ ```ruby
319
+ many :addresses do
320
+ subform do
321
+ element :street do
322
+ input type: :text
323
+ tags required: true
324
+ end
325
+ end
326
+ end
327
+
328
+ # Each address element will have:
329
+ # - input: :text
330
+ # - subform: :addresses (added automatically)
331
+ # - required: true (from custom tag)
332
+ ```
333
+
334
+ #### **Practical Examples**
335
+
336
+ **Conditional Styling:**
337
+ ```ruby
338
+ element :email do
339
+ input type: :email
340
+ tags field_type: "contact"
341
+ end
342
+
343
+ # In your form class:
344
+ def render_input(element)
345
+ super(class: css_class)
346
+ span do
347
+ help_info[element.tags[:field_type]]
348
+ end
349
+ end
350
+ ```
351
+
352
+ **Custom Error Handling:**
353
+ ```ruby
354
+ element :username do
355
+ input type: :text
356
+ tags custom_validation: true
357
+ end
358
+
359
+ # Override error rendering:
360
+ def render_inline_errors(element)
361
+ if element.tags[:custom_validation]
362
+ div(class: "custom-errors") { element.errors_messages.join(" | ") }
363
+ else
364
+ super
365
+ end
366
+ end
367
+ ```
368
+
369
+ The tagging system provides a powerful way to extend ActionForm's behavior without modifying the core library, enabling custom rendering logic and element classification.
370
+
371
+
372
+ ### Rendering process
373
+
374
+ ActionForm uses a hierarchical rendering system built on [Phlex](https://www.phlex.fun/) that allows complete customization of HTML output. The rendering process follows a clear flow from form-level down to individual elements.
375
+
376
+ #### **Rendering Flow**
377
+
378
+ ```
379
+ view_template (main entry point)
380
+
381
+ render_form (form wrapper)
382
+
383
+ render_elements (iterate through all elements)
384
+
385
+ render_element (individual element)
386
+
387
+ render_label + render_input + render_inline_errors
388
+ ```
389
+
390
+ #### **Core Rendering Methods**
391
+
392
+ **Form Level:**
393
+ - `view_template` - Main entry point, defines overall form structure
394
+ - `render_form` - Renders the `<form>` wrapper with attributes
395
+ - `render_elements` - Iterates through all form elements
396
+ - `render_submit` - Renders the submit button
397
+
398
+ **Element Level:**
399
+ - `render_element` - Renders a complete form element (label + input + errors)
400
+ - `render_label` - Renders the element's label
401
+ - `render_input` - Renders the input field
402
+ - `render_inline_errors` - Renders validation error messages
403
+
404
+ **Subform Level:**
405
+ - `render_subform` - Renders a single nested form
406
+ - `render_many_subforms` - Renders collections of nested forms with JavaScript
407
+ - `render_subform_template` - Renders templates for dynamic form addition
408
+
409
+ #### **Customizing Rendering**
410
+
411
+ You can override any rendering method in your form class to customize the HTML output:
412
+
413
+ **Basic Customization:**
414
+ ```ruby
415
+ class UserForm < ActionForm::Base
416
+ element :name do
417
+ input type: :text
418
+ output type: :string, presence: true
419
+ end
420
+
421
+ # Override element rendering to add custom wrapper
422
+ def render_element(element)
423
+ div(class: "form-group") do
424
+ super
425
+ end
426
+ end
427
+
428
+ # Customize label rendering
429
+ def render_label(element)
430
+ div(class: "label-wrapper") do
431
+ super
432
+ end
433
+ end
434
+
435
+ # Customize input rendering
436
+ def render_input(element, **html_attributes)
437
+ div(class: "input-wrapper") do
438
+ super(class: "form-control")
439
+ end
440
+ end
441
+ end
442
+ ```
443
+
444
+ **Bootstrap-Style Layout:**
445
+ ```ruby
446
+ class UserForm < ActionForm::Base
447
+ element :name do
448
+ input type: :text
449
+ output type: :string, presence: true
450
+ end
451
+
452
+ # Bootstrap grid layout
453
+ def render_element(element)
454
+ div(class: "row mb-3") do
455
+ render_label(element)
456
+ render_input(element)
457
+ render_inline_errors(element) if element.tags[:errors]
458
+ end
459
+ end
460
+
461
+ def render_label(element)
462
+ div(class: "col-md-3") do
463
+ super(class: "form-label")
464
+ end
465
+ end
466
+
467
+ def render_input(element, **html_attributes)
468
+ div(class: "col-md-9") do
469
+ super(class: "form-control")
470
+ end
471
+ end
472
+ end
473
+ ```
474
+
475
+ **Conditional Rendering:**
476
+ ```ruby
477
+ class UserForm < ActionForm::Base
478
+ element :email do
479
+ input type: :email
480
+ tags field_type: "contact"
481
+ end
482
+
483
+ element :password do
484
+ input type: :password
485
+ tags field_type: "security"
486
+ end
487
+
488
+ # Conditional rendering based on tags
489
+ def render_element(element)
490
+ case element.tags[:field_type]
491
+ when "contact"
492
+ div(class: "contact-field") { super }
493
+ when "security"
494
+ div(class: "security-field") { super }
495
+ else
496
+ super
497
+ end
498
+ end
499
+ end
500
+ ```
501
+
502
+ **Custom Error Rendering:**
503
+ ```ruby
504
+ class UserForm < ActionForm::Base
505
+ element :username do
506
+ input type: :text
507
+ output type: :string, presence: true
508
+ end
509
+
510
+ # Custom error display
511
+ def render_inline_errors(element)
512
+ if element.tags[:errors]
513
+ div(class: "alert alert-danger") do
514
+ strong { "Error: " }
515
+ element.errors_messages.join(", ")
516
+ end
517
+ end
518
+ end
519
+ end
520
+ ```
521
+
522
+ **Custom Submit Button:**
523
+ ```ruby
524
+ class UserForm < ActionForm::Base
525
+ # Custom submit button with styling
526
+ def render_submit(**html_attributes)
527
+ div(class: "form-actions") do
528
+ super(class: "btn btn-primary", **html_attributes)
529
+ end
530
+ end
531
+ end
532
+ ```
533
+
534
+ **Complete Form Layout Override:**
535
+ ```ruby
536
+ class UserForm < ActionForm::Base
537
+ element :name do
538
+ input type: :text
539
+ output type: :string, presence: true
540
+ end
541
+
542
+ # Override the entire form structure
543
+ def view_template
544
+ div(class: "custom-form") do
545
+ h2 { "User Registration" }
546
+ render_elements
547
+ div(class: "form-footer") do
548
+ render_submit
549
+ a(href: "/cancel") { "Cancel" }
550
+ end
551
+ end
552
+ end
553
+ end
554
+ ```
555
+
556
+ #### **Advanced Customization**
557
+
558
+ **Custom Input Types:**
559
+ ```ruby
560
+ class UserForm < ActionForm::Base
561
+ element :rating do
562
+ input type: :text
563
+ tags custom_input: "rating"
564
+ end
565
+
566
+ # Custom input rendering for specific types
567
+ def render_input(element, **html_attributes)
568
+ if element.tags[:custom_input] == "rating"
569
+ render_rating_input(element, **html_attributes)
570
+ else
571
+ super
572
+ end
573
+ end
574
+
575
+ private
576
+
577
+ def render_rating_input(element, **html_attributes)
578
+ div(class: "rating-input") do
579
+ 5.times do |i|
580
+ input(type: "radio",
581
+ name: element.html_name,
582
+ value: i + 1,
583
+ checked: element.value == i + 1)
584
+ end
585
+ end
586
+ end
587
+ end
588
+ ```
589
+
590
+ **Dynamic Form Structure:**
591
+ ```ruby
592
+ class UserForm < ActionForm::Base
593
+ element :name do
594
+ input type: :text
595
+ tags section: "basic"
596
+ end
597
+
598
+ element :email do
599
+ input type: :email
600
+ tags section: "contact"
601
+ end
602
+
603
+ # Group elements by sections
604
+ def render_elements
605
+ sections = elements_instances.group_by { |el| el.tags[:section] }
606
+
607
+ sections.each do |section_name, elements|
608
+ div(class: "form-section", id: section_name) do
609
+ h3 { section_name.to_s.capitalize }
610
+ elements.each { |element| render_element(element) }
611
+ end
612
+ end
613
+ end
614
+ end
615
+ ```
616
+
617
+ The rendering system provides complete flexibility while maintaining the declarative nature of form definition. You can customize as little or as much as needed, from individual elements to the entire form structure.
618
+
619
+ ### Element
620
+
621
+ The `ActionForm::Element` class represents individual form elements and provides methods to access their data, control rendering, and customize behavior. Each element is bound to an object and can access its current values, errors, and HTML attributes.
622
+
623
+ #### **Core Methods**
624
+
625
+ **`value`** - Gets the current value from the bound object
626
+ ```ruby
627
+ element :name do
628
+ input type: :text
629
+
630
+ def value
631
+ # Default: object.name
632
+ object.other_name
633
+ end
634
+ end
635
+ ```
636
+ **`html_value`** - Formats value for HTML (dafault `value.to_s`):
637
+ ```ruby
638
+ element :name do
639
+ input type: :text
640
+
641
+ def html_value
642
+ value.strftime('%Y-%m-%d') # Format before render
643
+ end
644
+ end
645
+ ```
646
+
647
+ **`render?`** - Controls whether the element should be rendered:
648
+ ```ruby
649
+ element :admin_field do
650
+ input type: :text
651
+
652
+ def render?
653
+ object.admin?
654
+ end
655
+ end
656
+
657
+ # Or conditionally render elements:
658
+ def render_elements
659
+ elements_instances.select(&:render?).each do |element|
660
+ render_element(element)
661
+ end
662
+ end
663
+ ```
664
+
665
+ **`detached?`** - Indicates if the element is detached from the object (uses static values):
666
+ ```ruby
667
+ element :static_field do
668
+ input type: :text, value: "Static Value"
669
+
670
+ def detached?
671
+ true # This element doesn't bind to object values
672
+ end
673
+ end
674
+ ```
675
+
676
+
677
+
678
+ #### **Label Methods**
679
+
680
+ **`label_text`** - Gets the text to display in the label:
681
+ ```ruby
682
+ element :full_name do
683
+ input type: :text
684
+ label text: "Complete Name", class: 'cool-label', id: 'full-name-label-id'
685
+ end
686
+ ```
687
+
688
+ **`display: false`** - Label won't be rendered
689
+ ```ruby
690
+ element :full_name do
691
+ input type: :text
692
+ label display: false
693
+ end
694
+ ```
695
+
696
+ #### **Element Properties**
697
+
698
+ **`name`** - The element's name (symbol):
699
+ ```ruby
700
+ element :username do
701
+ input type: :text
702
+ end
703
+
704
+ # Access the name:
705
+ element.name # => :username
706
+ ```
707
+
708
+ **`tags`** - Access to element tags:
709
+ ```ruby
710
+ element :priority_field do
711
+ input type: :text
712
+ tags priority: "high", section: "important"
713
+ end
714
+
715
+ element.tags[:priority] # => "high"
716
+ element.tags[:section] # => "important"
717
+ ```
718
+
719
+ **`errors_messages`** - Validation error messages:
720
+ ```ruby
721
+ element :email do
722
+ input type: :email
723
+ output type: :string, presence: true
724
+ end
725
+
726
+ # When validation fails:
727
+ element.errors_messages # => ["can't be blank", "is invalid"]
728
+ ```
729
+
730
+ **`disabled?`** - Controls whether the element is disabled:
731
+ ```ruby
732
+ element :username do
733
+ input type: :text
734
+
735
+ def disabled?
736
+ object.persisted? # Disable for existing records
737
+ end
738
+ end
739
+ ```
740
+
741
+ **`readonly?`** - Controls whether the element is readonly:
742
+ ```ruby
743
+ element :email do
744
+ input type: :email
745
+
746
+ def readonly?
747
+ object.verified? # Readonly if email is verified
748
+ end
749
+ end
750
+ ```
751
+
752
+ #### **Element Lifecycle**
753
+
754
+ Elements go through several phases:
755
+
756
+ 1. **Definition** - Element class is created with DSL configuration
757
+ 2. **Instantiation** - Element instance is created and bound to object
758
+ 3. **Rendering** - Element is rendered to HTML (if `render?` returns true)
759
+ 4. **Validation** - Element values are validated during form submission
760
+
761
+ ```ruby
762
+ class UserForm < ActionForm::Base
763
+ element :name do
764
+ input type: :text
765
+ output type: :string, presence: true
766
+ end
767
+
768
+ # Customize any phase:
769
+ def render_element(element)
770
+ if element.render?
771
+ div(class: "form-group") do
772
+ render_label(element)
773
+ render_input(element)
774
+ render_inline_errors(element) if element.tags[:errors]
775
+ end
776
+ end
777
+ end
778
+ end
779
+ ```
780
+
781
+ ### Rails integration
782
+
783
+ ActionForm provides seamless integration with Rails through `ActionForm::Rails::Base`, which extends the core functionality with Rails-specific features like automatic model binding, nested attributes, and Rails form helpers.
784
+
785
+ #### **Rails Form Class**
786
+
787
+ Use `ActionForm::Rails::Base` instead of `ActionForm::Base` for Rails applications:
788
+
789
+ ```ruby
790
+ class UserForm < ActionForm::Rails::Base
791
+ resource_model User
792
+
793
+ element :name do
794
+ input type: :text
795
+ output type: :string, presence: true
796
+ end
797
+
798
+ element :email do
799
+ input type: :email
800
+ output type: :string, presence: true
801
+ end
802
+ end
803
+ ```
804
+
805
+ #### **Model Binding**
806
+
807
+ The `resource_model` method automatically configures the form for your Rails model:
808
+
809
+ ```ruby
810
+ class UserForm < ActionForm::Rails::Base
811
+ resource_model User # Sets up automatic parameter scoping and model binding
812
+ end
813
+
814
+ # In your controller:
815
+ def new
816
+ @form = UserForm.new(model: User.new)
817
+ end
818
+
819
+ def create
820
+ @form = UserForm.new(model: User.new, params: params)
821
+ if @form.class.params_definition.new(params).valid?
822
+ # Process the form
823
+ else
824
+ render :new
825
+ end
826
+ end
827
+ ```
828
+
829
+ #### **Parameter Scoping**
830
+
831
+ ActionForm automatically handles Rails parameter scoping:
832
+
833
+ ```ruby
834
+ class UserForm < ActionForm::Rails::Base
835
+ resource_model User # Automatically scopes to 'user' parameters
836
+ end
837
+
838
+ # Form parameters are automatically scoped to:
839
+ # params[:user][:name]
840
+ # params[:user][:email]
841
+ # etc.
842
+ ```
843
+
844
+ You can also set custom scopes:
845
+
846
+ ```ruby
847
+ class AdminUserForm < ActionForm::Rails::Base
848
+ scope :admin_user # Parameters will be scoped to params[:admin_user]
849
+ end
850
+ ```
851
+
852
+ #### **Nested Attributes for many Relations**
853
+
854
+ Rails integration automatically handles nested attributes for `many` relationships:
855
+
856
+ ```ruby
857
+ class UserForm < ActionForm::Rails::Base
858
+ resource_model User
859
+
860
+ element :name do
861
+ input type: :text
862
+ output type: :string, presence: true
863
+ end
864
+
865
+ many :addresses do
866
+ subform do
867
+ element :street do
868
+ input type: :text
869
+ output type: :string, presence: true
870
+ end
871
+
872
+ element :city do
873
+ input type: :text
874
+ output type: :string, presence: true
875
+ end
876
+ end
877
+ end
878
+ end
879
+ ```
880
+
881
+ **Automatic Features:**
882
+ - Primary key elements (`id`) are automatically added for existing records
883
+ - Delete elements (`_destroy`) are automatically added for removal
884
+ - Parameters are properly scoped with `_attributes` suffix
885
+ - JavaScript for dynamic add/remove functionality
886
+
887
+ **Generated Parameters:**
888
+ ```ruby
889
+ # For addresses, parameters look like:
890
+ params[:user][:addresses_attributes] = {
891
+ "0" => { "id" => "1", "street" => "123 Main St", "city" => "Anytown" },
892
+ "1" => { "id" => "2", "street" => "456 Oak Ave", "city" => "Somewhere", "_destroy" => "1" }
893
+ }
894
+ ```
895
+
896
+ #### **Controller Integration**
897
+
898
+ ```ruby
899
+ class UsersController < ApplicationController
900
+ def new
901
+ @form = UserForm.new(model: User.new)
902
+ end
903
+
904
+ def create
905
+ @form = UserForm.new(model: User.new, params: params)
906
+ user_params = @form.class.params_definition.new(params)
907
+
908
+ if user_params.valid?
909
+ @user = User.create!(user_params.user.to_h)
910
+ redirect_to @user
911
+ else
912
+ @form = @form.with_params(user_params)
913
+ render :new
914
+ end
915
+ end
916
+
917
+ def edit
918
+ @form = UserForm.new(model: @user)
919
+ end
920
+
921
+ def update
922
+ @form = UserForm.new(model: @user, params: params)
923
+ user_params = @form.class.params_definition.new(params)
924
+
925
+ if user_params.valid?
926
+ @user.update!(user_params.user.to_h)
927
+ redirect_to @user
928
+ else
929
+ @form = @form.with_params(user_params)
930
+ render :edit
931
+ end
932
+ end
933
+ end
934
+ ```
935
+
936
+ #### **View Integration**
937
+
938
+ ```erb
939
+ <!-- app/views/users/new.html.erb -->
940
+ <%= @form %>
941
+ ```
942
+
943
+ #### **Error Handling**
944
+
945
+ ActionForm integrates with Rails validation errors:
946
+
947
+ ```ruby
948
+ class UserForm < ActionForm::Rails::Base
949
+ resource_model User
950
+
951
+ element :email do
952
+ input type: :email
953
+ output type: :string, presence: true, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
954
+ end
955
+ end
956
+
957
+ # When validation fails:
958
+ @form = @form.with_params(invalid_params)
959
+ # The form will automatically display validation errors
960
+ ```
961
+
962
+ #### **Rails-Specific Features**
963
+
964
+ **Automatic Form Attributes:**
965
+ - CSRF protection with authenticity tokens
966
+ - UTF-8 encoding
967
+ - Proper HTTP methods (POST/PATCH)
968
+ - Rails form helpers integration
969
+
970
+ **Model Integration:**
971
+ - Automatic `persisted?` checks
972
+ - Model name and param key handling
973
+ - Polymorphic path generation
974
+
975
+ **Nested Attributes Support:**
976
+ - Automatic `_attributes` parameter scoping
977
+ - Primary key handling for existing records
978
+ - Delete flag handling for record removal
979
+
980
+ #### **Dynamic Form Buttons**
981
+
982
+ ActionForm provides built-in methods for rendering add/remove buttons for dynamic `many` forms:
983
+
984
+ **`render_new_subform_button`** - Renders a button to add new subform instances:
985
+
986
+ ```ruby
987
+ class UserForm < ActionForm::Rails::Base
988
+ resource_model User
989
+
990
+ many :addresses do
991
+ subform do
992
+ element :street do
993
+ input type: :text
994
+ output type: :string, presence: true
995
+ end
996
+
997
+ element :city do
998
+ input type: :text
999
+ output type: :string, presence: true
1000
+ end
1001
+ end
1002
+ end
1003
+
1004
+ # Custom rendering with add button
1005
+ def render_many_subforms(subforms)
1006
+ super # Renders existing subforms and JavaScript
1007
+
1008
+ # Add a button to create new subforms
1009
+ div(class: "form-actions") do
1010
+ render_new_subform_button(class: "btn btn-primary") do
1011
+ "Add Address"
1012
+ end
1013
+ end
1014
+ end
1015
+ end
1016
+ ```
1017
+
1018
+ **`render_remove_subform_button`** - Renders a button to remove subform instances:
1019
+
1020
+ ```ruby
1021
+ class UserForm < ActionForm::Rails::Base
1022
+ resource_model User
1023
+
1024
+ many :addresses do
1025
+ subform do
1026
+ element :street do
1027
+ input type: :text
1028
+ output type: :string, presence: true
1029
+ end
1030
+
1031
+ element :city do
1032
+ input type: :text
1033
+ output type: :string, presence: true
1034
+ end
1035
+ end
1036
+ end
1037
+
1038
+ # Custom subform rendering with remove button
1039
+ def render_subform(subform)
1040
+ div(class: "address-form") do
1041
+ super # Render the subform elements
1042
+
1043
+ # Add remove button for each subform
1044
+ div(class: "form-actions") do
1045
+ render_remove_subform_button(class: "btn btn-danger btn-sm") do
1046
+ "Remove Address"
1047
+ end
1048
+ end
1049
+ end
1050
+ end
1051
+ end
1052
+ ```
1053
+
1054
+ **Complete Dynamic Form Example:**
1055
+
1056
+ ```ruby
1057
+ class UserForm < ActionForm::Rails::Base
1058
+ resource_model User
1059
+
1060
+ many :addresses do
1061
+ element :street do
1062
+ input type: :text, class: "form-control"
1063
+ output type: :string, presence: true
1064
+ end
1065
+
1066
+ element :city do
1067
+ input type: :text, class: "form-control"
1068
+ output type: :string, presence: true
1069
+ end
1070
+
1071
+ element :zip_code do
1072
+ input type: :text, class: "form-control"
1073
+ output type: :string, presence: true
1074
+ end
1075
+ end
1076
+
1077
+ # Custom rendering with both add and remove buttons
1078
+ def render_many_subforms(subforms)
1079
+ super
1080
+ # Add button to create new subforms
1081
+ div(class: "add-address-section") do
1082
+ render_new_subform_button(
1083
+ class: "btn btn-success",
1084
+ data: { insert_before_selector: ".add-address-section" }
1085
+ ) do
1086
+ span(class: "glyphicon glyphicon-plus") { }
1087
+ " Add Address"
1088
+ end
1089
+ end
1090
+ end
1091
+
1092
+ private
1093
+
1094
+ def render_subform(subform)
1095
+ div(class: "address-form border p-3 mb-3") do
1096
+ # Render subform elements
1097
+ super
1098
+
1099
+ # Remove button
1100
+ div(class: "form-actions text-right") do
1101
+ render_remove_subform_button(
1102
+ class: "btn btn-outline-danger btn-sm"
1103
+ ) do
1104
+ span(class: "glyphicon glyphicon-trash") { }
1105
+ " Remove"
1106
+ end
1107
+ end
1108
+ end
1109
+ end
1110
+ end
1111
+ ```
1112
+
1113
+ **Button Customization:**
1114
+
1115
+ Both methods accept HTML attributes and blocks for complete customization:
1116
+
1117
+ ```ruby
1118
+ # Custom styling and attributes
1119
+ render_new_subform_button(
1120
+ class: "btn btn-primary btn-lg",
1121
+ id: "add-address-btn",
1122
+ data: {
1123
+ insert_before_selector: ".address-list",
1124
+ confirm: "Add a new address?"
1125
+ }
1126
+ ) do
1127
+ icon("plus") + " Add New Address"
1128
+ end
1129
+
1130
+ render_remove_subform_button(
1131
+ class: "btn btn-danger btn-sm",
1132
+ data: {
1133
+ confirm: "Are you sure you want to remove this address?",
1134
+ method: "delete"
1135
+ }
1136
+ ) do
1137
+ icon("trash") + " Remove"
1138
+ end
1139
+ ```
1140
+
1141
+ **JavaScript Integration:**
1142
+
1143
+ The buttons automatically integrate with ActionForm's JavaScript functions:
1144
+ - `easyFormAddSubform(event)` - Adds new subform instances
1145
+ - `easyFormRemoveSubform(event)` - Removes or marks subforms for deletion
1146
+
1147
+ The JavaScript handles:
1148
+ - Template cloning with unique IDs
1149
+ - Proper form field naming
1150
+ - Delete flag setting for existing records
1151
+ - DOM manipulation for dynamic forms
1152
+
1153
+ ## Development
1154
+
1155
+ 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.
1156
+
1157
+ 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).
1158
+
1159
+ ## Contributing
1160
+
1161
+ Bug reports and pull requests are welcome on GitHub at https://github.com/andriy-baran/action_form. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/andriy-baran/action_form/blob/master/CODE_OF_CONDUCT.md).
1162
+
1163
+ ## License
1164
+
1165
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
1166
+
1167
+ ## Code of Conduct
1168
+
1169
+ Everyone interacting in the ActionForm project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/andriy-baran/action_form/blob/master/CODE_OF_CONDUCT.md).