plutonium 0.33.1 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/# Plutonium: The pre-alpha demo.md +4 -2
- data/.claude/skills/assets/SKILL.md +416 -0
- data/.claude/skills/connect-resource/SKILL.md +112 -0
- data/.claude/skills/controller/SKILL.md +302 -0
- data/.claude/skills/create-resource/SKILL.md +240 -0
- data/.claude/skills/definition/SKILL.md +218 -0
- data/.claude/skills/definition-actions/SKILL.md +386 -0
- data/.claude/skills/definition-fields/SKILL.md +474 -0
- data/.claude/skills/definition-query/SKILL.md +334 -0
- data/.claude/skills/forms/SKILL.md +439 -0
- data/.claude/skills/installation/SKILL.md +300 -0
- data/.claude/skills/interaction/SKILL.md +382 -0
- data/.claude/skills/model/SKILL.md +267 -0
- data/.claude/skills/model-features/SKILL.md +286 -0
- data/.claude/skills/nested-resources/SKILL.md +274 -0
- data/.claude/skills/package/SKILL.md +191 -0
- data/.claude/skills/policy/SKILL.md +352 -0
- data/.claude/skills/portal/SKILL.md +400 -0
- data/.claude/skills/resource/SKILL.md +281 -0
- data/.claude/skills/rodauth/SKILL.md +452 -0
- data/.claude/skills/views/SKILL.md +563 -0
- data/Appraisals +46 -4
- data/CHANGELOG.md +32 -1
- data/app/assets/plutonium.css +2 -2
- data/config/brakeman.ignore +239 -0
- data/config/initializers/action_policy.rb +1 -1
- data/docs/.vitepress/config.ts +132 -47
- data/docs/concepts/architecture.md +226 -0
- data/docs/concepts/auto-detection.md +254 -0
- data/docs/concepts/index.md +61 -0
- data/docs/concepts/packages-portals.md +304 -0
- data/docs/concepts/resources.md +224 -0
- data/docs/cookbook/blog.md +412 -0
- data/docs/cookbook/index.md +289 -0
- data/docs/cookbook/saas.md +481 -0
- data/docs/getting-started/index.md +56 -0
- data/docs/getting-started/installation.md +146 -0
- data/docs/getting-started/tutorial/01-setup.md +118 -0
- data/docs/getting-started/tutorial/02-first-resource.md +180 -0
- data/docs/getting-started/tutorial/03-authentication.md +246 -0
- data/docs/getting-started/tutorial/04-authorization.md +170 -0
- data/docs/getting-started/tutorial/05-custom-actions.md +202 -0
- data/docs/getting-started/tutorial/06-nested-resources.md +147 -0
- data/docs/getting-started/tutorial/07-customizing-ui.md +254 -0
- data/docs/getting-started/tutorial/index.md +64 -0
- data/docs/guides/adding-resources.md +420 -0
- data/docs/guides/authentication.md +551 -0
- data/docs/guides/authorization.md +468 -0
- data/docs/guides/creating-packages.md +380 -0
- data/docs/guides/custom-actions.md +523 -0
- data/docs/guides/index.md +45 -0
- data/docs/guides/multi-tenancy.md +302 -0
- data/docs/guides/nested-resources.md +411 -0
- data/docs/guides/search-filtering.md +266 -0
- data/docs/guides/theming.md +321 -0
- data/docs/index.md +67 -26
- data/docs/public/CLAUDE.md +64 -21
- data/docs/reference/assets/index.md +496 -0
- data/docs/reference/controller/index.md +363 -0
- data/docs/reference/definition/actions.md +400 -0
- data/docs/reference/definition/fields.md +350 -0
- data/docs/reference/definition/index.md +252 -0
- data/docs/reference/definition/query.md +342 -0
- data/docs/reference/generators/index.md +469 -0
- data/docs/reference/index.md +49 -0
- data/docs/reference/interaction/index.md +445 -0
- data/docs/reference/model/features.md +248 -0
- data/docs/reference/model/index.md +219 -0
- data/docs/reference/policy/index.md +385 -0
- data/docs/reference/portal/index.md +382 -0
- data/docs/reference/views/forms.md +396 -0
- data/docs/reference/views/index.md +479 -0
- data/gemfiles/rails_7.gemfile +9 -2
- data/gemfiles/rails_7.gemfile.lock +146 -111
- data/gemfiles/rails_8.0.gemfile +20 -0
- data/gemfiles/rails_8.0.gemfile.lock +417 -0
- data/gemfiles/rails_8.1.gemfile +20 -0
- data/gemfiles/rails_8.1.gemfile.lock +419 -0
- data/lib/generators/pu/gem/dotenv/templates/.env +2 -0
- data/lib/generators/pu/gem/dotenv/templates/config/initializers/001_ensure_required_env.rb +3 -1
- data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +13 -16
- data/lib/generators/pu/pkg/portal/USAGE +65 -0
- data/lib/generators/pu/pkg/portal/portal_generator.rb +22 -9
- data/lib/generators/pu/res/conn/USAGE +71 -0
- data/lib/generators/pu/res/model/USAGE +106 -110
- data/lib/generators/pu/res/model/templates/model.rb.tt +6 -2
- data/lib/generators/pu/res/scaffold/USAGE +85 -0
- data/lib/generators/pu/rodauth/install_generator.rb +2 -6
- data/lib/generators/pu/rodauth/templates/config/initializers/url_options.rb +17 -0
- data/lib/generators/pu/skills/sync/USAGE +14 -0
- data/lib/generators/pu/skills/sync/sync_generator.rb +66 -0
- data/lib/plutonium/action_policy/sti_policy_lookup.rb +1 -1
- data/lib/plutonium/core/controller.rb +2 -2
- data/lib/plutonium/interaction/base.rb +1 -0
- data/lib/plutonium/package/engine.rb +2 -2
- data/lib/plutonium/query/adhoc_block.rb +6 -2
- data/lib/plutonium/query/model_scope.rb +1 -1
- data/lib/plutonium/railtie.rb +4 -0
- data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +1 -1
- data/lib/plutonium/resource/query_object.rb +38 -8
- data/lib/plutonium/ui/table/components/scopes_bar.rb +39 -34
- data/lib/plutonium/version.rb +1 -1
- data/lib/tasks/release.rake +19 -4
- data/package.json +1 -1
- metadata +76 -39
- data/brakeman.ignore +0 -28
- data/docs/api-examples.md +0 -49
- data/docs/guide/claude-code-guide.md +0 -74
- data/docs/guide/deep-dive/authorization.md +0 -189
- data/docs/guide/deep-dive/multitenancy.md +0 -256
- data/docs/guide/deep-dive/resources.md +0 -390
- data/docs/guide/getting-started/01-installation.md +0 -165
- data/docs/guide/index.md +0 -28
- data/docs/guide/introduction/01-what-is-plutonium.md +0 -211
- data/docs/guide/introduction/02-core-concepts.md +0 -440
- data/docs/guide/tutorial/01-project-setup.md +0 -75
- data/docs/guide/tutorial/02-creating-a-feature-package.md +0 -45
- data/docs/guide/tutorial/03-defining-resources.md +0 -90
- data/docs/guide/tutorial/04-creating-a-portal.md +0 -101
- data/docs/guide/tutorial/05-customizing-the-ui.md +0 -128
- data/docs/guide/tutorial/06-adding-custom-actions.md +0 -101
- data/docs/guide/tutorial/07-implementing-authorization.md +0 -90
- data/docs/markdown-examples.md +0 -85
- data/docs/modules/action.md +0 -244
- data/docs/modules/authentication.md +0 -236
- data/docs/modules/configuration.md +0 -599
- data/docs/modules/controller.md +0 -443
- data/docs/modules/core.md +0 -316
- data/docs/modules/definition.md +0 -1308
- data/docs/modules/display.md +0 -759
- data/docs/modules/form.md +0 -495
- data/docs/modules/generator.md +0 -400
- data/docs/modules/index.md +0 -167
- data/docs/modules/interaction.md +0 -642
- data/docs/modules/package.md +0 -151
- data/docs/modules/policy.md +0 -176
- data/docs/modules/portal.md +0 -710
- data/docs/modules/query.md +0 -297
- data/docs/modules/resource_record.md +0 -618
- data/docs/modules/routing.md +0 -690
- data/docs/modules/table.md +0 -301
- data/docs/modules/ui.md +0 -631
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: definition-fields
|
|
3
|
+
description: Configure how resource fields are rendered in forms, show pages, and tables
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Definition Fields
|
|
7
|
+
|
|
8
|
+
Configure how fields are rendered using `field`, `input`, `display`, and `column` declarations.
|
|
9
|
+
|
|
10
|
+
## Core Methods
|
|
11
|
+
|
|
12
|
+
| Method | Applies To | Use When |
|
|
13
|
+
|--------|-----------|----------|
|
|
14
|
+
| `field` | Forms + Show + Table | Universal type override |
|
|
15
|
+
| `input` | Forms only | Form-specific options |
|
|
16
|
+
| `display` | Show page only | Display-specific options |
|
|
17
|
+
| `column` | Table only | Table-specific options |
|
|
18
|
+
|
|
19
|
+
## Basic Usage
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
class PostDefinition < ResourceDefinition
|
|
23
|
+
# field - changes type everywhere
|
|
24
|
+
field :content, as: :rich_text
|
|
25
|
+
|
|
26
|
+
# input - form-specific
|
|
27
|
+
input :title,
|
|
28
|
+
label: "Article Title",
|
|
29
|
+
hint: "Enter a descriptive title",
|
|
30
|
+
placeholder: "e.g. Getting Started"
|
|
31
|
+
|
|
32
|
+
# display - show page specific
|
|
33
|
+
display :content,
|
|
34
|
+
as: :markdown,
|
|
35
|
+
description: "Published content",
|
|
36
|
+
wrapper: {class: "col-span-full"}
|
|
37
|
+
|
|
38
|
+
# column - table specific
|
|
39
|
+
column :title, label: "Article", align: :center
|
|
40
|
+
column :view_count, align: :end
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Available Field Types
|
|
45
|
+
|
|
46
|
+
### Input Types (Forms)
|
|
47
|
+
|
|
48
|
+
| Category | Types |
|
|
49
|
+
|----------|-------|
|
|
50
|
+
| **Text** | `:string`, `:text`, `:email`, `:url`, `:tel`, `:password` |
|
|
51
|
+
| **Rich Text** | `:rich_text`, `:markdown` |
|
|
52
|
+
| **Numeric** | `:number`, `:integer`, `:decimal`, `:range` |
|
|
53
|
+
| **Boolean** | `:boolean` |
|
|
54
|
+
| **Date/Time** | `:date`, `:time`, `:datetime` |
|
|
55
|
+
| **Selection** | `:select`, `:slim_select`, `:radio_buttons`, `:check_boxes` |
|
|
56
|
+
| **Files** | `:file`, `:uppy`, `:attachment` |
|
|
57
|
+
| **Associations** | `:association`, `:secure_association`, `:belongs_to`, `:has_many`, `:has_one` |
|
|
58
|
+
| **Special** | `:hidden`, `:color`, `:phone` |
|
|
59
|
+
|
|
60
|
+
### Display Types (Show/Index)
|
|
61
|
+
|
|
62
|
+
`:string`, `:text`, `:email`, `:url`, `:phone`, `:markdown`, `:number`, `:integer`, `:decimal`, `:boolean`, `:date`, `:time`, `:datetime`, `:association`, `:attachment`
|
|
63
|
+
|
|
64
|
+
## Field Options
|
|
65
|
+
|
|
66
|
+
### Field-Level Options (wrapper)
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
input :title,
|
|
70
|
+
label: "Custom Label", # Custom label text
|
|
71
|
+
hint: "Help text for forms", # Form help text
|
|
72
|
+
placeholder: "Enter value", # Input placeholder
|
|
73
|
+
description: "For displays" # Display description
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Tag-Level Options (HTML element)
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
input :title,
|
|
80
|
+
class: "custom-class", # CSS class
|
|
81
|
+
data: {controller: "custom"}, # Data attributes
|
|
82
|
+
required: true, # HTML required
|
|
83
|
+
readonly: true, # HTML readonly
|
|
84
|
+
disabled: true # HTML disabled
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Wrapper Options
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
display :content, wrapper: {class: "col-span-full"}
|
|
91
|
+
input :notes, wrapper: {class: "bg-gray-50"}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Select/Choices
|
|
95
|
+
|
|
96
|
+
### Static Choices
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
input :category, as: :select, choices: %w[Tech Business Lifestyle]
|
|
100
|
+
input :status, as: :select, choices: Post.statuses.keys
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Dynamic Choices (requires block)
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
# Basic dynamic
|
|
107
|
+
input :author do |f|
|
|
108
|
+
choices = User.active.pluck(:name, :id)
|
|
109
|
+
f.select_tag choices: choices
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# With context access
|
|
113
|
+
input :team_members do |f|
|
|
114
|
+
choices = current_user.organization.users.pluck(:name, :id)
|
|
115
|
+
f.select_tag choices: choices
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Based on object state
|
|
119
|
+
input :related_posts do |f|
|
|
120
|
+
choices = if object.persisted?
|
|
121
|
+
Post.where.not(id: object.id).published.pluck(:title, :id)
|
|
122
|
+
else
|
|
123
|
+
[]
|
|
124
|
+
end
|
|
125
|
+
f.select_tag choices: choices
|
|
126
|
+
end
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Conditional Rendering
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
class PostDefinition < ResourceDefinition
|
|
133
|
+
# Show based on object state
|
|
134
|
+
display :published_at, condition: -> { object.published? }
|
|
135
|
+
display :rejection_reason, condition: -> { object.rejected? }
|
|
136
|
+
|
|
137
|
+
# Show based on environment
|
|
138
|
+
field :debug_info, condition: -> { Rails.env.development? }
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Note:** Use `condition` for UI state logic. Use **policies** for authorization.
|
|
143
|
+
|
|
144
|
+
## Dynamic Forms (pre_submit)
|
|
145
|
+
|
|
146
|
+
Use `pre_submit: true` to create forms that dynamically show/hide fields based on other field values. When a `pre_submit` field changes, the form re-renders server-side and conditions are re-evaluated.
|
|
147
|
+
|
|
148
|
+
### Basic Pattern
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
class PostDefinition < ResourceDefinition
|
|
152
|
+
# Trigger field - causes form to re-render on change
|
|
153
|
+
input :send_notifications, pre_submit: true
|
|
154
|
+
|
|
155
|
+
# Dependent field - only shown when condition is true
|
|
156
|
+
input :notification_channel,
|
|
157
|
+
as: :select,
|
|
158
|
+
choices: %w[Email SMS],
|
|
159
|
+
condition: -> { object.send_notifications? }
|
|
160
|
+
end
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### How It Works
|
|
164
|
+
|
|
165
|
+
1. User changes a `pre_submit: true` field
|
|
166
|
+
2. Form submits via Turbo (no page reload)
|
|
167
|
+
3. Server re-renders the form with updated `object` state
|
|
168
|
+
4. Fields with `condition` procs are re-evaluated
|
|
169
|
+
5. Newly visible fields appear, hidden fields disappear
|
|
170
|
+
|
|
171
|
+
### Multiple Dependent Fields
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
class QuestionDefinition < ResourceDefinition
|
|
175
|
+
# Primary selector
|
|
176
|
+
input :question_type, as: :select,
|
|
177
|
+
choices: %w[text choice scale date boolean],
|
|
178
|
+
pre_submit: true
|
|
179
|
+
|
|
180
|
+
# Conditional fields based on question_type
|
|
181
|
+
input :max_length,
|
|
182
|
+
as: :integer,
|
|
183
|
+
condition: -> { object.question_type == "text" }
|
|
184
|
+
|
|
185
|
+
input :choices,
|
|
186
|
+
as: :text,
|
|
187
|
+
hint: "One choice per line",
|
|
188
|
+
condition: -> { object.question_type == "choice" }
|
|
189
|
+
|
|
190
|
+
input :min_value,
|
|
191
|
+
as: :integer,
|
|
192
|
+
condition: -> { object.question_type == "scale" }
|
|
193
|
+
|
|
194
|
+
input :max_value,
|
|
195
|
+
as: :integer,
|
|
196
|
+
condition: -> { object.question_type == "scale" }
|
|
197
|
+
end
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Cascading Dependencies
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
class PropertyDefinition < ResourceDefinition
|
|
204
|
+
# First level
|
|
205
|
+
input :property_type, as: :select,
|
|
206
|
+
choices: %w[residential commercial],
|
|
207
|
+
pre_submit: true
|
|
208
|
+
|
|
209
|
+
# Second level - depends on property_type
|
|
210
|
+
input :residential_type, as: :select,
|
|
211
|
+
choices: %w[apartment house condo],
|
|
212
|
+
condition: -> { object.property_type == "residential" },
|
|
213
|
+
pre_submit: true
|
|
214
|
+
|
|
215
|
+
input :commercial_type, as: :select,
|
|
216
|
+
choices: %w[office retail warehouse],
|
|
217
|
+
condition: -> { object.property_type == "commercial" },
|
|
218
|
+
pre_submit: true
|
|
219
|
+
|
|
220
|
+
# Third level - depends on residential_type
|
|
221
|
+
input :apartment_floor,
|
|
222
|
+
as: :integer,
|
|
223
|
+
condition: -> { object.residential_type == "apartment" }
|
|
224
|
+
end
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Dynamic Choices with pre_submit
|
|
228
|
+
|
|
229
|
+
Combine `pre_submit` with block-based dynamic choices:
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
class SurveyResponseDefinition < ResourceDefinition
|
|
233
|
+
input :category, as: :select,
|
|
234
|
+
choices: Category.pluck(:name, :id),
|
|
235
|
+
pre_submit: true
|
|
236
|
+
|
|
237
|
+
# Choices change based on selected category
|
|
238
|
+
input :subcategory do |f|
|
|
239
|
+
choices = if object.category.present?
|
|
240
|
+
Category.find(object.category).subcategories.pluck(:name, :id)
|
|
241
|
+
else
|
|
242
|
+
[]
|
|
243
|
+
end
|
|
244
|
+
f.select_tag choices: choices
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Tips
|
|
250
|
+
|
|
251
|
+
- Only add `pre_submit: true` to fields that control visibility of other fields
|
|
252
|
+
- Keep dependencies simple - deeply nested conditions are hard to debug
|
|
253
|
+
- The form submits on change, so avoid `pre_submit` on frequently-changed fields
|
|
254
|
+
|
|
255
|
+
## Custom Rendering
|
|
256
|
+
|
|
257
|
+
### Block Syntax
|
|
258
|
+
|
|
259
|
+
**For Display (can return any component):**
|
|
260
|
+
```ruby
|
|
261
|
+
display :status do |field|
|
|
262
|
+
StatusBadgeComponent.new(value: field.value, class: field.dom.css_class)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
display :metrics do |field|
|
|
266
|
+
if field.value.present?
|
|
267
|
+
MetricsChartComponent.new(data: field.value)
|
|
268
|
+
else
|
|
269
|
+
EmptyStateComponent.new(message: "No metrics")
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**For Input (must use form builder methods):**
|
|
275
|
+
```ruby
|
|
276
|
+
input :birth_date do |f|
|
|
277
|
+
case object.age_category
|
|
278
|
+
when 'adult'
|
|
279
|
+
f.date_tag(min: 18.years.ago.to_date)
|
|
280
|
+
when 'minor'
|
|
281
|
+
f.date_tag(max: 18.years.ago.to_date)
|
|
282
|
+
else
|
|
283
|
+
f.date_tag
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### phlexi_tag (Advanced Display)
|
|
289
|
+
|
|
290
|
+
```ruby
|
|
291
|
+
# With component class
|
|
292
|
+
display :status, as: :phlexi_tag, with: StatusBadgeComponent
|
|
293
|
+
|
|
294
|
+
# With inline proc
|
|
295
|
+
display :priority, as: :phlexi_tag, with: ->(value, attrs) {
|
|
296
|
+
case value
|
|
297
|
+
when 'high'
|
|
298
|
+
span(class: "badge badge-danger") { "High" }
|
|
299
|
+
when 'medium'
|
|
300
|
+
span(class: "badge badge-warning") { "Medium" }
|
|
301
|
+
else
|
|
302
|
+
span(class: "badge badge-info") { "Low" }
|
|
303
|
+
end
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Custom Component Class
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
input :color_picker, as: ColorPickerComponent
|
|
311
|
+
display :chart, as: ChartComponent
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Column Alignment
|
|
315
|
+
|
|
316
|
+
```ruby
|
|
317
|
+
column :title, align: :start # Left (default)
|
|
318
|
+
column :status, align: :center # Center
|
|
319
|
+
column :amount, align: :end # Right
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Nested Inputs
|
|
323
|
+
|
|
324
|
+
Render inline forms for associated records. Requires `accepts_nested_attributes_for` on the model.
|
|
325
|
+
|
|
326
|
+
### Model Setup
|
|
327
|
+
|
|
328
|
+
```ruby
|
|
329
|
+
class Post < ResourceRecord
|
|
330
|
+
has_many :comments
|
|
331
|
+
has_one :metadata
|
|
332
|
+
|
|
333
|
+
accepts_nested_attributes_for :comments, allow_destroy: true, limit: 10
|
|
334
|
+
accepts_nested_attributes_for :metadata, update_only: true
|
|
335
|
+
end
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Basic Declaration
|
|
339
|
+
|
|
340
|
+
```ruby
|
|
341
|
+
class PostDefinition < ResourceDefinition
|
|
342
|
+
# Block syntax
|
|
343
|
+
nested_input :comments do |n|
|
|
344
|
+
n.input :body, as: :text
|
|
345
|
+
n.input :author_name
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Using another definition
|
|
349
|
+
nested_input :metadata, using: PostMetadataDefinition, fields: %i[seo_title seo_description]
|
|
350
|
+
end
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Options
|
|
354
|
+
|
|
355
|
+
| Option | Description |
|
|
356
|
+
|--------|-------------|
|
|
357
|
+
| `limit` | Max records (auto-detected from model, default: 10) |
|
|
358
|
+
| `allow_destroy` | Show delete checkbox (auto-detected from model) |
|
|
359
|
+
| `update_only` | Hide "Add" button, only edit existing |
|
|
360
|
+
| `description` | Help text above the section |
|
|
361
|
+
| `condition` | Proc to show/hide section |
|
|
362
|
+
| `using` | Reference another Definition class |
|
|
363
|
+
| `fields` | Which fields to render from the definition |
|
|
364
|
+
|
|
365
|
+
```ruby
|
|
366
|
+
nested_input :amenities,
|
|
367
|
+
allow_destroy: true,
|
|
368
|
+
limit: 20,
|
|
369
|
+
description: "Add property amenities" do |n|
|
|
370
|
+
n.input :name
|
|
371
|
+
n.input :icon, as: :select, choices: ICONS
|
|
372
|
+
end
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Singular Associations
|
|
376
|
+
|
|
377
|
+
For `has_one` and `belongs_to`, limit is automatically 1:
|
|
378
|
+
|
|
379
|
+
```ruby
|
|
380
|
+
nested_input :profile do |n| # has_one
|
|
381
|
+
n.input :bio
|
|
382
|
+
n.input :website
|
|
383
|
+
end
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Conditional Nested Inputs
|
|
387
|
+
|
|
388
|
+
```ruby
|
|
389
|
+
nested_input :shipping_address,
|
|
390
|
+
condition: -> { object.requires_shipping? } do |n|
|
|
391
|
+
n.input :street
|
|
392
|
+
n.input :city
|
|
393
|
+
end
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### How It Works
|
|
397
|
+
|
|
398
|
+
1. Renders a template (hidden) for new records
|
|
399
|
+
2. Renders fieldsets for existing records
|
|
400
|
+
3. Stimulus controller handles Add/Remove
|
|
401
|
+
4. `_destroy` checkbox marks records for deletion
|
|
402
|
+
5. Parameters submitted as `model[association_attributes][id][field]`
|
|
403
|
+
|
|
404
|
+
### Gotchas
|
|
405
|
+
|
|
406
|
+
- Model must have `accepts_nested_attributes_for`
|
|
407
|
+
- For custom class names, use `class_name:` in both model and `using:` in definition
|
|
408
|
+
- `update_only: true` hides the Add button
|
|
409
|
+
- Limit is enforced in UI (Add button hidden when reached)
|
|
410
|
+
|
|
411
|
+
## File Uploads
|
|
412
|
+
|
|
413
|
+
```ruby
|
|
414
|
+
input :avatar, as: :file
|
|
415
|
+
input :avatar, as: :uppy
|
|
416
|
+
|
|
417
|
+
input :documents, as: :file, multiple: true
|
|
418
|
+
input :documents, as: :uppy,
|
|
419
|
+
allowed_file_types: ['.pdf', '.doc'],
|
|
420
|
+
max_file_size: 5.megabytes
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
## Common Patterns
|
|
424
|
+
|
|
425
|
+
### Rich Text Content
|
|
426
|
+
|
|
427
|
+
```ruby
|
|
428
|
+
field :content, as: :rich_text # Form: rich editor
|
|
429
|
+
display :content, as: :markdown # Show: rendered markdown
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Money Fields
|
|
433
|
+
|
|
434
|
+
```ruby
|
|
435
|
+
input :price, as: :decimal, class: "font-mono"
|
|
436
|
+
display :price, class: "font-bold text-green-600"
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Status Badges
|
|
440
|
+
|
|
441
|
+
```ruby
|
|
442
|
+
display :status do |field|
|
|
443
|
+
color = case field.value
|
|
444
|
+
when 'active' then 'green'
|
|
445
|
+
when 'pending' then 'yellow'
|
|
446
|
+
else 'gray'
|
|
447
|
+
end
|
|
448
|
+
span(class: "badge badge-#{color}") { field.value.humanize }
|
|
449
|
+
end
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Hidden Fields
|
|
453
|
+
|
|
454
|
+
```ruby
|
|
455
|
+
field :author_id, as: :hidden
|
|
456
|
+
input :tenant_id, as: :hidden
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
## Context in Blocks
|
|
460
|
+
|
|
461
|
+
Inside `condition` procs and `input` blocks:
|
|
462
|
+
- `object` - The record being edited/displayed
|
|
463
|
+
- `current_user` - The authenticated user
|
|
464
|
+
- `current_parent` - Parent record for nested resources
|
|
465
|
+
- `request`, `params` - Request information
|
|
466
|
+
- All helper methods
|
|
467
|
+
|
|
468
|
+
## Related Skills
|
|
469
|
+
|
|
470
|
+
- `definition` - Overview and structure
|
|
471
|
+
- `definition-actions` - Actions and interactions
|
|
472
|
+
- `definition-query` - Search, filters, scopes
|
|
473
|
+
- `forms` - Custom form templates and field builders
|
|
474
|
+
- `views` - Custom page and display templates
|