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.
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 provides a comprehensive form building system for Plutonium applications. Built on top of `Phlexi::Form`, it offers enhanced input components, automatic field inference, secure associations, and modern UI interactions for creating rich, accessible forms.
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
- ## Overview
13
+ ## Key Features
14
14
 
15
- - **Enhanced Input Components**: Rich input types with JavaScript integration.
16
- - **Secure Associations**: SGID-based association handling with authorization.
17
- - **Type Inference**: Automatic component selection based on field types.
18
- - **Resource Integration**: Seamless integration with resource definitions.
19
- - **Modern UI**: File uploads, date pickers, rich text editors, and more.
20
- - **Accessibility**: ARIA-compliant forms with keyboard navigation.
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 Components
22
+ ## Core Form Classes
23
23
 
24
- ### Base Form (`lib/plutonium/ui/form/base.rb`)
24
+ Plutonium provides several base form classes, each tailored for a specific purpose.
25
25
 
26
- This is the foundation that all Plutonium form components inherit from. It extends `Phlexi::Form::Base` with Plutonium's specific behaviors and custom input components.
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
- include Plutonium::UI::Component::Behaviour
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
- def easymde_tag(**options, &block)
38
- create_component(Plutonium::UI::Form::Components::Easymde, :easymde, **options, &block)
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(**options, &block)
43
- create_component(Components::Flatpickr, :flatpickr, **options, &block)
44
- end
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(**options, &block)
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 secure_association_tag(**attributes, &block)
53
- create_component(Components::SecureAssociation, :association, **attributes, &block)
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 to use secure versions
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 Form (`lib/plutonium/ui/form/resource.rb`)
65
+ ### `Form::Resource`
66
66
 
67
- This is a specialized form for resource objects that automatically renders fields based on the resource's definition, handling nested resources and actions gracefully.
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 PostForm < Plutonium::UI::Form::Resource
71
- def initialize(post, resource_definition:)
72
- super(post, resource_definition: resource_definition)
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
- render_resource_fields # Render configured input fields
77
- render_nested_resources # Render nested associations
78
- render_actions # Render submit/cancel buttons
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
- ### Rich Text Editor (Easymde)
125
+ Plutonium replaces standard form inputs with enhanced versions that provide a modern user experience.
126
+
127
+ ### Input Block Syntax
86
128
 
