plutonium 0.23.5 → 0.24.1
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/definition.md +44 -14
- data/docs/modules/form.md +313 -423
- data/docs/public/plutonium.mdc +57 -172
- data/lib/plutonium/definition/nested_inputs.rb +0 -8
- data/lib/plutonium/resource/controller.rb +1 -1
- data/lib/plutonium/ui/display/resource.rb +5 -1
- data/lib/plutonium/ui/form/resource.rb +5 -1
- data/lib/plutonium/version.rb +1 -1
- metadata +2 -2
data/docs/modules/form.md
CHANGED
@@ -4,278 +4,329 @@ 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.
|
126
|
+
|
127
|
+
### Input Block Syntax
|
86
128
|
|
87
|
-
|
129
|
+
Input blocks provide additional flexibility for custom logic while ensuring proper form integration. **Important**: Input blocks can only use existing form builder methods (like `date_tag`, `text_tag`, etc.) because the form system requires inputs to be registered internally.
|
88
130
|
|
89
131
|
::: code-group
|
90
|
-
```ruby [
|
91
|
-
#
|
92
|
-
|
132
|
+
```ruby [Input Block Example]
|
133
|
+
# Valid: Using form builder methods with custom logic
|
134
|
+
input :birth_date do |f|
|
135
|
+
case object.age_category
|
136
|
+
when 'adult'
|
137
|
+
f.date_tag(min: 18.years.ago.to_date)
|
138
|
+
when 'minor'
|
139
|
+
f.date_tag(max: 18.years.ago.to_date)
|
140
|
+
else
|
141
|
+
f.date_tag
|
142
|
+
end
|
143
|
+
end
|
93
144
|
```
|
94
145
|
|
95
|
-
```ruby [
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
146
|
+
```ruby [Custom Component Classes]
|
147
|
+
# New: Pass component classes directly to as: option
|
148
|
+
# Works for both input and display
|
149
|
+
input :color_picker, as: ColorPickerComponent
|
150
|
+
input :custom_slider, as: RangeSliderComponent
|
151
|
+
|
152
|
+
display :status_badge, as: StatusBadgeComponent
|
153
|
+
display :progress_chart, as: ProgressChartComponent
|
101
154
|
```
|
102
155
|
:::
|
103
156
|
|
104
|
-
###
|
157
|
+
### Markdown Editor (Easymde)
|
105
158
|
|
106
|
-
|
159
|
+
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.
|
107
160
|
|
108
161
|
::: code-group
|
109
|
-
```ruby [
|
110
|
-
# Automatically used for
|
111
|
-
render field(:
|
112
|
-
|
113
|
-
|
114
|
-
|
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
|
-
)
|
162
|
+
```ruby [Automatic Usage]
|
163
|
+
# Automatically used for ActionText rich_text fields
|
164
|
+
render field(:content).easymde_tag
|
165
|
+
|
166
|
+
# Or explicitly with an alias
|
167
|
+
render field(:description).markdown_tag
|
126
168
|
```
|
127
169
|
:::
|
128
170
|
|
129
|
-
:::
|
130
|
-
The
|
131
|
-
- **Date fields**: Basic date picker with `altInput: true`
|
132
|
-
- **Time fields**: Time picker with `enableTime: true, noCalendar: true`
|
133
|
-
- **Datetime fields**: Date and time picker with `enableTime: true`
|
171
|
+
::: details Component Internals
|
172
|
+
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.
|
134
173
|
|
135
|
-
|
174
|
+
```ruby
|
175
|
+
class Plutonium::UI::Form::Components::Easymde < Phlexi::Form::Components::Base
|
176
|
+
def view_template
|
177
|
+
textarea(**attributes, data_controller: "easymde") do
|
178
|
+
normalize_value(field.value)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
# ...
|
182
|
+
end
|
183
|
+
```
|
136
184
|
:::
|
137
185
|
|
138
|
-
###
|
186
|
+
### Date/Time Picker (Flatpickr)
|
139
187
|
|
140
|
-
A
|
188
|
+
A beautiful and lightweight date/time picker from [Flatpickr](https://flatpickr.js.org/). It's automatically enabled for `date`, `time`, and `datetime` fields.
|
141
189
|
|
142
190
|
::: code-group
|
143
|
-
```ruby [
|
144
|
-
# Automatically used
|
145
|
-
render field(:
|
146
|
-
|
147
|
-
|
148
|
-
render field(:documents).uppy_tag(multiple: true)
|
191
|
+
```ruby [Automatic Usage]
|
192
|
+
# Automatically used based on field type
|
193
|
+
render field(:published_at).flatpickr_tag # datetime field
|
194
|
+
render field(:event_date).flatpickr_tag # date field
|
195
|
+
render field(:meeting_time).flatpickr_tag # time field
|
149
196
|
```
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
197
|
+
:::
|
198
|
+
|
199
|
+
::: details Component Internals
|
200
|
+
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).
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
class Plutonium::UI::Form::Components::Flatpickr < Phlexi::Form::Components::Input
|
204
|
+
private
|
205
|
+
|
206
|
+
def build_input_attributes
|
207
|
+
super
|
208
|
+
attributes[:data_controller] = tokens(attributes[:data_controller], :flatpickr)
|
209
|
+
end
|
210
|
+
end
|
156
211
|
```
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
)
|
212
|
+
:::
|
213
|
+
|
214
|
+
### International Phone Input
|
215
|
+
|
216
|
+
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).
|
217
|
+
|
218
|
+
::: code-group
|
219
|
+
```ruby [Usage]
|
220
|
+
# Automatically used for fields of type :tel
|
221
|
+
render field(:phone).int_tel_input_tag
|
222
|
+
|
223
|
+
# Or using its alias
|
224
|
+
render field(:mobile).phone_tag
|
162
225
|
```
|
163
226
|
:::
|
164
227
|
|
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
|
228
|
+
::: details Component Internals
|
229
|
+
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
230
|
|
231
|
+
```ruby
|
232
|
+
class Plutonium::UI::Form::Components::IntlTelInput < Phlexi::Form::Components::Input
|
177
233
|
def view_template
|
178
|
-
div(
|
179
|
-
|
180
|
-
render_upload_interface
|
234
|
+
div(data_controller: "intl-tel-input") do
|
235
|
+
super # Renders the input with proper data targets
|
181
236
|
end
|
182
237
|
end
|
183
238
|
|
184
239
|
private
|
185
240
|
|
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
|
241
|
+
def build_input_attributes
|
242
|
+
super
|
243
|
+
attributes[:data_intl_tel_input_target] = tokens(attributes[:data_intl_tel_input_target], :input)
|
199
244
|
end
|
200
245
|
end
|
201
246
|
```
|
202
247
|
:::
|
203
248
|
|
204
|
-
###
|
249
|
+
### File Upload (Uppy)
|
205
250
|
|
206
|
-
|
251
|
+
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
252
|
|
208
253
|
::: code-group
|
209
254
|
```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
|
255
|
+
# Automatically used for file and Active Storage attachments
|
256
|
+
render field(:avatar).uppy_tag
|
257
|
+
render field(:documents).file_tag # alias
|
258
|
+
render field(:gallery).attachment_tag # alias
|
216
259
|
```
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
260
|
+
|
261
|
+
```ruby [With Options]
|
262
|
+
render field(:documents).uppy_tag(
|
263
|
+
multiple: true,
|
264
|
+
direct_upload: true, # For S3, etc.
|
265
|
+
max_file_size: 10.megabytes,
|
266
|
+
allowed_file_types: ['.pdf', '.doc']
|
221
267
|
)
|
222
268
|
```
|
223
269
|
:::
|
224
270
|
|
225
|
-
:::
|
226
|
-
The
|
227
|
-
|
228
|
-
|
229
|
-
|
271
|
+
::: details Component Internals
|
272
|
+
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.
|
273
|
+
|
274
|
+
```ruby
|
275
|
+
class Plutonium::UI::Form::Components::Uppy < Phlexi::Form::Components::Input
|
276
|
+
# Automatic features:
|
277
|
+
# - Interactive preview of existing attachments
|
278
|
+
# - Delete buttons for removing attachments
|
279
|
+
# - Support for direct cloud uploads
|
280
|
+
# - File type and size validation via Uppy options
|
281
|
+
# ...
|
230
282
|
|
231
|
-
|
283
|
+
def view_template
|
284
|
+
div(class: "flex flex-col-reverse gap-2") do
|
285
|
+
render_existing_attachments
|
286
|
+
render_upload_interface
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
```
|
232
291
|
:::
|
233
292
|
|
234
293
|
### Secure Association Inputs
|
235
294
|
|
236
|
-
Plutonium
|
295
|
+
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
296
|
|
238
297
|
::: code-group
|
239
|
-
```ruby [
|
240
|
-
# Automatically used for
|
298
|
+
```ruby [Association Types]
|
299
|
+
# Automatically used for all standard association types
|
241
300
|
render field(:author).belongs_to_tag
|
242
|
-
```
|
243
|
-
```ruby [Has Many]
|
244
|
-
# Automatically used for has_many associations
|
245
301
|
render field(:tags).has_many_tag
|
302
|
+
render field(:profile).has_one_tag
|
303
|
+
render field(:commentable).polymorphic_belongs_to_tag
|
246
304
|
```
|
247
|
-
|
305
|
+
|
306
|
+
```ruby [With Options]
|
248
307
|
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
|
308
|
+
choices: Category.published.pluck(:name, :id),
|
309
|
+
add_action: new_category_path, # Adds a "+" button to add new records
|
310
|
+
skip_authorization: false # Enforces authorization policies
|
255
311
|
)
|
256
312
|
```
|
257
313
|
:::
|
258
314
|
|
259
|
-
::: details
|
315
|
+
::: details Security & Implementation
|
316
|
+
The `SecureAssociation` component is the cornerstone of Plutonium's form security.
|
317
|
+
- **SGID Encoding**: It uses `to_signed_global_id` as the value method, so raw database IDs are never exposed to the client.
|
318
|
+
- **Authorization**: It uses `authorized_resource_scope` to ensure that the choices presented to the user are only the ones they are permitted to see.
|
319
|
+
- **Add Action**: It can render an "add new" button that automatically includes a `return_to` parameter for a smooth UX.
|
320
|
+
|
260
321
|
```ruby
|
261
322
|
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
323
|
def choices
|
270
324
|
collection = if @skip_authorization
|
271
|
-
|
325
|
+
# ...
|
272
326
|
else
|
273
327
|
# Only show records user is authorized to see
|
274
|
-
authorized_resource_scope(
|
275
|
-
|
276
|
-
with: @scope_with,
|
277
|
-
context: @scope_context
|
278
|
-
)
|
328
|
+
authorized_resource_scope(association_reflection.klass,
|
329
|
+
relation: choices_from_association(association_reflection.klass))
|
279
330
|
end
|
280
331
|
# ...
|
281
332
|
end
|
@@ -283,29 +334,13 @@ end
|
|
283
334
|
```
|
284
335
|
:::
|
285
336
|
|
286
|
-
## Type Inference
|
287
|
-
|
288
|
-
### Automatic Component Selection (`lib/plutonium/ui/form/options/inferred_types.rb`)
|
337
|
+
## Type Inference System
|
289
338
|
|
290
|
-
|
339
|
+
Plutonium is smart about choosing the right input for a given field, minimizing boilerplate in your forms.
|
291
340
|
|
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
|
-
```
|
341
|
+
### Automatic Component Selection
|
307
342
|
|
308
|
-
|
343
|
+
The `InferredTypes` module overrides the default type inference to map common types to Plutonium's enhanced components.
|
309
344
|
|
310
345
|
```ruby
|
311
346
|
module Plutonium::UI::Form::Options::InferredTypes
|
@@ -314,292 +349,147 @@ module Plutonium::UI::Form::Options::InferredTypes
|
|
314
349
|
def infer_field_component
|
315
350
|
case inferred_field_type
|
316
351
|
when :rich_text
|
317
|
-
:markdown # Use
|
352
|
+
return :markdown # Use Easymde for ActionText fields
|
318
353
|
end
|
319
354
|
|
320
|
-
|
321
|
-
case
|
355
|
+
inferred = super
|
356
|
+
case inferred
|
322
357
|
when :select
|
323
|
-
:slim_select
|
358
|
+
:slim_select # Enhance selects with SlimSelect
|
324
359
|
when :date, :time, :datetime
|
325
|
-
:flatpickr
|
360
|
+
:flatpickr # Use Flatpickr for date/time fields
|
326
361
|
else
|
327
|
-
|
362
|
+
inferred
|
328
363
|
end
|
329
364
|
end
|
330
365
|
end
|
331
366
|
```
|
332
367
|
|
333
|
-
|
334
|
-
|
335
|
-
### Form Theme (`lib/plutonium/ui/form/theme.rb`)
|
336
|
-
|
337
|
-
Comprehensive theming for consistent form appearance:
|
368
|
+
This means you often don't need to specify the input type at all.
|
338
369
|
|
339
370
|
```ruby
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
371
|
+
# These are automatically inferred:
|
372
|
+
render field(:title) # -> input (string)
|
373
|
+
render field(:content) # -> easymde (rich_text)
|
374
|
+
render field(:published_at) # -> flatpickr (datetime)
|
375
|
+
render field(:phone) # -> int_tel_input (tel)
|
376
|
+
render field(:author) # -> secure_association (belongs_to)
|
377
|
+
render field(:avatar) # -> uppy (Active Storage attachment)
|
378
|
+
render field(:category) # -> slim_select (enum/select)
|
379
|
+
```
|
346
380
|
|
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",
|
381
|
+
## Nested Resources
|
351
382
|
|
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",
|
383
|
+
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
384
|
|
358
|
-
|
359
|
-
association: :select,
|
385
|
+
### Defining Nested Inputs
|
360
386
|
|
361
|
-
|
362
|
-
file: "w-full border rounded-md shadow-sm font-medium text-sm border-gray-300 dark:border-gray-600",
|
387
|
+
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
388
|
|
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",
|
389
|
+
You can declare a nested input with a simple block or by referencing another definition class.
|
367
390
|
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
391
|
+
::: code-group
|
392
|
+
```ruby [Block Definition]
|
393
|
+
# app/models/post.rb
|
394
|
+
class Post < ApplicationRecord
|
395
|
+
has_many :comments
|
396
|
+
accepts_nested_attributes_for :comments, allow_destroy: true, limit: 5
|
374
397
|
end
|
375
|
-
```
|
376
|
-
|
377
|
-
## Usage Patterns
|
378
|
-
|
379
|
-
### Basic Form
|
380
398
|
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
render field(:message).textarea_tag
|
388
|
-
render field(:phone).int_tel_input_tag
|
399
|
+
# app/definitions/post_definition.rb
|
400
|
+
class PostDefinition < Plutonium::Resource::Definition
|
401
|
+
# This automatically inherits allow_destroy: true and limit: 5 from the model
|
402
|
+
nested_input :comments do |n|
|
403
|
+
n.input :content, as: :textarea
|
404
|
+
n.input :author_name, as: :string
|
389
405
|
end
|
390
406
|
end
|
391
407
|
```
|
392
408
|
|
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
|
409
|
+
```ruby [Reference Definition]
|
410
|
+
# app/models/post.rb
|
411
|
+
class Post < ApplicationRecord
|
412
|
+
has_many :tags
|
413
|
+
accepts_nested_attributes_for :tags, update_only: true
|
414
|
+
end
|
407
415
|
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
end
|
415
|
-
end
|
416
|
+
# app/definitions/post_definition.rb
|
417
|
+
class PostDefinition < Plutonium::Resource::Definition
|
418
|
+
# This inherits update_only: true from the model
|
419
|
+
nested_input :tags,
|
420
|
+
using: TagDefinition,
|
421
|
+
fields: %i[name color]
|
416
422
|
end
|
417
423
|
```
|
424
|
+
:::
|
418
425
|
|
419
|
-
###
|
426
|
+
### Overriding Configuration
|
420
427
|
|
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
|
428
|
+
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
429
|
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
430
|
+
```ruby
|
431
|
+
class PostDefinition < Plutonium::Resource::Definition
|
432
|
+
# Explicit options override the model's configuration
|
433
|
+
nested_input :comments,
|
434
|
+
allow_destroy: false, # Overrides model's allow_destroy: true
|
435
|
+
limit: 10, # Overrides model's limit: 5
|
436
|
+
description: "Add up to 10 comments for this post." do |n|
|
437
|
+
n.input :content
|
438
438
|
end
|
439
439
|
end
|
440
|
-
|
441
|
-
# In view
|
442
|
-
<%= render @form %>
|
443
440
|
```
|
444
441
|
|
445
|
-
###
|
446
|
-
|
447
|
-
```ruby
|
448
|
-
# Create custom input component
|
449
|
-
class ColorPickerComponent < Plutonium::UI::Form::Components::Input
|
450
|
-
def view_template
|
451
|
-
div(data: { controller: "color-picker" }) do
|
452
|
-
input(**attributes, type: :color)
|
453
|
-
input(**color_text_attributes, type: :text, placeholder: "#000000")
|
454
|
-
end
|
455
|
-
end
|
442
|
+
### Automatic Rendering
|
456
443
|
|
457
|
-
|
444
|
+
The `Form::Resource` class automatically renders the nested form based on your definition:
|
445
|
+
- For `has_many` associations, it provides "Add" and "Remove" buttons, respecting the `limit`.
|
446
|
+
- For `has_one` and `belongs_to` associations, it renders inline fields for a single record.
|
447
|
+
- If `allow_destroy: true`, a "Delete" checkbox is rendered for persisted records.
|
448
|
+
- If `update_only: true`, the "Add" button is hidden.
|
458
449
|
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
data: { color_picker_target: "text" }
|
463
|
-
)
|
464
|
-
end
|
465
|
-
end
|
450
|
+
::: details Nested Rendering Internals
|
451
|
+
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.
|
452
|
+
:::
|
466
453
|
|
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
|
454
|
+
## Theming
|
473
455
|
|
474
|
-
|
475
|
-
render field(:brand_color).color_picker_tag
|
476
|
-
```
|
456
|
+
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
457
|
|
478
|
-
|
458
|
+
::: details Form Theme Configuration
|
459
|
+
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
460
|
|
480
461
|
```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
|
462
|
+
class Plutonium::UI::Form::Theme < Phlexi::Form::Theme
|
463
|
+
def self.theme
|
464
|
+
super.merge({
|
465
|
+
# Layout
|
466
|
+
base: "relative bg-white dark:bg-gray-800 shadow-md sm:rounded-lg my-3 p-6 space-y-6",
|
467
|
+
fields_wrapper: "grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-4 gap-4",
|
468
|
+
actions_wrapper: "flex justify-end space-x-2",
|
555
469
|
|
556
|
-
|
470
|
+
# Input styling
|
471
|
+
input: "w-full p-2 border rounded-md shadow-sm dark:bg-gray-700 focus:ring-2",
|
472
|
+
valid_input: "bg-green-50 border-green-500 ...",
|
473
|
+
invalid_input: "bg-red-50 border-red-500 ...",
|
557
474
|
|
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
|
-
)
|
475
|
+
# Enhanced component themes (aliases to base styles)
|
476
|
+
flatpickr: :input,
|
477
|
+
int_tel_input: :input,
|
478
|
+
uppy: :file,
|
479
|
+
association: :select,
|
480
|
+
# ...
|
481
|
+
})
|
581
482
|
end
|
582
483
|
end
|
583
484
|
```
|
485
|
+
:::
|
584
486
|
|
585
|
-
|
487
|
+
## JavaScript & Stimulus
|
586
488
|
|
587
|
-
|
489
|
+
Interactivity is powered by a set of dedicated Stimulus controllers. Plutonium automatically loads these controllers and the required third-party libraries.
|
588
490
|
|
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
|
-
:::
|
491
|
+
- **`form`**: The main controller for handling pre-submit refreshes (for conditional fields).
|
492
|
+
- **`nested-resource-form-fields`**: Manages adding and removing nested form fields dynamically.
|
493
|
+
- **`slim-select`**: Initializes the SlimSelect library on select fields.
|
494
|
+
- **`easymde`**, **`flatpickr`**, **`intl-tel-input`**: Controllers for their respective input components.
|
495
|
+
- **`attachment-input`** & **`attachment-preview`**: Work together to manage the Uppy file upload experience.
|