nitro_kit 2.0.0.alpha.2 → 2.0.0.alpha.4
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/CHANGELOG.md +229 -1
- data/README.md +25 -10
- data/STYLE_GUIDE.md +47 -5
- data/app/assets/stylesheets/nitro_kit-tailwind-v4.css +3 -3
- data/app/assets/stylesheets/nitro_kit.css +2820 -1816
- data/app/components/nitro_kit/accordion.rb +2 -26
- data/app/components/nitro_kit/alert.rb +1 -11
- data/app/components/nitro_kit/app_navigation.rb +45 -11
- data/app/components/nitro_kit/app_shell.rb +1 -7
- data/app/components/nitro_kit/appearance_picker.rb +11 -12
- data/app/components/nitro_kit/avatar.rb +6 -1
- data/app/components/nitro_kit/avatar_stack.rb +3 -0
- data/app/components/nitro_kit/badge.rb +13 -4
- data/app/components/nitro_kit/button_group.rb +2 -2
- data/app/components/nitro_kit/checkbox.rb +7 -4
- data/app/components/nitro_kit/checkbox_group.rb +0 -25
- data/app/components/nitro_kit/combobox.rb +4 -33
- data/app/components/nitro_kit/command_palette.rb +42 -59
- data/app/components/nitro_kit/component.rb +57 -5
- data/app/components/nitro_kit/danger_zone.rb +5 -1
- data/app/components/nitro_kit/data_section.rb +28 -4
- data/app/components/nitro_kit/details_table.rb +19 -8
- data/app/components/nitro_kit/dialog.rb +23 -19
- data/app/components/nitro_kit/dropdown.rb +1 -6
- data/app/components/nitro_kit/dropzone.rb +4 -15
- data/app/components/nitro_kit/empty_state.rb +21 -2
- data/app/components/nitro_kit/field.rb +35 -11
- data/app/components/nitro_kit/form_builder.rb +1 -2
- data/app/components/nitro_kit/progressive_image.rb +2 -2
- data/app/components/nitro_kit/radio_button.rb +7 -4
- data/app/components/nitro_kit/radio_button_group.rb +0 -25
- data/app/components/nitro_kit/select.rb +4 -17
- data/app/components/nitro_kit/settings_layout.rb +17 -4
- data/app/components/nitro_kit/{form_section.rb → settings_section.rb} +18 -11
- data/app/components/nitro_kit/sheet.rb +29 -20
- data/app/components/nitro_kit/stat_grid.rb +18 -2
- data/app/components/nitro_kit/table.rb +22 -3
- data/app/components/nitro_kit/tabs.rb +2 -13
- data/app/components/nitro_kit/toast.rb +4 -2
- data/app/components/nitro_kit/toolbar.rb +2 -0
- data/app/components/nitro_kit/tooltip.rb +7 -10
- data/app/javascript/controllers/nk/app_shell_controller.js +3 -3
- data/app/javascript/controllers/nk/appearance_controller.js +6 -0
- data/app/javascript/controllers/nk/combobox_controller.js +51 -1
- data/app/javascript/controllers/nk/command_palette_controller.js +25 -11
- data/app/javascript/controllers/nk/dialog_controller.js +54 -0
- data/app/javascript/controllers/nk/dropdown_controller.js +45 -0
- data/app/javascript/controllers/nk/dropzone_controller.js +2 -2
- data/app/javascript/controllers/nk/progressive_image_controller.js +11 -7
- data/app/javascript/controllers/nk/tabs_controller.js +21 -2
- data/app/javascript/controllers/nk/tooltip_controller.js +9 -0
- data/config/locales/en.yml +4 -1
- data/docs/agent_guide.md +63 -123
- data/docs/agent_native_spec.md +72 -383
- data/docs/browser_support.md +82 -0
- data/docs/component_contracts.md +108 -82
- data/docs/customization.md +689 -91
- data/docs/hotwire.md +50 -76
- data/docs/initialization_prompt.md +26 -31
- data/docs/migration_1_to_2.md +88 -30
- data/docs/patterns/application_foundation.md +45 -119
- data/docs/patterns/crud_resource.md +44 -126
- data/docs/patterns/destructive_action.md +45 -77
- data/docs/patterns/flash_and_toast.md +18 -36
- data/docs/patterns/inline_edit.md +21 -44
- data/docs/patterns/queryable_collection.md +65 -161
- data/docs/patterns/resource_form.md +33 -81
- data/docs/rails_conventions.md +38 -75
- data/docs/rails_integration.md +96 -446
- data/lib/generators/nitro_kit/upgrade_smoke_tests_generator.rb +12 -0
- data/lib/nitro_kit/installation.rb +20 -7
- data/lib/nitro_kit/migration_inventory.rb +275 -8
- data/lib/nitro_kit/upgrade_smoke_test.rb +10 -12
- data/lib/nitro_kit/version.rb +1 -1
- data/lib/rails/commands/nitro_kit/nitro_kit_command.rb +10 -3
- data/plugins/nitro-kit/skills/nitro-kit-hotwire/SKILL.md +4 -0
- data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +13 -1
- data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +32 -15
- data/src/stylesheets/nitro_kit/components/accordion.css +44 -44
- data/src/stylesheets/nitro_kit/components/alert.css +27 -36
- data/src/stylesheets/nitro_kit/components/app_navigation.css +92 -20
- data/src/stylesheets/nitro_kit/components/app_shell.css +199 -196
- data/src/stylesheets/nitro_kit/components/appearance_picker.css +61 -39
- data/src/stylesheets/nitro_kit/components/auth_shell.css +2 -2
- data/src/stylesheets/nitro_kit/components/avatar.css +15 -11
- data/src/stylesheets/nitro_kit/components/avatar_stack.css +48 -29
- data/src/stylesheets/nitro_kit/components/badge.css +13 -9
- data/src/stylesheets/nitro_kit/components/button.css +121 -79
- data/src/stylesheets/nitro_kit/components/button_group.css +19 -14
- data/src/stylesheets/nitro_kit/components/card.css +12 -6
- data/src/stylesheets/nitro_kit/components/checkbox.css +54 -67
- data/src/stylesheets/nitro_kit/components/checkbox_group.css +16 -16
- data/src/stylesheets/nitro_kit/components/combobox.css +39 -29
- data/src/stylesheets/nitro_kit/components/command_palette.css +110 -51
- data/src/stylesheets/nitro_kit/components/container.css +6 -6
- data/src/stylesheets/nitro_kit/components/control_group.css +61 -67
- data/src/stylesheets/nitro_kit/components/danger_zone.css +8 -8
- data/src/stylesheets/nitro_kit/components/data_section.css +4 -4
- data/src/stylesheets/nitro_kit/components/details_table.css +17 -15
- data/src/stylesheets/nitro_kit/components/dialog.css +8 -5
- data/src/stylesheets/nitro_kit/components/dropdown.css +36 -30
- data/src/stylesheets/nitro_kit/components/dropzone.css +78 -35
- data/src/stylesheets/nitro_kit/components/empty_state.css +7 -7
- data/src/stylesheets/nitro_kit/components/field.css +32 -28
- data/src/stylesheets/nitro_kit/components/field_group.css +13 -0
- data/src/stylesheets/nitro_kit/components/fieldset.css +8 -2
- data/src/stylesheets/nitro_kit/components/flex.css +1 -1
- data/src/stylesheets/nitro_kit/components/grid.css +1 -1
- data/src/stylesheets/nitro_kit/components/icon.css +5 -5
- data/src/stylesheets/nitro_kit/components/input.css +42 -8
- data/src/stylesheets/nitro_kit/components/layout.css +165 -165
- data/src/stylesheets/nitro_kit/components/page_header.css +7 -7
- data/src/stylesheets/nitro_kit/components/pagination.css +58 -49
- data/src/stylesheets/nitro_kit/components/pagination_bar.css +9 -9
- data/src/stylesheets/nitro_kit/components/palette.css +145 -77
- data/src/stylesheets/nitro_kit/components/progressive_image.css +45 -55
- data/src/stylesheets/nitro_kit/components/radio_button.css +48 -46
- data/src/stylesheets/nitro_kit/components/radio_button_group.css +49 -49
- data/src/stylesheets/nitro_kit/components/rich_text_area.css +16 -1
- data/src/stylesheets/nitro_kit/components/select.css +24 -15
- data/src/stylesheets/nitro_kit/components/settings_layout.css +26 -23
- data/src/stylesheets/nitro_kit/components/settings_section.css +84 -0
- data/src/stylesheets/nitro_kit/components/sheet.css +30 -10
- data/src/stylesheets/nitro_kit/components/stat_grid.css +11 -10
- data/src/stylesheets/nitro_kit/components/switch.css +60 -62
- data/src/stylesheets/nitro_kit/components/table.css +43 -35
- data/src/stylesheets/nitro_kit/components/tabs.css +48 -40
- data/src/stylesheets/nitro_kit/components/textarea.css +20 -5
- data/src/stylesheets/nitro_kit/components/toast.css +11 -47
- data/src/stylesheets/nitro_kit/components/toolbar.css +16 -41
- data/src/stylesheets/nitro_kit/components/tooltip.css +38 -29
- data/src/stylesheets/nitro_kit/components/typeset.css +150 -4
- data/src/stylesheets/nitro_kit/reset.css +15 -15
- data/src/stylesheets/nitro_kit/tokens.css +501 -79
- metadata +4 -4
- data/docs/new_app_strategy.md +0 -22
- data/src/stylesheets/nitro_kit/components/form_section.css +0 -71
|
@@ -1,187 +1,91 @@
|
|
|
1
1
|
# Queryable collection
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Audience:** Coding agents and developers implementing filters, sorting, and
|
|
4
|
+
pagination with Turbo Frames.
|
|
4
5
|
|
|
5
6
|
## Summary
|
|
6
7
|
|
|
7
|
-
- One GET-driven Turbo Frame
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
## Contract
|
|
20
|
-
|
|
21
|
-
- The frame has one stable ID.
|
|
22
|
-
- The frame promotes pagination with `data-turbo-action="advance"` so each page
|
|
23
|
-
remains a meaningful Back and Forward step.
|
|
24
|
-
- Filter forms use GET, target that frame, and replace the current history entry.
|
|
25
|
-
- A small filter set stays in one responsive row at wide widths and stacks on
|
|
26
|
-
narrow screens. Do not turn two fields and two actions into a full-page form.
|
|
27
|
-
- Sort and pagination links preserve the active query parameters. Sort links
|
|
28
|
-
replace the current history entry; pagination inherits the frame's `advance`.
|
|
29
|
-
- `NitroKit::Table` renders the sortable table contract but does not own query policy.
|
|
30
|
-
- The HTML response always contains the same frame, including empty results.
|
|
31
|
-
- No Stimulus controller is required for submit-based filters. An optional autosubmit controller may submit the same GET form without becoming the source of truth.
|
|
32
|
-
|
|
33
|
-
## Controller
|
|
34
|
-
|
|
35
|
-
Keep parameter normalization and allowlists in an application query object. Ransack is one possible implementation, not a Nitro dependency.
|
|
8
|
+
- One GET-driven Turbo Frame owns filters, sorting, results, and pagination;
|
|
9
|
+
URL parameters are the state.
|
|
10
|
+
- An application query object owns allowlists, defaults, tenant scope, and page
|
|
11
|
+
bounds; `NitroKit::Table` owns no query policy.
|
|
12
|
+
- Pagination advances browser history; filters, reset, and sorting replace the
|
|
13
|
+
current history entry.
|
|
14
|
+
- Every response contains the same frame, including empty results; links that
|
|
15
|
+
leave the collection target `_top`.
|
|
16
|
+
|
|
17
|
+
## Query contract
|
|
18
|
+
|
|
19
|
+
Build the query from a tenant-scoped relation:
|
|
36
20
|
|
|
37
21
|
```ruby
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
22
|
+
def index
|
|
23
|
+
@query = ProjectsQuery.new(
|
|
24
|
+
Current.account.projects,
|
|
25
|
+
params: params.fetch(:q, {}),
|
|
26
|
+
page: params[:page]
|
|
27
|
+
)
|
|
42
28
|
end
|
|
43
29
|
```
|
|
44
30
|
|
|
45
|
-
The query object
|
|
31
|
+
The query object owns parameter allowlists, default ordering, page bounds, and
|
|
32
|
+
query URL generation. It may expose `records`, `filters`, `current_sort`,
|
|
33
|
+
`direction`, `sort_url(key)`, `pagination`, and `summary`. Ransack is one
|
|
34
|
+
possible implementation, not a Nitro dependency.
|
|
46
35
|
|
|
47
|
-
##
|
|
36
|
+
## Frame composition
|
|
48
37
|
|
|
49
38
|
```ruby
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
39
|
+
FRAME_ID = "projects-results"
|
|
40
|
+
|
|
41
|
+
turbo_frame_tag(FRAME_ID, data: { turbo_action: "advance" }) do
|
|
42
|
+
form_with(
|
|
43
|
+
scope: :q,
|
|
44
|
+
url: projects_path,
|
|
45
|
+
method: :get,
|
|
46
|
+
builder: NitroKit::FormBuilder,
|
|
47
|
+
data: { turbo_frame: FRAME_ID, turbo_action: "replace" }
|
|
48
|
+
) do |form|
|
|
49
|
+
form.group do
|
|
50
|
+
form.field(:name_cont, as: :search, label: "Search")
|
|
51
|
+
form.submit("Apply filters")
|
|
59
52
|
end
|
|
53
|
+
end
|
|
60
54
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
55
|
+
render NitroKit::Table.new(
|
|
56
|
+
sort: query.current_sort,
|
|
57
|
+
direction: query.direction
|
|
58
|
+
) do |table|
|
|
59
|
+
table.caption("Projects")
|
|
60
|
+
table.thead do
|
|
61
|
+
table.tr do
|
|
62
|
+
table.th(:name, sort: :name, href: query.sort_url(:name),
|
|
63
|
+
sort_data: { turbo_action: "replace" })
|
|
68
64
|
end
|
|
69
65
|
end
|
|
66
|
+
table.tbody do
|
|
67
|
+
query.records.each { |project| render_project_row(table, project) }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def render_filters
|
|
75
|
-
form_with(
|
|
76
|
-
scope: :q,
|
|
77
|
-
url: "/projects",
|
|
78
|
-
method: :get,
|
|
79
|
-
builder: NitroKit::FormBuilder,
|
|
80
|
-
data: {
|
|
81
|
-
turbo_frame: FRAME_ID,
|
|
82
|
-
turbo_action: "replace"
|
|
83
|
-
}
|
|
84
|
-
) do |form|
|
|
85
|
-
form.hidden_field(:s, value: query.filters["s"])
|
|
86
|
-
render NitroKit::Grid.new(cols: "1 md:3", gap: 3) do
|
|
87
|
-
form.field(:name_cont, as: :search, label: "Search", value: query.filters["name_cont"])
|
|
88
|
-
form.field(:status_eq, as: :select, label: "Status", options: Project.statuses.keys, include_blank: "All statuses", value: query.filters["status_eq"])
|
|
89
|
-
render NitroKit::Flex.new(dir: :row, gap: 2, align: :end, justify: :end) do
|
|
90
|
-
render NitroKit::Button.new(
|
|
91
|
-
"Reset",
|
|
92
|
-
href: "/projects",
|
|
93
|
-
data: {
|
|
94
|
-
turbo_frame: FRAME_ID,
|
|
95
|
-
turbo_action: "replace"
|
|
96
|
-
}
|
|
97
|
-
)
|
|
98
|
-
form.submit("Apply filters", data: { turbo_submits_with: "Filtering…" })
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def render_results
|
|
105
|
-
render NitroKit::Table.new(
|
|
106
|
-
sort: query.current_sort,
|
|
107
|
-
direction: query.direction
|
|
108
|
-
) do |table|
|
|
109
|
-
table.caption("Projects")
|
|
110
|
-
table.thead do
|
|
111
|
-
table.tr do
|
|
112
|
-
table.th(
|
|
113
|
-
sort: :name,
|
|
114
|
-
href: query.sort_url(:name),
|
|
115
|
-
sort_data: { turbo_action: "replace" }
|
|
116
|
-
)
|
|
117
|
-
table.th(
|
|
118
|
-
sort: :status,
|
|
119
|
-
href: query.sort_url(:status),
|
|
120
|
-
sort_data: { turbo_action: "replace" }
|
|
121
|
-
)
|
|
122
|
-
table.th(
|
|
123
|
-
"Updated",
|
|
124
|
-
sort: :updated_at,
|
|
125
|
-
href: query.sort_url(:updated_at),
|
|
126
|
-
align: :right,
|
|
127
|
-
sort_data: { turbo_action: "replace" }
|
|
128
|
-
)
|
|
129
|
-
table.th("Actions", align: :right)
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
table.tbody do
|
|
133
|
-
if query.records.any?
|
|
134
|
-
query.records.each { |project| render_row(table, project) }
|
|
135
|
-
else
|
|
136
|
-
table.tr { table.td("No projects match these filters.", html: { colspan: 4 }) }
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def render_row(table, project)
|
|
143
|
-
table.tr do
|
|
144
|
-
table.th(project.name, scope: :row)
|
|
145
|
-
table.td { render NitroKit::Badge.new(project.status.humanize) }
|
|
146
|
-
table.td(project.updated_at.to_date.to_fs(:long), align: :right)
|
|
147
|
-
table.td(align: :right) do
|
|
148
|
-
render NitroKit::Button.new(
|
|
149
|
-
"View",
|
|
150
|
-
href: project_path(project),
|
|
151
|
-
data: { turbo_frame: "_top" }
|
|
152
|
-
)
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def render_pagination
|
|
158
|
-
render NitroKit::PaginationBar.new do |bar|
|
|
159
|
-
bar.summary(query.summary)
|
|
160
|
-
bar.pagination(query.pagination)
|
|
161
|
-
end
|
|
162
|
-
end
|
|
71
|
+
render NitroKit::PaginationBar.new do |bar|
|
|
72
|
+
bar.summary(query.summary)
|
|
73
|
+
bar.pagination(query.pagination)
|
|
163
74
|
end
|
|
164
75
|
end
|
|
165
76
|
```
|
|
166
77
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
`data: { turbo_frame: FRAME_ID }` when a query control sits outside the frame.
|
|
172
|
-
Reset with a plain collection URL so stale query parameters disappear.
|
|
78
|
+
Reset with the plain collection URL so stale parameters disappear. Sort and
|
|
79
|
+
filter controls use `turbo_action: "replace"`; pagination inherits the frame's
|
|
80
|
+
`advance`. Give View, Edit, and New links
|
|
81
|
+
`data: { turbo_frame: "_top" }` or place them outside the frame.
|
|
173
82
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
the results frame. Otherwise Turbo will look for the collection frame in the
|
|
177
|
-
destination response and replace the region with “Content missing.”
|
|
83
|
+
No Stimulus controller is required. Optional autosubmit may call the same GET
|
|
84
|
+
form's `requestSubmit`; the URL remains the source of truth.
|
|
178
85
|
|
|
179
86
|
## Tests
|
|
180
87
|
|
|
181
|
-
Request
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
Forward, assert the address bar after each query transition, and open a row
|
|
186
|
-
action without a frame-missing error. Use Capybara's retrying assertions rather
|
|
187
|
-
than sleeps or arbitrary waits.
|
|
88
|
+
Request-test parameter preservation, safe fallback for invalid sort keys, and
|
|
89
|
+
the stable frame in populated and empty responses. System-test filter → sort →
|
|
90
|
+
paginate → Back/Forward, address-bar changes, and a row link leaving the frame.
|
|
91
|
+
Use Capybara waiting assertions, not sleeps.
|
|
@@ -1,126 +1,78 @@
|
|
|
1
1
|
# Resource form
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Audience:** Coding agents and developers implementing model-backed create
|
|
4
|
+
and update forms.
|
|
4
5
|
|
|
5
6
|
## Summary
|
|
6
7
|
|
|
7
|
-
- One model-backed Phlex component renders
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
call `form.submit` in the body.
|
|
15
|
-
- A genuinely standalone form keeps its submit inside the same `form.group` as
|
|
16
|
-
its visible fields.
|
|
17
|
-
- Parents own rhythm: stacked fields always sit inside `FieldGroup`
|
|
18
|
-
(`form.group` on the builder, or `render NitroKit::FieldGroup.new`). Fields
|
|
19
|
-
rendered as bare siblings of the `form` element stack with no gap — that is
|
|
20
|
-
an authoring mistake, not a layout the field can correct itself.
|
|
21
|
-
- Wrap the form in a Turbo Frame only when it is embedded in a larger screen
|
|
22
|
-
and needs an independent lifecycle.
|
|
8
|
+
- One model-backed Phlex component renders initial and invalid states.
|
|
9
|
+
- Rails owns names, values, and errors; Nitro owns presentation.
|
|
10
|
+
- Invalid mutations render the same model with `422`; success redirects with
|
|
11
|
+
`303`.
|
|
12
|
+
- Use one primary submit: the shell toolbar owns it, or a standalone form keeps
|
|
13
|
+
it inside `form.group`.
|
|
14
|
+
- Wrap the form in a Turbo Frame only when it needs an independent lifecycle.
|
|
23
15
|
|
|
24
|
-
##
|
|
16
|
+
## Form
|
|
25
17
|
|
|
26
18
|
```ruby
|
|
27
19
|
module UI
|
|
28
20
|
class ProjectForm < Phlex::HTML
|
|
29
21
|
include Phlex::Rails::Helpers::FormWith
|
|
30
22
|
|
|
31
|
-
def initialize(
|
|
32
|
-
project,
|
|
33
|
-
form_id: ActionView::RecordIdentifier.dom_id(project, :form)
|
|
34
|
-
)
|
|
23
|
+
def initialize(project, form_id:)
|
|
35
24
|
@project = project
|
|
36
25
|
@form_id = form_id
|
|
37
26
|
end
|
|
38
27
|
|
|
39
28
|
def view_template
|
|
40
|
-
render NitroKit::
|
|
41
|
-
title: project.persisted? ? "Edit project" : "New project",
|
|
42
|
-
description: "Project details are visible to every workspace member."
|
|
43
|
-
) do |section|
|
|
29
|
+
render NitroKit::SettingsSection.new(title: "Project details") do |section|
|
|
44
30
|
section.form do
|
|
45
31
|
form_with(
|
|
46
|
-
model: project,
|
|
32
|
+
model: @project,
|
|
47
33
|
builder: NitroKit::FormBuilder,
|
|
48
|
-
id: form_id
|
|
34
|
+
id: @form_id
|
|
49
35
|
) do |form|
|
|
50
36
|
form.group do
|
|
51
|
-
form.field(:name, required: true
|
|
52
|
-
form.field(
|
|
53
|
-
:status,
|
|
54
|
-
as: :select,
|
|
55
|
-
options: Project.statuses.keys.map do |value|
|
|
56
|
-
[ value.humanize, value ]
|
|
57
|
-
end
|
|
58
|
-
)
|
|
37
|
+
form.field(:name, required: true)
|
|
38
|
+
form.field(:status, as: :select, options: Project.statuses.keys)
|
|
59
39
|
form.field(:description, as: :textarea)
|
|
60
40
|
end
|
|
61
41
|
end
|
|
62
42
|
end
|
|
63
43
|
end
|
|
64
44
|
end
|
|
65
|
-
|
|
66
|
-
private
|
|
67
|
-
attr_reader :project, :form_id
|
|
68
45
|
end
|
|
69
46
|
end
|
|
70
47
|
```
|
|
71
48
|
|
|
72
|
-
In an application shell, render
|
|
73
|
-
|
|
49
|
+
In an application shell, render one toolbar submit associated through the
|
|
50
|
+
native form ID:
|
|
74
51
|
|
|
75
52
|
```ruby
|
|
76
|
-
Button(
|
|
77
|
-
project.persisted? ? "Save project" : "Create project",
|
|
78
|
-
type: :submit,
|
|
79
|
-
form: form_id,
|
|
80
|
-
variant: :primary,
|
|
81
|
-
data: { turbo_submits_with: "Saving…" }
|
|
82
|
-
)
|
|
53
|
+
Button("Save project", type: :submit, form: form_id, variant: :primary)
|
|
83
54
|
```
|
|
84
55
|
|
|
85
|
-
Do not also call `form.submit` in the
|
|
86
|
-
|
|
87
|
-
submit inside the same `form.group` as its visible fields.
|
|
88
|
-
|
|
89
|
-
## Controller
|
|
56
|
+
Do not also call `form.submit` in the body. A standalone form without a toolbar
|
|
57
|
+
keeps its submit in the same `form.group` as its visible fields.
|
|
90
58
|
|
|
91
|
-
|
|
59
|
+
## Responses
|
|
92
60
|
|
|
93
61
|
```ruby
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
redirect_to @project, status: :see_other, notice: "Project created"
|
|
100
|
-
else
|
|
101
|
-
render UI::ProjectForm.new(@project), status: :unprocessable_entity
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def update
|
|
106
|
-
@project = Current.account.projects.find(params[:id])
|
|
107
|
-
|
|
108
|
-
if @project.update(project_params)
|
|
109
|
-
redirect_to @project, status: :see_other, notice: "Project updated"
|
|
110
|
-
else
|
|
111
|
-
render UI::ProjectForm.new(@project), status: :unprocessable_entity
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
private
|
|
116
|
-
def project_params
|
|
117
|
-
params.expect(project: [:name, :status, :description])
|
|
118
|
-
end
|
|
62
|
+
if @project.update(project_params)
|
|
63
|
+
redirect_to @project, status: :see_other, notice: "Project updated"
|
|
64
|
+
else
|
|
65
|
+
render UI::ProjectForm.new(@project, form_id:),
|
|
66
|
+
status: :unprocessable_entity
|
|
119
67
|
end
|
|
120
68
|
```
|
|
121
69
|
|
|
122
|
-
|
|
70
|
+
Wrap the component in a stable frame only when embedded in a larger screen.
|
|
71
|
+
Use a Turbo Stream only when success updates multiple regions. See
|
|
72
|
+
[Inline edit](inline_edit.md).
|
|
123
73
|
|
|
124
74
|
## Tests
|
|
125
75
|
|
|
126
|
-
Assert
|
|
76
|
+
Assert redirect and flash at `303`; submitted values and model errors at `422`;
|
|
77
|
+
and the stable frame ID when embedded. Add a system test for focus or
|
|
78
|
+
multi-region behavior, not to repeat controller coverage.
|
data/docs/rails_conventions.md
CHANGED
|
@@ -1,95 +1,58 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Rails conventions for Nitro Kit applications
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
explicitly includes changing them.
|
|
3
|
+
**Audience:** Coding agents and developers building Rails application features
|
|
4
|
+
with Nitro Kit. These are greenfield defaults; preserve established application
|
|
5
|
+
conventions unless the task explicitly changes them.
|
|
7
6
|
|
|
8
|
-
##
|
|
7
|
+
## Domain and tenancy
|
|
9
8
|
|
|
10
|
-
- Put business rules on domain models.
|
|
11
|
-
|
|
12
|
-
- Scope tenant-owned records through `Current.team` or `Current.account`.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
`docs/patterns/application_foundation.md` before adding authentication,
|
|
19
|
-
invitations, team administration, or account settings.
|
|
20
|
-
- Represent meaningful lifecycle state with a record when the state has
|
|
21
|
-
identity, timing, provenance, or behavior. A published post has a
|
|
22
|
-
`Post::Publication`; it is not merely a `published` boolean.
|
|
23
|
-
- Name capability concerns with adjectives such as `Publishable` only after
|
|
24
|
-
more than one model shares the behavior. Keep the first implementation on
|
|
25
|
-
the model that owns it.
|
|
9
|
+
- Put business rules on domain models. Add service, form, command, or policy
|
|
10
|
+
objects only when a real boundary requires one.
|
|
11
|
+
- Scope tenant-owned records through `Current.team` or `Current.account`. Use
|
|
12
|
+
`Current.user` as the actor, not the tenancy boundary.
|
|
13
|
+
- For team-aware products, model `User`, `Team`, and `Membership`; roles belong
|
|
14
|
+
to memberships. See [Application foundation](patterns/application_foundation.md).
|
|
15
|
+
- Model lifecycle state as a record when it has identity, timing, provenance,
|
|
16
|
+
or behavior.
|
|
26
17
|
|
|
27
|
-
##
|
|
18
|
+
## Routes and responses
|
|
28
19
|
|
|
29
|
-
|
|
30
|
-
|
|
20
|
+
Use REST resources. When an action does not fit the seven standard actions,
|
|
21
|
+
find the missing noun:
|
|
31
22
|
|
|
32
23
|
```ruby
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
resource :publication, only: %i[create destroy],
|
|
36
|
-
module: :posts
|
|
37
|
-
end
|
|
24
|
+
resources :posts do
|
|
25
|
+
resource :publication, only: %i[create destroy], module: :posts
|
|
38
26
|
end
|
|
39
27
|
```
|
|
40
28
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
domain method, then render or redirect.
|
|
29
|
+
Keep controllers thin: load through the tenant boundary, invoke the model, and
|
|
30
|
+
render or redirect.
|
|
44
31
|
|
|
45
|
-
Use
|
|
46
|
-
|
|
47
|
-
representation. Public controllers must query only records visible to the
|
|
48
|
-
public.
|
|
49
|
-
|
|
50
|
-
## Use ordinary Rails responses
|
|
51
|
-
|
|
52
|
-
- Use `form_with` and `NitroKit::FormBuilder` for model-backed forms.
|
|
53
|
-
- Redirect successful non-GET submissions with `status: :see_other`.
|
|
32
|
+
- Use `form_with(..., builder: NitroKit::FormBuilder)` for model-backed forms.
|
|
33
|
+
- Redirect successful mutations with `status: :see_other`.
|
|
54
34
|
- Render the same invalid model with `status: :unprocessable_entity`.
|
|
55
|
-
- Keep an HTML response for
|
|
35
|
+
- Keep an HTML response for Turbo-enhanced flows.
|
|
56
36
|
- Keep filtering, sorting, and pagination in GET parameters.
|
|
57
|
-
- Set flash on the server and render it through
|
|
37
|
+
- Set Rails flash on the server and render it once through
|
|
58
38
|
`NitroKit::Toast::FlashMessages`.
|
|
59
39
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
For an authenticated admin or product area, prefer the hybrid `AppShell` with
|
|
63
|
-
a `Toolbar` in its topbar. The shell supplies responsive navigation disclosure;
|
|
64
|
-
the toolbar supplies the route's one `h1` and its basic actions. The application
|
|
65
|
-
owns one content gutter immediately inside the shell main region.
|
|
66
|
-
|
|
67
|
-
Do not repeat the route title in a `PageHeader`, section heading, card title,
|
|
68
|
-
and table caption. Use `PageHeader` inside an application shell only when the
|
|
69
|
-
page needs a genuinely content-led introduction beyond the toolbar title. A
|
|
70
|
-
single-table index renders the table directly. A form page begins with the
|
|
71
|
-
actual `FormSection`. A detail page begins with status or metadata. Use a Card
|
|
72
|
-
only for a bounded object that needs a surface, never as the automatic wrapper
|
|
73
|
-
for every region.
|
|
74
|
-
|
|
75
|
-
Read `docs/patterns/crud_resource.md` before building a complete resource.
|
|
76
|
-
|
|
77
|
-
## Keep the browser thin
|
|
40
|
+
See [Browser support](browser_support.md) for native month and week input
|
|
41
|
+
limitations, and [Hotwire](hotwire.md) for transport choices.
|
|
78
42
|
|
|
79
|
-
|
|
80
|
-
region, request Streams for multi-target responses, broadcasts for other
|
|
81
|
-
sessions, and Stimulus only for browser-owned behavior. Do not create a JSON
|
|
82
|
-
API or client-side state store for an ordinary Rails screen.
|
|
43
|
+
## Page composition
|
|
83
44
|
|
|
84
|
-
|
|
45
|
+
Use one route title, normally the shell toolbar's `h1`. Add another heading
|
|
46
|
+
only for a genuinely separate region. Do not repeat the title across a toolbar,
|
|
47
|
+
PageHeader, section, Card, and table caption.
|
|
85
48
|
|
|
86
|
-
Use
|
|
49
|
+
Use Card for a bounded object that needs a surface, not as a default wrapper.
|
|
50
|
+
Use one application-owned content gutter inside the shell main region. See
|
|
51
|
+
[Complete product resource](patterns/crud_resource.md) for CRUD composition.
|
|
87
52
|
|
|
88
|
-
|
|
89
|
-
- public visibility of drafts and published records;
|
|
90
|
-
- successful `303` redirects and invalid `422` renders;
|
|
91
|
-
- the noun resource that creates or removes lifecycle state;
|
|
92
|
-
- the HTML fallback before adding a system test;
|
|
93
|
-
- browser behavior with Capybara waiting assertions, never sleeps.
|
|
53
|
+
## Tests
|
|
94
54
|
|
|
95
|
-
|
|
55
|
+
Use Minitest and fixtures. Cover tenant isolation, authorization differences,
|
|
56
|
+
lifecycle resources, public visibility, `303` success, `422` validation, and
|
|
57
|
+
the HTML fallback. Add browser tests for behavior that request tests cannot
|
|
58
|
+
prove, using Capybara waiting assertions instead of sleeps.
|