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.
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 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.
86
126
 
87
- A client-side markdown editor with live preview, based on [EasyMDE](https://github.com/Ionaru/easy-markdown-editor).
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 [Basic Usage]
91
- # Automatically used for :markdown fields
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
- ```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
- )
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 powerful and lightweight date and time picker from [Flatpickr](https://flatpickr.js.org/).
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 [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
- )
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
- ::: 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`
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
- Custom Flatpickr options (like `dateFormat`, `mode: "range"`) are not currently supported through tag attributes.
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
- ### File Upload (Uppy)
184
+ ### International Phone Input
139
185
 
140
- A sleek, modern file uploader powered by [Uppy](https://uppy.io/).
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 [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)
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 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
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(class: "flex flex-col-reverse gap-2") do
179
- render_existing_attachments
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 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
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
- ### International Phone Input
219
+ ### File Upload (Uppy)
205
220
 
206
- A user-friendly phone number input with country code selection, using [intl-tel-input](https://github.com/jackocnr/intl-tel-input).
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 :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
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
- ```ruby [With HTML Attributes]
218
- render field(:contact_phone).int_tel_input_tag(
219
- class: "custom-phone-input",
220
- placeholder: "Enter phone number"
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
- ::: 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
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
- Custom intl-tel-input options (like `onlyCountries`, `preferredCountries`) are not currently supported through tag attributes.
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's association inputs are secure by default, using SGIDs to prevent parameter tampering and scoping options based on user authorization.
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 [Belongs To]
240
- # Automatically used for belongs_to associations
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
- ```ruby [With Custom Choices]
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 Secure Association Implementation
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
- choices_from_association(association_reflection.klass)
295
+ # ...
272
296
  else
273
297
  # 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
- )
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
- ### Automatic Component Selection (`lib/plutonium/ui/form/options/inferred_types.rb`)
309
+ Plutonium is smart about choosing the right input for a given field, minimizing boilerplate in your forms.
289
310
 
290
- The system automatically selects appropriate input components:
311
+ ### Automatic Component Selection
291
312
 
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
- ```
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 EasyMDE for rich text
322
+ return :markdown # Use Easymde for ActionText fields
318
323
  end
319
324
 
320
- inferred_component = super
321
- case inferred_component
325
+ inferred = super
326
+ case inferred
322
327
  when :select
323
- :slim_select # Enhance selects with SlimSelect
328
+ :slim_select # Enhance selects with SlimSelect
324
329
  when :date, :time, :datetime
325
- :flatpickr # Use Flatpickr for date/time
330
+ :flatpickr # Use Flatpickr for date/time fields
326
331
  else
327
- inferred_component
332
+ inferred
328
333
  end
329
334
  end
330
335
  end
331
336
  ```
332
337
 
333
- ## Theme System
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
- 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",
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
- # 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",
351
+ ## Nested Resources
351
352
 
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",
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
- # Association components
359
- association: :select,
355
+ ### Defining Nested Inputs
360
356
 
361
- # File input
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
- # 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",
359
+ You can declare a nested input with a simple block or by referencing another definition class.
367
360
 
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
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
- ## Usage Patterns
378
-
379
- ### Basic Form
380
-
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
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
- ### 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
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
- # 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
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
- ### Resource Form
396
+ ### Overriding Configuration
420
397
 
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
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
- def edit
433
- @post = Post.find(params[:id])
434
- @form = Plutonium::UI::Form::Resource.new(
435
- @post,
436
- resource_definition: current_definition
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
- ### Custom Form Components
412
+ ### Automatic Rendering
446
413
 
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
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
- def color_text_attributes
460
- attributes.merge(
461
- name: "#{attributes[:name]}_text",
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
- # 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
424
+ ## Theming
473
425
 
474
- # Use in form
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
- ### Nested Resources
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 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
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
- ### Form Validation
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
- ```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
- )
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
- ### Dynamic Forms
457
+ ## JavaScript & Stimulus
586
458
 
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.
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
- ```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
- :::
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.