87
- A client-side markdown editor with live preview, based on [EasyMDE](https://github.com/Ionaru/easy-markdown-editor).
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 [Basic Usage]
91
- # Automatically used for :markdown fields
92
- render field(:content).easymde_tag
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 [With Options]
96
- render field(:description).easymde_tag(
97
- toolbar: ["bold", "italic", "heading", "|", "quote"],
98
- spellChecker: false,
99
- autosave: { enabled: true, uniqueId: "post_content" }
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
- ### Date/Time Picker (Flatpickr)
157
+ ### Markdown Editor (Easymde)
105
158
 
106
- A powerful and lightweight date and time picker from [Flatpickr](https://flatpickr.js.org/).
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 [Date Picker]
110
- # Automatically used for :date fields
111
- render field(:published_at).flatpickr_tag
112
- ```
113
- ```ruby [Time Picker]
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
- )
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
- ::: warning Flatpickr Configuration
130
- The current implementation uses automatic configuration based on field type:
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
- Custom Flatpickr options (like `dateFormat`, `mode: "range"`) are not currently supported through tag attributes.
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
- ### File Upload (Uppy)
186
+ ### Date/Time Picker (Flatpickr)
139
187
 
140
- A sleek, modern file uploader powered by [Uppy](https://uppy.io/).
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 [Single File]
144
- # Automatically used for :file or :attachment fields
145
- render field(:avatar).uppy_tag
146
- ```
147
- ```ruby [Multiple Files]
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
- ```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
- )
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
- ```ruby [Direct to Cloud]
158
- render field(:videos).uppy_tag(
159
- direct_upload: true, # For S3, etc.
160
- max_total_size: 100.megabytes
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 Uppy Component Implementation
166
- The Uppy component automatically handles rendering existing attachments and providing an interface to upload new ones.
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(class: "flex flex-col-reverse gap-2") do
179
- render_existing_attachments
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 render_existing_attachments
187
- Array(field.value).each do |attachment|
188
- render_attachment_preview(attachment)
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
- ### International Phone Input
249
+ ### File Upload (Uppy)
205
250
 
206
- A user-friendly phone number input with country code selection, using [intl-tel-input](https://github.com/jackocnr/intl-tel-input).
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 :tel fields
211
- render field(:phone).int_tel_input_tag
212
- ```
213
- ```ruby [Phone Tag Alias]
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
- ```ruby [With HTML Attributes]
218
- render field(:contact_phone).int_tel_input_tag(
219
- class: "custom-phone-input",
220
- placeholder: "Enter phone number"
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
- ::: warning Int Tel Input Configuration
226
- The current implementation uses a fixed configuration:
227
- - **Strict Mode**: Enabled for validation
228
- - **Utils Loading**: Automatically loads validation utilities
229
- - **Hidden Input**: Creates hidden field for form submission
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
- Custom intl-tel-input options (like `onlyCountries`, `preferredCountries`) are not currently supported through tag attributes.
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's association inputs are secure by default, using SGIDs to prevent parameter tampering and scoping options based on user authorization.
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 [Belongs To]
240
- # Automatically used for belongs_to associations
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
- ```ruby [With Custom Choices]
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 Secure Association Implementation
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
- choices_from_association(association_reflection.klass)
325
+ # ...
272
326
  else
273
327
  # Only show records user is authorized to see
274
- authorized_resource_scope(
275
- association_reflection.klass,
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
- The system automatically selects appropriate input components:
339
+ Plutonium is smart about choosing the right input for a given field, minimizing boilerplate in your forms.
291
340
 
292
- ```ruby
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
- ### Type Mapping
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 EasyMDE for rich text
352
+ return :markdown # Use Easymde for ActionText fields
318
353
  end
319
354
 
320
- inferred_component = super
321
- case inferred_component
355
+ inferred = super
356
+ case inferred
322
357
  when :select
323
- :slim_select # Enhance selects with SlimSelect
358
+ :slim_select # Enhance selects with SlimSelect
324
359
  when :date, :time, :datetime
325
- :flatpickr # Use Flatpickr for date/time
360
+ :flatpickr # Use Flatpickr for date/time fields
326
361
  else
327
- inferred_component
362
+ inferred
328
363
  end
329
364
  end
330
365
  end
331
366
  ```
332
367
 
333
- ## Theme System
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
- class Plutonium::UI::Form::Theme < Phlexi::Form::Theme
341
- def self.theme
342
- super.merge({
343
- # Layout
344
- fields_wrapper: "space-y-6",
345
- actions_wrapper: "flex justify-end space-x-3 pt-6 border-t",
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
- # Input styles
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
- # Enhanced components
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
- # Association components
359
- association: :select,
385
+ ### Defining Nested Inputs
360
386
 
361
- # File input
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
- # States
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
- # Labels and hints
369
- label: "block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1",
370
- hint: "mt-2 text-sm text-gray-500 dark:text-gray-200",
371
- error: "mt-2 text-sm text-red-600 dark:text-red-500"
372
- })
373
- end
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
- ```ruby
382
- # Simple form
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
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
- ### Field Rendering and Wrappers
394
-
395
- All fields must be explicitly rendered using the `render` method. Use wrappers to control layout and styling:
396
-
397
- ```ruby
398
- class PostForm < Plutonium::UI::Form::Resource
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
- # Custom wrapper with data attributes
409
- render field(:author).wrapped(
410
- class: "border rounded-lg p-4",
411
- data: { controller: "tooltip" }
412
- ) do |f|
413
- render f.belongs_to_tag
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
- ### Resource Form
426
+ ### Overriding Configuration
420
427
 
421
- ```ruby
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
- def edit
433
- @post = Post.find(params[:id])
434
- @form = Plutonium::UI::Form::Resource.new(
435
- @post,
436
- resource_definition: current_definition
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
- ### Custom Form Components
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
- private
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
- def color_text_attributes
460
- attributes.merge(
461
- name: "#{attributes[:name]}_text",
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
- # Register in form builder
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
- # Use in form
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
- ### Nested Resources
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 PostForm < Plutonium::UI::Form::Resource
482
- def form_template
483
- field(:title).input_tag
484
- field(:content).easymde_tag
485
-
486
- # Nested comments
487
- nested(:comments, allow_destroy: true) do |comment_form|
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
- ### Form Validation
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
- ```ruby
559
- class PostForm < Plutonium::UI::Form::Resource
560
- def form_template
561
- # Client-side validation attributes
562
- render field(:title).input_tag(
563
- required: true,
564
- minlength: 3,
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
- ### Dynamic Forms
487
+ ## JavaScript & Stimulus
586
488
 
587
- The recommended way to create dynamic forms is by using the `condition` and `pre_submit` options in your resource definition file. This keeps the logic declarative and out of custom form classes.
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
- ```ruby
590
- # app/definitions/post_definition.rb
591
- class PostDefinition < Plutonium::Resource::Definition
592
- # This input will trigger a form refresh whenever its value changes.
593
- input :send_notifications, as: :boolean, pre_submit: true
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.