plutonium 0.23.5 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/assets/plutonium.css +1 -1
- data/config/initializers/rabl.rb +17 -0
- data/docs/modules/form.md +283 -423
- data/lib/plutonium/definition/nested_inputs.rb +0 -8
- data/lib/plutonium/resource/controller.rb +1 -1
- data/lib/plutonium/version.rb +1 -1
- metadata +2 -2
data/docs/modules/form.md
CHANGED
@@ -4,278 +4,299 @@ title: Form Module
|
|
4
4
|
|
5
5
|
# Form Module
|
6
6
|
|
7
|
-
The Form module
|
7
|
+
The Form module is Plutonium's comprehensive system for building powerful, modern, and secure forms. It extends the `Phlexi::Form` library to provide a suite of enhanced input components, automatic field inference, secure-by-default associations, and seamless integration with your resources. This module is designed to make creating rich, accessible, and interactive forms a breeze.
|
8
8
|
|
9
9
|
::: tip
|
10
10
|
The Form module is located in `lib/plutonium/ui/form/`.
|
11
11
|
:::
|
12
12
|
|
13
|
-
##
|
13
|
+
## Key Features
|
14
14
|
|
15
|
-
- **
|
16
|
-
- **Secure
|
17
|
-
- **Type Inference**:
|
18
|
-
- **Resource Integration**:
|
19
|
-
- **Modern
|
20
|
-
- **
|
15
|
+
- **Rich Input Components**: Out-of-the-box support for markdown editors, date pickers, file uploads with previews, and international phone inputs.
|
16
|
+
- **Secure by Default**: All associations use Signed Global IDs (SGIDs) to prevent parameter tampering, with automatic authorization checks.
|
17
|
+
- **Intelligent Type Inference**: Automatically selects the best input component based on Active Record column types, saving you from boilerplate.
|
18
|
+
- **Deep Resource Integration**: Generate forms automatically from your resource definitions, including support for conditional fields.
|
19
|
+
- **Modern Frontend**: A complete theme system built with Tailwind CSS, Stimulus for interactivity, and first-class dark mode support.
|
20
|
+
- **Complex Form Structures**: Easily manage associations with nested forms supporting dynamic "add" and "remove" functionality.
|
21
21
|
|
22
|
-
## Core
|
22
|
+
## Core Form Classes
|
23
23
|
|
24
|
-
|
24
|
+
Plutonium provides several base form classes, each tailored for a specific purpose.
|
25
25
|
|
26
|
-
|
26
|
+
### `Form::Base`
|
27
|
+
|
28
|
+
This is the foundation for all forms in Plutonium. It extends `Phlexi::Form::Base` and includes the core form builder with all the custom input components. You can inherit from this class to create custom, one-off forms.
|
29
|
+
|
30
|
+
::: details Builder Implementation
|
31
|
+
The `Builder` class within `Form::Base` is where all the custom input tag methods are defined. It aliases standard Rails form helpers like `belongs_to_tag` to Plutonium's secure and enhanced versions.
|
27
32
|
|
28
|
-
::: details Base Form Implementation
|
29
33
|
```ruby
|
30
34
|
class Plutonium::UI::Form::Base < Phlexi::Form::Base
|
31
|
-
|
32
|
-
|
33
|
-
# Enhanced builder with Plutonium-specific components
|
35
|
+
# ...
|
34
36
|
class Builder < Builder
|
35
37
|
include Plutonium::UI::Form::Options::InferredTypes
|
36
38
|
|
37
|
-
|
38
|
-
|
39
|
-
end
|
39
|
+
# Enhanced input components
|
40
|
+
def easymde_tag(**); end
|
40
41
|
alias_method :markdown_tag, :easymde_tag
|
41
42
|
|
42
|
-
def flatpickr_tag(**
|
43
|
-
|
44
|
-
|
43
|
+
def flatpickr_tag(**); end
|
44
|
+
def int_tel_input_tag(**); end
|
45
|
+
alias_method :phone_tag, :int_tel_input_tag
|
45
46
|
|
46
|
-
def uppy_tag(**
|
47
|
-
create_component(Components::Uppy, :uppy, **options, &block)
|
48
|
-
end
|
47
|
+
def uppy_tag(**); end
|
49
48
|
alias_method :file_tag, :uppy_tag
|
50
49
|
alias_method :attachment_tag, :uppy_tag
|
51
50
|
|
52
|
-
def
|
53
|
-
|
54
|
-
end
|
51
|
+
def slim_select_tag(**); end
|
52
|
+
def secure_association_tag(**); end
|
53
|
+
def secure_polymorphic_association_tag(**); end
|
55
54
|
|
56
|
-
# Override default association methods
|
55
|
+
# Override default association methods
|
57
56
|
alias_method :belongs_to_tag, :secure_association_tag
|
58
57
|
alias_method :has_many_tag, :secure_association_tag
|
59
58
|
alias_method :has_one_tag, :secure_association_tag
|
59
|
+
alias_method :polymorphic_belongs_to_tag, :secure_polymorphic_association_tag
|
60
60
|
end
|
61
61
|
end
|
62
62
|
```
|
63
63
|
:::
|
64
64
|
|
65
|
-
### Resource
|
65
|
+
### `Form::Resource`
|
66
66
|
|
67
|
-
This is a specialized form
|
67
|
+
This is a specialized form that intelligently renders inputs based on a resource definition. It's the primary way you'll create `new` and `edit` forms for your models. It automatically handles field rendering, nested resources, and conditional logic defined in your resource class.
|
68
68
|
|
69
69
|
```ruby
|
70
|
-
class
|
71
|
-
|
72
|
-
|
70
|
+
class Plutonium::UI::Form::Resource < Base
|
71
|
+
include Plutonium::UI::Form::Concerns::RendersNestedResourceFields
|
72
|
+
|
73
|
+
def initialize(object, resource_fields:, resource_definition:, **options)
|
74
|
+
# ...
|
73
75
|
end
|
74
76
|
|
75
77
|
def form_template
|
76
|
-
|
77
|
-
|
78
|
-
|
78
|
+
render_fields # Renders inputs from resource_definition
|
79
|
+
render_actions # Renders submit/cancel buttons
|
80
|
+
end
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
### `Form::Query`
|
85
|
+
|
86
|
+
This form is built for search and filtering. It integrates with Plutonium's Query Objects to create search inputs, dynamic filter controls, and hidden fields for sorting and pagination, all submitted via GET requests to preserve filterable URLs.
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
class Plutonium::UI::Form::Query < Base
|
90
|
+
def initialize(object, query_object:, page_size:, **options)
|
91
|
+
# ... configured as a GET form with Turbo integration
|
92
|
+
end
|
93
|
+
|
94
|
+
def form_template
|
95
|
+
render_search_fields
|
96
|
+
render_filter_fields
|
97
|
+
render_sort_fields
|
98
|
+
render_scope_fields
|
99
|
+
end
|
100
|
+
end
|
101
|
+
```
|
102
|
+
|
103
|
+
### `Form::Interaction`
|
104
|
+
|
105
|
+
This specialized form is designed for handling user interactions and actions. It automatically configures itself based on an interaction object, setting up the appropriate fields and form behavior for interactive actions.
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
class Plutonium::UI::Form::Interaction < Resource
|
109
|
+
def initialize(interaction, **options)
|
110
|
+
# Automatically configures fields from interaction attributes
|
111
|
+
options[:resource_fields] = interaction.attribute_names.map(&:to_sym) - %i[resource resources]
|
112
|
+
options[:resource_definition] = interaction
|
113
|
+
# ...
|
114
|
+
end
|
115
|
+
|
116
|
+
# Form posts to the same page for interaction handling
|
117
|
+
def form_action
|
118
|
+
nil
|
79
119
|
end
|
80
120
|
end
|
81
121
|
```
|
82
122
|
|
83
123
|
## Enhanced Input Components
|
84
124
|
|
85
|
-
|
125
|
+
Plutonium replaces standard form inputs with enhanced versions that provide a modern user experience.
|
86
126
|
|
87
|
-
|
127
|
+
### Markdown Editor (Easymde)
|
128
|
+
|
129
|
+
For rich text content, Plutonium integrates a client-side markdown editor based on [EasyMDE](https://github.com/Ionaru/easy-markdown-editor). It's automatically used for `rich_text` fields (like ActionText) and provides a live preview.
|
88
130
|
|
89
131
|
::: code-group
|
90
|
-
```ruby [
|
91
|
-
# Automatically used for
|
132
|
+
```ruby [Automatic Usage]
|
133
|
+
# Automatically used for ActionText rich_text fields
|
92
134
|
render field(:content).easymde_tag
|
135
|
+
|
136
|
+
# Or explicitly with an alias
|
137
|
+
render field(:description).markdown_tag
|
93
138
|
```
|
139
|
+
:::
|
94
140
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
141
|
+
::: details Component Internals
|
142
|
+
The component renders a `textarea` with a `data-controller="easymde"` attribute. It also includes logic to correctly handle ActionText objects by calling `to_plain_text` on the value.
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
class Plutonium::UI::Form::Components::Easymde < Phlexi::Form::Components::Base
|
146
|
+
def view_template
|
147
|
+
textarea(**attributes, data_controller: "easymde") do
|
148
|
+
normalize_value(field.value)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
# ...
|
152
|
+
end
|
101
153
|
```
|
102
154
|
:::
|
103
155
|
|
104
156
|
### Date/Time Picker (Flatpickr)
|
105
157
|
|
106
|
-
A
|
158
|
+
A beautiful and lightweight date/time picker from [Flatpickr](https://flatpickr.js.org/). It's automatically enabled for `date`, `time`, and `datetime` fields.
|
107
159
|
|
108
160
|
::: code-group
|
109
|
-
```ruby [
|
110
|
-
# Automatically used
|
111
|
-
render field(:published_at).flatpickr_tag
|
112
|
-
|
113
|
-
|
114
|
-
# Automatically used for :time fields
|
115
|
-
render field(:meeting_time).flatpickr_tag
|
116
|
-
```
|
117
|
-
```ruby [Datetime Picker]
|
118
|
-
# Automatically used for :datetime fields
|
119
|
-
render field(:deadline).flatpickr_tag
|
120
|
-
```
|
121
|
-
```ruby [With HTML Attributes]
|
122
|
-
render field(:event_date).flatpickr_tag(
|
123
|
-
class: "custom-date-picker",
|
124
|
-
placeholder: "Select date..."
|
125
|
-
)
|
161
|
+
```ruby [Automatic Usage]
|
162
|
+
# Automatically used based on field type
|
163
|
+
render field(:published_at).flatpickr_tag # datetime field
|
164
|
+
render field(:event_date).flatpickr_tag # date field
|
165
|
+
render field(:meeting_time).flatpickr_tag # time field
|
126
166
|
```
|
127
167
|
:::
|
128
168
|
|
129
|
-
:::
|
130
|
-
The
|
131
|
-
|
132
|
-
|
133
|
-
|
169
|
+
::: details Component Internals
|
170
|
+
The component simply adds a `data-controller="flatpickr"` attribute to a standard input. The corresponding Stimulus controller then inspects the input's `type` attribute (`date`, `time`, or `datetime-local`) to initialize Flatpickr with the correct options (e.g., with or without the time picker).
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
class Plutonium::UI::Form::Components::Flatpickr < Phlexi::Form::Components::Input
|
174
|
+
private
|
134
175
|
|
135
|
-
|
176
|
+
def build_input_attributes
|
177
|
+
super
|
178
|
+
attributes[:data_controller] = tokens(attributes[:data_controller], :flatpickr)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
```
|
136
182
|
:::
|
137
183
|
|
138
|
-
###
|
184
|
+
### International Phone Input
|
139
185
|
|
140
|
-
|
186
|
+
For phone numbers, a user-friendly input with a country-code dropdown is provided by [intl-tel-input](https://github.com/jackocnr/intl-tel-input).
|
141
187
|
|
142
188
|
::: code-group
|
143
|
-
```ruby [
|
144
|
-
# Automatically used for
|
145
|
-
render field(:
|
146
|
-
|
147
|
-
|
148
|
-
render field(:
|
149
|
-
```
|
150
|
-
```ruby [With Restrictions]
|
151
|
-
render field(:gallery).uppy_tag(
|
152
|
-
multiple: true,
|
153
|
-
allowed_file_types: ['.jpg', '.jpeg', '.png'],
|
154
|
-
max_file_size: 5.megabytes
|
155
|
-
)
|
156
|
-
```
|
157
|
-
```ruby [Direct to Cloud]
|
158
|
-
render field(:videos).uppy_tag(
|
159
|
-
direct_upload: true, # For S3, etc.
|
160
|
-
max_total_size: 100.megabytes
|
161
|
-
)
|
189
|
+
```ruby [Usage]
|
190
|
+
# Automatically used for fields of type :tel
|
191
|
+
render field(:phone).int_tel_input_tag
|
192
|
+
|
193
|
+
# Or using its alias
|
194
|
+
render field(:mobile).phone_tag
|
162
195
|
```
|
163
196
|
:::
|
164
197
|
|
165
|
-
::: details
|
166
|
-
|
167
|
-
```ruby
|
168
|
-
class Plutonium::UI::Form::Components::Uppy
|
169
|
-
# Automatic features:
|
170
|
-
# - Drag and drop upload
|
171
|
-
# - Progress indicators
|
172
|
-
# - Image previews and thumbnails
|
173
|
-
# - File type and size validation
|
174
|
-
# - Direct-to-cloud upload support
|
175
|
-
# - Interactive preview and deletion of existing attachments
|
198
|
+
::: details Component Internals
|
199
|
+
This component wraps the input in a `div` with a `data-controller="intl-tel-input"` and adds a `data_intl_tel_input_target` to the input itself, allowing the Stimulus controller to initialize the library.
|
176
200
|
|
201
|
+
```ruby
|
202
|
+
class Plutonium::UI::Form::Components::IntlTelInput < Phlexi::Form::Components::Input
|
177
203
|
def view_template
|
178
|
-
div(
|
179
|
-
|
180
|
-
render_upload_interface
|
204
|
+
div(data_controller: "intl-tel-input") do
|
205
|
+
super # Renders the input with proper data targets
|
181
206
|
end
|
182
207
|
end
|
183
208
|
|
184
209
|
private
|
185
210
|
|
186
|
-
def
|
187
|
-
|
188
|
-
|
189
|
-
end
|
190
|
-
end
|
191
|
-
|
192
|
-
def render_attachment_preview(attachment)
|
193
|
-
# Interactive preview with delete option
|
194
|
-
div(class: "attachment-preview", data: { controller: "attachment-preview" }) do
|
195
|
-
render_thumbnail(attachment)
|
196
|
-
render_filename(attachment)
|
197
|
-
render_delete_button
|
198
|
-
end
|
211
|
+
def build_input_attributes
|
212
|
+
super
|
213
|
+
attributes[:data_intl_tel_input_target] = tokens(attributes[:data_intl_tel_input_target], :input)
|
199
214
|
end
|
200
215
|
end
|
201
216
|
```
|
202
217
|
:::
|
203
218
|
|
204
|
-
###
|
219
|
+
### File Upload (Uppy)
|
205
220
|
|
206
|
-
|
221
|
+
File uploads are handled by [Uppy](https://uppy.io/), a sleek, modern uploader. It supports drag & drop, progress indicators, direct-to-cloud uploads, and interactive previews for existing attachments.
|
207
222
|
|
208
223
|
::: code-group
|
209
224
|
```ruby [Basic Usage]
|
210
|
-
# Automatically used for
|
211
|
-
render field(:
|
212
|
-
|
213
|
-
|
214
|
-
# Alias for int_tel_input_tag
|
215
|
-
render field(:mobile).phone_tag
|
225
|
+
# Automatically used for file and Active Storage attachments
|
226
|
+
render field(:avatar).uppy_tag
|
227
|
+
render field(:documents).file_tag # alias
|
228
|
+
render field(:gallery).attachment_tag # alias
|
216
229
|
```
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
230
|
+
|
231
|
+
```ruby [With Options]
|
232
|
+
render field(:documents).uppy_tag(
|
233
|
+
multiple: true,
|
234
|
+
direct_upload: true, # For S3, etc.
|
235
|
+
max_file_size: 10.megabytes,
|
236
|
+
allowed_file_types: ['.pdf', '.doc']
|
221
237
|
)
|
222
238
|
```
|
223
239
|
:::
|
224
240
|
|
225
|
-
:::
|
226
|
-
The
|
227
|
-
|
228
|
-
|
229
|
-
|
241
|
+
::: details Component Internals
|
242
|
+
The Uppy component is quite sophisticated. It renders an interactive preview grid for existing attachments (each with its own `attachment-preview` Stimulus controller for deletion) and a file input managed by an `attachment-input` Stimulus controller that initializes Uppy.
|
243
|
+
|
244
|
+
```ruby
|
245
|
+
class Plutonium::UI::Form::Components::Uppy < Phlexi::Form::Components::Input
|
246
|
+
# Automatic features:
|
247
|
+
# - Interactive preview of existing attachments
|
248
|
+
# - Delete buttons for removing attachments
|
249
|
+
# - Support for direct cloud uploads
|
250
|
+
# - File type and size validation via Uppy options
|
251
|
+
# ...
|
230
252
|
|
231
|
-
|
253
|
+
def view_template
|
254
|
+
div(class: "flex flex-col-reverse gap-2") do
|
255
|
+
render_existing_attachments
|
256
|
+
render_upload_interface
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
```
|
232
261
|
:::
|
233
262
|
|
234
263
|
### Secure Association Inputs
|
235
264
|
|
236
|
-
Plutonium
|
265
|
+
Plutonium overrides all standard Rails association helpers (`belongs_to`, `has_many`, etc.) to use a secure, enhanced version that integrates with [SlimSelect](https://slimselectjs.com/) for a better UI.
|
237
266
|
|
238
267
|
::: code-group
|
239
|
-
```ruby [
|
240
|
-
# Automatically used for
|
268
|
+
```ruby [Association Types]
|
269
|
+
# Automatically used for all standard association types
|
241
270
|
render field(:author).belongs_to_tag
|
242
|
-
```
|
243
|
-
```ruby [Has Many]
|
244
|
-
# Automatically used for has_many associations
|
245
271
|
render field(:tags).has_many_tag
|
272
|
+
render field(:profile).has_one_tag
|
273
|
+
render field(:commentable).polymorphic_belongs_to_tag
|
246
274
|
```
|
247
|
-
|
275
|
+
|
276
|
+
```ruby [With Options]
|
248
277
|
render field(:category).belongs_to_tag(
|
249
|
-
choices: Category.published.pluck(:name, :id)
|
250
|
-
|
251
|
-
|
252
|
-
```ruby [With Add Action]
|
253
|
-
render field(:publisher).belongs_to_tag(
|
254
|
-
add_action: new_publisher_path
|
278
|
+
choices: Category.published.pluck(:name, :id),
|
279
|
+
add_action: new_category_path, # Adds a "+" button to add new records
|
280
|
+
skip_authorization: false # Enforces authorization policies
|
255
281
|
)
|
256
282
|
```
|
257
283
|
:::
|
258
284
|
|
259
|
-
::: details
|
285
|
+
::: details Security & Implementation
|
286
|
+
The `SecureAssociation` component is the cornerstone of Plutonium's form security.
|
287
|
+
- **SGID Encoding**: It uses `to_signed_global_id` as the value method, so raw database IDs are never exposed to the client.
|
288
|
+
- **Authorization**: It uses `authorized_resource_scope` to ensure that the choices presented to the user are only the ones they are permitted to see.
|
289
|
+
- **Add Action**: It can render an "add new" button that automatically includes a `return_to` parameter for a smooth UX.
|
290
|
+
|
260
291
|
```ruby
|
261
292
|
class Plutonium::UI::Form::Components::SecureAssociation
|
262
|
-
# Automatic features:
|
263
|
-
# - SGID-based value encoding for security.
|
264
|
-
# - Authorization checks before showing options.
|
265
|
-
# - "Add new record" button with `return_to` handling.
|
266
|
-
# - Polymorphic association support.
|
267
|
-
# - Search and filtering (via SlimSelect).
|
268
|
-
|
269
293
|
def choices
|
270
294
|
collection = if @skip_authorization
|
271
|
-
|
295
|
+
# ...
|
272
296
|
else
|
273
297
|
# Only show records user is authorized to see
|
274
|
-
authorized_resource_scope(
|
275
|
-
|
276
|
-
with: @scope_with,
|
277
|
-
context: @scope_context
|
278
|
-
)
|
298
|
+
authorized_resource_scope(association_reflection.klass,
|
299
|
+
relation: choices_from_association(association_reflection.klass))
|
279
300
|
end
|
280
301
|
# ...
|
281
302
|
end
|
@@ -283,29 +304,13 @@ end
|
|
283
304
|
```
|
284
305
|
:::
|
285
306
|
|
286
|
-
## Type Inference
|
307
|
+
## Type Inference System
|
287
308
|
|
288
|
-
|
309
|
+
Plutonium is smart about choosing the right input for a given field, minimizing boilerplate in your forms.
|
289
310
|
|
290
|
-
|
311
|
+
### Automatic Component Selection
|
291
312
|
|
292
|
-
|
293
|
-
# Automatic inference based on Active Record column types
|
294
|
-
render field(:title).input_tag # → input_tag (string)
|
295
|
-
render field(:content).easymde_tag # → easymde_tag (text/rich_text)
|
296
|
-
render field(:published_at).flatpickr_tag # → flatpickr_tag (datetime)
|
297
|
-
render field(:author).secure_association_tag # → secure_association_tag (belongs_to)
|
298
|
-
render field(:featured_image).uppy_tag # → uppy_tag (Active Storage)
|
299
|
-
render field(:category).slim_select_tag # → slim_select_tag (select)
|
300
|
-
|
301
|
-
# Manual override
|
302
|
-
render field(:title).input_tag(as: :string)
|
303
|
-
render field(:content).easymde_tag
|
304
|
-
render field(:published_at).flatpickr_tag
|
305
|
-
render field(:documents).uppy_tag(multiple: true)
|
306
|
-
```
|
307
|
-
|
308
|
-
### Type Mapping
|
313
|
+
The `InferredTypes` module overrides the default type inference to map common types to Plutonium's enhanced components.
|
309
314
|
|
310
315
|
```ruby
|
311
316
|
module Plutonium::UI::Form::Options::InferredTypes
|
@@ -314,292 +319,147 @@ module Plutonium::UI::Form::Options::InferredTypes
|
|
314
319
|
def infer_field_component
|
315
320
|
case inferred_field_type
|
316
321
|
when :rich_text
|
317
|
-
:markdown # Use
|
322
|
+
return :markdown # Use Easymde for ActionText fields
|
318
323
|
end
|
319
324
|
|
320
|
-
|
321
|
-
case
|
325
|
+
inferred = super
|
326
|
+
case inferred
|
322
327
|
when :select
|
323
|
-
:slim_select
|
328
|
+
:slim_select # Enhance selects with SlimSelect
|
324
329
|
when :date, :time, :datetime
|
325
|
-
:flatpickr
|
330
|
+
:flatpickr # Use Flatpickr for date/time fields
|
326
331
|
else
|
327
|
-
|
332
|
+
inferred
|
328
333
|
end
|
329
334
|
end
|
330
335
|
end
|
331
336
|
```
|
332
337
|
|
333
|
-
|
334
|
-
|
335
|
-
### Form Theme (`lib/plutonium/ui/form/theme.rb`)
|
336
|
-
|
337
|
-
Comprehensive theming for consistent form appearance:
|
338
|
+
This means you often don't need to specify the input type at all.
|
338
339
|
|
339
340
|
```ruby
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
341
|
+
# These are automatically inferred:
|
342
|
+
render field(:title) # -> input (string)
|
343
|
+
render field(:content) # -> easymde (rich_text)
|
344
|
+
render field(:published_at) # -> flatpickr (datetime)
|
345
|
+
render field(:phone) # -> int_tel_input (tel)
|
346
|
+
render field(:author) # -> secure_association (belongs_to)
|
347
|
+
render field(:avatar) # -> uppy (Active Storage attachment)
|
348
|
+
render field(:category) # -> slim_select (enum/select)
|
349
|
+
```
|
346
350
|
|
347
|
-
|
348
|
-
input: "w-full border rounded-md shadow-sm px-3 py-2 border-gray-300 dark:border-gray-600 focus:ring-primary-500 focus:border-primary-500",
|
349
|
-
textarea: "w-full border rounded-md shadow-sm px-3 py-2 border-gray-300 dark:border-gray-600 focus:ring-primary-500 focus:border-primary-500",
|
350
|
-
select: "w-full border rounded-md shadow-sm px-3 py-2 border-gray-300 dark:border-gray-600 focus:ring-primary-500 focus:border-primary-500",
|
351
|
+
## Nested Resources
|
351
352
|
|
352
|
-
|
353
|
-
flatpickr: :input,
|
354
|
-
int_tel_input: :input,
|
355
|
-
easymde: "w-full border rounded-md border-gray-300 dark:border-gray-600",
|
356
|
-
uppy: "w-full border rounded-md border-gray-300 dark:border-gray-600",
|
353
|
+
Plutonium has first-class support for `accepts_nested_attributes_for`, allowing you to build complex forms with nested records. This is handled by the `RendersNestedResourceFields` concern in `Form::Resource`.
|
357
354
|
|
358
|
-
|
359
|
-
association: :select,
|
355
|
+
### Defining Nested Inputs
|
360
356
|
|
361
|
-
|
362
|
-
file: "w-full border rounded-md shadow-sm font-medium text-sm border-gray-300 dark:border-gray-600",
|
357
|
+
You define nested inputs in your resource definition file. Plutonium will automatically detect the configuration from your Rails model's `accepts_nested_attributes_for` declaration—including options like `allow_destroy`, `update_only`, and `limit`—and use them to render the appropriate form controls.
|
363
358
|
|
364
|
-
|
365
|
-
valid_input: "border-green-500 focus:ring-green-500 focus:border-green-500",
|
366
|
-
invalid_input: "border-red-500 focus:ring-red-500 focus:border-red-500",
|
359
|
+
You can declare a nested input with a simple block or by referencing another definition class.
|
367
360
|
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
361
|
+
::: code-group
|
362
|
+
```ruby [Block Definition]
|
363
|
+
# app/models/post.rb
|
364
|
+
class Post < ApplicationRecord
|
365
|
+
has_many :comments
|
366
|
+
accepts_nested_attributes_for :comments, allow_destroy: true, limit: 5
|
374
367
|
end
|
375
|
-
```
|
376
368
|
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
class ContactForm < Plutonium::UI::Form::Base
|
384
|
-
def form_template
|
385
|
-
render field(:name).input_tag(as: :string)
|
386
|
-
render field(:email).input_tag(as: :email)
|
387
|
-
render field(:message).textarea_tag
|
388
|
-
render field(:phone).int_tel_input_tag
|
369
|
+
# app/definitions/post_definition.rb
|
370
|
+
class PostDefinition < Plutonium::Resource::Definition
|
371
|
+
# This automatically inherits allow_destroy: true and limit: 5 from the model
|
372
|
+
nested_input :comments do |n|
|
373
|
+
n.input :content, as: :textarea
|
374
|
+
n.input :author_name, as: :string
|
389
375
|
end
|
390
376
|
end
|
391
377
|
```
|
392
378
|
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
def form_template
|
400
|
-
# Basic field rendering
|
401
|
-
render field(:title).input_tag
|
402
|
-
|
403
|
-
# Field with wrapper styling
|
404
|
-
render field(:content).wrapped(class: "col-span-full") do |f|
|
405
|
-
render f.easymde_tag
|
406
|
-
end
|
379
|
+
```ruby [Reference Definition]
|
380
|
+
# app/models/post.rb
|
381
|
+
class Post < ApplicationRecord
|
382
|
+
has_many :tags
|
383
|
+
accepts_nested_attributes_for :tags, update_only: true
|
384
|
+
end
|
407
385
|
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
end
|
415
|
-
end
|
386
|
+
# app/definitions/post_definition.rb
|
387
|
+
class PostDefinition < Plutonium::Resource::Definition
|
388
|
+
# This inherits update_only: true from the model
|
389
|
+
nested_input :tags,
|
390
|
+
using: TagDefinition,
|
391
|
+
fields: %i[name color]
|
416
392
|
end
|
417
393
|
```
|
394
|
+
:::
|
418
395
|
|
419
|
-
###
|
396
|
+
### Overriding Configuration
|
420
397
|
|
421
|
-
|
422
|
-
# Automatic resource form based on definition
|
423
|
-
class PostsController < ApplicationController
|
424
|
-
def new
|
425
|
-
@post = Post.new
|
426
|
-
@form = Plutonium::UI::Form::Resource.new(
|
427
|
-
@post,
|
428
|
-
resource_definition: current_definition
|
429
|
-
)
|
430
|
-
end
|
398
|
+
While Plutonium automatically uses your Rails configuration, you can easily override it by passing options directly to the `nested_input` method. Explicit options always take precedence.
|
431
399
|
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
400
|
+
```ruby
|
401
|
+
class PostDefinition < Plutonium::Resource::Definition
|
402
|
+
# Explicit options override the model's configuration
|
403
|
+
nested_input :comments,
|
404
|
+
allow_destroy: false, # Overrides model's allow_destroy: true
|
405
|
+
limit: 10, # Overrides model's limit: 5
|
406
|
+
description: "Add up to 10 comments for this post." do |n|
|
407
|
+
n.input :content
|
438
408
|
end
|
439
409
|
end
|
440
|
-
|
441
|
-
# In view
|
442
|
-
<%= render @form %>
|
443
410
|
```
|
444
411
|
|
445
|
-
###
|
412
|
+
### Automatic Rendering
|
446
413
|
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
input(**attributes, type: :color)
|
453
|
-
input(**color_text_attributes, type: :text, placeholder: "#000000")
|
454
|
-
end
|
455
|
-
end
|
456
|
-
|
457
|
-
private
|
414
|
+
The `Form::Resource` class automatically renders the nested form based on your definition:
|
415
|
+
- For `has_many` associations, it provides "Add" and "Remove" buttons, respecting the `limit`.
|
416
|
+
- For `has_one` and `belongs_to` associations, it renders inline fields for a single record.
|
417
|
+
- If `allow_destroy: true`, a "Delete" checkbox is rendered for persisted records.
|
418
|
+
- If `update_only: true`, the "Add" button is hidden.
|
458
419
|
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
data: { color_picker_target: "text" }
|
463
|
-
)
|
464
|
-
end
|
465
|
-
end
|
420
|
+
::: details Nested Rendering Internals
|
421
|
+
The `render_nested_resource_field` method orchestrates the rendering of the nested form, including the header, existing records, the "add" button, and the template for new records. This is all managed by the `nested-resource-form-fields` Stimulus controller.
|
422
|
+
:::
|
466
423
|
|
467
|
-
|
468
|
-
class CustomFormBuilder < Plutonium::UI::Form::Base::Builder
|
469
|
-
def color_picker_tag(**options, &block)
|
470
|
-
create_component(ColorPickerComponent, :color_picker, **options, &block)
|
471
|
-
end
|
472
|
-
end
|
424
|
+
## Theming
|
473
425
|
|
474
|
-
|
475
|
-
render field(:brand_color).color_picker_tag
|
476
|
-
```
|
426
|
+
Forms are styled using a comprehensive theme system that leverages Tailwind CSS utility classes. The theme is defined in `lib/plutonium/ui/form/theme.rb`.
|
477
427
|
|
478
|
-
|
428
|
+
::: details Form Theme Configuration
|
429
|
+
The `Plutonium::UI::Form::Theme.theme` method returns a hash where keys represent form elements (like `input`, `label`, `error`) and values are the corresponding CSS classes. It includes styles for layout, inputs in different states (valid, invalid), and all custom components.
|
479
430
|
|
480
431
|
```ruby
|
481
|
-
class
|
482
|
-
def
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
comment_form.field(:content).textarea_tag
|
489
|
-
comment_form.field(:author_name).input_tag(as: :string)
|
490
|
-
end
|
491
|
-
end
|
492
|
-
end
|
493
|
-
```
|
494
|
-
|
495
|
-
## JavaScript Integration
|
496
|
-
|
497
|
-
### Automatic Dependencies
|
498
|
-
|
499
|
-
Plutonium automatically includes JavaScript libraries for enhanced form components:
|
500
|
-
- **EasyMDE** for markdown editing
|
501
|
-
- **Flatpickr** for date/time picking
|
502
|
-
- **Intl-Tel-Input** for phone inputs
|
503
|
-
- **Uppy** for file uploads
|
504
|
-
|
505
|
-
### Stimulus Controllers
|
506
|
-
|
507
|
-
Each enhanced component uses a Stimulus controller for initialization and cleanup:
|
508
|
-
|
509
|
-
```javascript
|
510
|
-
// easymde_controller.js
|
511
|
-
export default class extends Controller {
|
512
|
-
connect() {
|
513
|
-
this.editor = new EasyMDE({
|
514
|
-
element: this.element,
|
515
|
-
spellChecker: false,
|
516
|
-
toolbar: ["bold", "italic", "heading", "|", "quote"]
|
517
|
-
});
|
518
|
-
}
|
519
|
-
|
520
|
-
disconnect() {
|
521
|
-
if (this.editor) {
|
522
|
-
this.editor.toTextArea();
|
523
|
-
this.editor = null;
|
524
|
-
}
|
525
|
-
}
|
526
|
-
}
|
527
|
-
|
528
|
-
// flatpickr_controller.js
|
529
|
-
export default class extends Controller {
|
530
|
-
connect() {
|
531
|
-
this.picker = new flatpickr(this.element, this.#buildOptions());
|
532
|
-
}
|
533
|
-
|
534
|
-
disconnect() {
|
535
|
-
if (this.picker) {
|
536
|
-
this.picker.destroy();
|
537
|
-
this.picker = null;
|
538
|
-
}
|
539
|
-
}
|
540
|
-
|
541
|
-
#buildOptions() {
|
542
|
-
let options = { altInput: true };
|
543
|
-
if (this.element.attributes.type.value == "datetime-local") {
|
544
|
-
options.enableTime = true;
|
545
|
-
} else if (this.element.attributes.type.value == "time") {
|
546
|
-
options.enableTime = true;
|
547
|
-
options.noCalendar = true;
|
548
|
-
}
|
549
|
-
return options;
|
550
|
-
}
|
551
|
-
}
|
552
|
-
```
|
553
|
-
|
554
|
-
## Advanced Features
|
432
|
+
class Plutonium::UI::Form::Theme < Phlexi::Form::Theme
|
433
|
+
def self.theme
|
434
|
+
super.merge({
|
435
|
+
# Layout
|
436
|
+
base: "relative bg-white dark:bg-gray-800 shadow-md sm:rounded-lg my-3 p-6 space-y-6",
|
437
|
+
fields_wrapper: "grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-4 gap-4",
|
438
|
+
actions_wrapper: "flex justify-end space-x-2",
|
555
439
|
|
556
|
-
|
440
|
+
# Input styling
|
441
|
+
input: "w-full p-2 border rounded-md shadow-sm dark:bg-gray-700 focus:ring-2",
|
442
|
+
valid_input: "bg-green-50 border-green-500 ...",
|
443
|
+
invalid_input: "bg-red-50 border-red-500 ...",
|
557
444
|
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
maxlength: 100
|
566
|
-
)
|
567
|
-
|
568
|
-
render field(:email).input_tag(
|
569
|
-
type: :email,
|
570
|
-
pattern: "[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$"
|
571
|
-
)
|
572
|
-
|
573
|
-
# Custom validation with JavaScript
|
574
|
-
render field(:password).input_tag(
|
575
|
-
type: :password,
|
576
|
-
data: {
|
577
|
-
controller: "password-validator",
|
578
|
-
action: "input->password-validator#validate"
|
579
|
-
}
|
580
|
-
)
|
445
|
+
# Enhanced component themes (aliases to base styles)
|
446
|
+
flatpickr: :input,
|
447
|
+
int_tel_input: :input,
|
448
|
+
uppy: :file,
|
449
|
+
association: :select,
|
450
|
+
# ...
|
451
|
+
})
|
581
452
|
end
|
582
453
|
end
|
583
454
|
```
|
455
|
+
:::
|
584
456
|
|
585
|
-
|
457
|
+
## JavaScript & Stimulus
|
586
458
|
|
587
|
-
|
459
|
+
Interactivity is powered by a set of dedicated Stimulus controllers. Plutonium automatically loads these controllers and the required third-party libraries.
|
588
460
|
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
# This input will only be shown if the `condition` evaluates to true.
|
596
|
-
# The condition is re-evaluated after a pre-submit refresh.
|
597
|
-
input :notification_channel,
|
598
|
-
as: :select,
|
599
|
-
collection: %w[Email SMS Push],
|
600
|
-
condition: -> { object.send_notifications? }
|
601
|
-
end
|
602
|
-
```
|
603
|
-
::: tip
|
604
|
-
For more details on how to configure conditional inputs, see the [Definition Module documentation](./definition.md#4-add-conditional-logic).
|
605
|
-
:::
|
461
|
+
- **`form`**: The main controller for handling pre-submit refreshes (for conditional fields).
|
462
|
+
- **`nested-resource-form-fields`**: Manages adding and removing nested form fields dynamically.
|
463
|
+
- **`slim-select`**: Initializes the SlimSelect library on select fields.
|
464
|
+
- **`easymde`**, **`flatpickr`**, **`intl-tel-input`**: Controllers for their respective input components.
|
465
|
+
- **`attachment-input`** & **`attachment-preview`**: Work together to manage the Uppy file upload experience.
|