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.
- checksums.yaml +7 -0
- data/.qlty/.gitignore +7 -0
- data/.qlty/qlty.toml +86 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +1169 -0
- data/Rakefile +12 -0
- data/lib/action_form/base.rb +110 -0
- data/lib/action_form/element.rb +153 -0
- data/lib/action_form/elements_dsl.rb +46 -0
- data/lib/action_form/input.rb +72 -0
- data/lib/action_form/rails/base.rb +116 -0
- data/lib/action_form/rails/rendering.rb +42 -0
- data/lib/action_form/rails/subform.rb +44 -0
- data/lib/action_form/rendering.rb +74 -0
- data/lib/action_form/schema_dsl.rb +41 -0
- data/lib/action_form/subform.rb +57 -0
- data/lib/action_form/subforms_collection.rb +96 -0
- data/lib/action_form/version.rb +5 -0
- data/lib/action_form.rb +21 -0
- data/sig/action_form.rbs +4 -0
- data/sig/easy_form.rbs +4 -0
- metadata +109 -0
data/README.md
ADDED
@@ -0,0 +1,1169 @@
|
|
1
|
+
# ActionForm
|
2
|
+
|
3
|
+
[](https://qlty.sh/gh/andriy-baran/projects/easy_params)
|
4
|
+
[](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).
|