Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Skills may also include a `references/` directory for supporting markdown files
- **usability-heuristics**: Audit UIs against Nielsen's 10 Usability Heuristics. Produces a structured issue log with severity ratings, heuristic mappings, and remediation guidance. Designed to complement `laws-of-ux` with a formal evaluation methodology.
- **frontend-patterns**: Frontend patterns for Rails applications using Slim templates, Stimulus, and CSS with Optics utilities.
- **stimulus-controllers**: Create and register Stimulus controllers for interactive JavaScript features.
- **turbo-fetch**: Implement dynamic form updates using Turbo Streams and Stimulus (cascading dropdowns, conditional fields, dynamic option lists).
- **dynamic-forms**: Forms that change as they are filled in (dependent dropdowns, conditional fields, dynamic option lists) with the `turbo_form` gem, RoleModel's mechanism for dynamic form interaction. Points at the gem's GitHub README for the API. Existing `turbo_fetch` routes, actions, and `turbo-form`/`turbo-fetch` Stimulus wiring are superseded and converted when touched.
- **form-auto-save**: Automatic form submission with debounce for seamless auto-save experiences.
- **dynamic-nested-attributes**: Rails nested attributes with dynamic add/remove functionality using Turbo Streams and Simple Form.

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ Skills follow the [Agent Skills](https://agentskills.io) open standard and work
| Skill | Description |
|-------|-------------|
| **[action-cable](skills/action-cable)** | ActionCable for real-time features using WebSockets, broadcasting, and Turbo Streams over cable. |
| **[dynamic-forms](skills/dynamic-forms)** | Forms that change as they are filled in (dependent dropdowns, conditional fields, dynamic option lists) with the `turbo_form` gem — RoleModel's mechanism for dynamic form interaction. Supersedes hand-wired `turbo_fetch`. |
| **[dynamic-nested-attributes](skills/dynamic-nested-attributes)** | Rails nested attributes with dynamic add/remove using Turbo Streams and Simple Form. |
| **[form-auto-save](skills/form-auto-save)** | Automatic form submission with debounce for seamless auto-save experiences. |
| **[frontend-patterns](skills/frontend-patterns)** | Frontend patterns for Rails apps using Slim templates, Stimulus, and CSS with Optics utilities. |
| **[stimulus-controllers](skills/stimulus-controllers)** | Create and register Stimulus controllers for interactive JavaScript features. |
| **[turbo-fetch](skills/turbo-fetch)** | Dynamic form updates using Turbo Streams and Stimulus (cascading dropdowns, conditional fields, dynamic option lists). |

### Rails Backend

Expand Down Expand Up @@ -226,7 +226,7 @@ Several skills are designed to complement each other:

- **BEM + Optics** — BEM provides CSS structure; Optics provides design tokens and components. Use both when writing or reviewing stylesheets.
- **Laws of UX + Usability Heuristics + AI UX Enhancements** — Laws of UX provides theoretical principles; Usability Heuristics provides a structured audit methodology; AI UX Enhancements adds automatable review rules. Use together for comprehensive UI reviews.
- **Frontend Patterns + Stimulus + Turbo Fetch + Action Cable** — These cover the full Rails frontend stack using Hotwire and real-time features.
- **Frontend Patterns + Stimulus + Dynamic Forms + Action Cable** — These cover the full Rails frontend stack using Hotwire and real-time features.
- **Routing Patterns + Controller Patterns + Polymorphic Parent Resources** — Routing Patterns covers the route concern that passes the parent type; Controller Patterns covers the controller conventions; Polymorphic Parent Resources joins the two for child resources that hang off many parents. Reach for it at the second parent, when a nested controller would otherwise be copied.
- **Sentry Top Issue + Sentry Issue Fixer** — Top Issue selects the highest-priority Sentry issue; Issue Fixer runs the full diagnosis-and-fix workflow. Run together or invoke the fixer directly with a known issue.
- **Explain + Trace** — Explain orients you to a feature area or concept; Trace follows a specific code path through the stack. Use Explain first to build context, then Trace to dig into a specific flow.
Expand Down
36 changes: 36 additions & 0 deletions skills/dynamic-forms/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
name: dynamic-forms
description: Build forms that change as they are filled in — cascading or dependent dropdowns, conditional fields and sections, option lists that depend on another field, STI type switches — with the turbo_form gem. Use when a form must update without a full page reload, when adding or changing a dynamic form, or when touching existing `turbo_fetch` routes, actions, or `turbo-form`/`turbo-fetch` Stimulus wiring.
metadata:
triggers: "dynamic form, dependent dropdown, cascading dropdown, conditional field, conditional section, dynamic options, update form on change, turbo_form, dynamic: true, dynamic_trigger, dynamic_action, expect_dynamic_form_request, turbo_fetch, turbo-fetch, turbo-form controller"
---

# Dynamic Forms

The [turbo_form](https://github.com/OutlawAndy/turbo_form) gem is RoleModel's mechanism for dynamic form interaction. Every new dynamic form uses it. Do not write a `turbo_fetch` route, controller action, `.turbo_stream` view, or Stimulus controller for a form that turbo_form can drive.

The gem's [README](https://github.com/OutlawAndy/turbo_form#readme) is the API documentation. Read it before writing or changing a dynamic form. This skill does not restate it.

## Setup

Check the `Gemfile` for `turbo_form`. If it is missing, add it and run `bin/rails generate turbo_form:install`, then draw the `:turbo_form` route concern on the resource:

```ruby
resources :materials, concerns: :turbo_form
```

The engine defines the concern and the controller behavior. Do not define a `:turbo_form` or `:turbo_fetch` concern in `config/routes.rb`.

## Shape

```slim
= simple_form_for @material, dynamic: true do |f|
= f.input :type, input_html: { dynamic_trigger: true }
= f.input :substance, collection: @material.substances
```

The trigger re-renders the page through its own `new` or `edit` action with the submitted values assigned, so `@material.substances` answers for the type just picked. The options, conventions, `dynamic_action:` for custom Turbo Stream responses, and `expect_dynamic_form_request` for system tests are all in the README.

## Existing `turbo_fetch` code

Older apps wire dynamic forms by hand: a `turbo_fetch` route concern, a `turbo_fetch` controller action, a `turbo_fetch.turbo_stream` view, a Stimulus controller registered as `turbo-form` or `turbo-fetch` (installed by `rolemodel_rails`, and unrelated to the turbo_form gem despite the name), and `expect_turbo_form_request` or `expect_turbo_fetch_request` in specs. That pattern is superseded. Do not extend it. When a change touches one of these forms, convert it to turbo_form and delete the route, action, view, and spec helper usage it no longer needs.
Comment thread
OutlawAndy marked this conversation as resolved.
63 changes: 15 additions & 48 deletions skills/form-auto-save/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,20 @@ The pattern uses a Stimulus controller (`form-auto-save`) that handles the auto-
- Listens to both `change` and `lexxy:change` events (for custom components)
- Uses passive event listeners for better performance
- Provides `cancel()` and `submit()` methods for programmatic control
- Counts successful saves in `countValue`, which system specs wait on

**Controller Code Pattern:**
```javascript
import { Controller } from '@hotwired/stimulus'

export default class extends Controller {
static DEBOUNCE_TIME = 8000
static values = { count: Number }

connect() {
this.element.addEventListener('change', this.#debounceSubmit.bind(this), { passive: true })
this.element.addEventListener('lexxy:change', this.#debounceSubmit.bind(this), { passive: true })
this.element.addEventListener('turbo:submit-end', ({ detail }) => { if (detail.success) this.countValue++ })
}
Comment thread
OutlawAndy marked this conversation as resolved.

cancel() {
Expand Down Expand Up @@ -96,58 +99,22 @@ Attach the controller to the form element using Stimulus data attributes.

## Testing

For testing auto-save functionality, use the `turbo-fetch` controller alongside `form-auto-save` to track request completion without relying on sleep timers.
Wait on the controller's `countValue`, not on sleep timers. It goes up when Turbo reports a successful submission, so it covers the debounce and the round trip. `turbo_permanent: true` keeps the form, and its count, when the response redirects.
Comment thread
OutlawAndy marked this conversation as resolved.

### Turbo Fetch Controller
Add this controller to your JavaScript controllers:

**File:** `app/javascript/controllers/turbo_fetch_controller.js`
```javascript
import { Controller } from '@hotwired/stimulus'
import { patch } from '@rails/request.js'

export default class extends Controller {
static values = {
url: String,
count: Number,
isRunning: { type: Boolean, default: false }
}

async perform({ params: { url: urlParam, query: queryParams } }) {
this.isRunningValue = true
const body = new FormData(this.element)

if (queryParams) Object.keys(queryParams).forEach(key => body.append(key, queryParams[key]))

const response = await patch(urlParam || this.urlValue, { body, responseKind: 'turbo-stream' })
this.isRunningValue = false
if (response.ok) this.countValue += 1
}
}
```

### Turbo Fetch Helper
Add this helper to your RSpec support files:

**File:** `spec/support/helpers/turbo_fetch_helper.rb`
### Auto Save Helper
**File:** `spec/support/helpers/form_auto_save_helper.rb`
```ruby
module TurboFetchHelper
def expect_turbo_fetch_request
count_value = find("[data-controller='turbo-fetch']")['data-turbo-fetch-count-value'] || 0
module FormAutoSaveHelper
def expect_auto_save
form = find("[data-controller~='form-auto-save']")
count = form['data-form-auto-save-count-value'].to_i
yield
expect(page).to have_selector("[data-turbo-fetch-count-value='#{count_value.to_i + 1}']")
expect(page).to have_css("[data-form-auto-save-count-value='#{count + 1}']", wait: 10)
end
end
```

### View Integration for Testing
Add the `turbo-fetch` controller alongside `form-auto-save`:

```slim
= simple_form_for resource, html: { data: { controller: 'form-auto-save turbo-fetch', turbo_permanent: true } } do |f|
= f.input :field_name
= f.rich_text_area :content
```
Include it for system specs in `spec/support/helpers.rb` with `c.include FormAutoSaveHelper, type: :system`. The `wait:` must be longer than `DEBOUNCE_TIME`.

### System Spec Example
```ruby
Expand All @@ -158,7 +125,7 @@ RSpec.describe 'Form Auto Save', :js do
resource = create(:resource)
visit edit_resource_path(resource)

expect_turbo_fetch_request do
expect_auto_save do
fill_in 'Field name', with: 'Updated value'
end

Expand All @@ -169,7 +136,7 @@ RSpec.describe 'Form Auto Save', :js do
resource = create(:resource)
visit edit_resource_path(resource)

expect_turbo_fetch_request do
expect_auto_save do
fill_in 'Field name', with: 'First'
fill_in 'Field name', with: 'Second'
fill_in 'Field name', with: 'Final'
Expand Down Expand Up @@ -202,7 +169,7 @@ end
- Consider adding "unsaved changes" warning

## Related Patterns
- **Turbo Streams:** For more complex form updates and partial page replacements
- **Dynamic Forms:** Fields that change as the form is filled in belong to the `dynamic-forms` skill (the turbo_form gem)
- **Stimulus Values:** If you need per-instance debounce times
- **Form Validation:** Consider inline validation with auto-save

Expand Down
2 changes: 1 addition & 1 deletion skills/rails-audit/references/rails_antipatterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class User::ActivationsController < ApplicationController
end
```

**Exceptions**: Actions named `turbo_fetch` are an intentional convention for Turbo Stream responses and should not be flagged as REST violations.
**Exceptions**: Don't flag actions named `turbo_fetch` as REST violations. They are the superseded hand-wired dynamic form pattern: report them as candidates for conversion to the turbo_form gem (see the `dynamic-forms` skill).

**Audit Check**: Count public actions per controller. Flag controllers with > 8 actions. Check `config/routes.rb` for excessive `member`/`collection` blocks.

Expand Down
12 changes: 3 additions & 9 deletions skills/routing-patterns/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,7 @@ concern :duplicatable do
end
```

**Example: Dynamic Form Updates (Turbo/AJAX)**
```ruby
concern :turbo_fetch do
patch :turbo_fetch, on: :collection
end
```
**Dynamic forms** use the `:turbo_form` concern, which the `turbo_form` gem's engine defines. Apply it; never define it. See the `dynamic-forms` skill.

**Key Points:**
- Concerns extract common nested resource patterns into reusable modules
Expand All @@ -61,8 +56,8 @@ end
Apply concerns using the `concerns:` option with an array of symbols:

```ruby
resources :products, concerns: %i[duplicatable turbo_fetch]
resources :articles, concerns: %i[commentable duplicatable turbo_fetch]
resources :products, concerns: %i[duplicatable turbo_form]
resources :articles, concerns: %i[commentable duplicatable turbo_form]
```

**Benefits:**
Expand Down Expand Up @@ -123,7 +118,6 @@ Always be explicit about which actions a resource provides. This improves securi

```ruby
# Only specific actions
resources :webhooks, only: [], concerns: %i[turbo_fetch] # No standard REST actions, only custom
resources :duplications, only: %i[create] # Only create action needed
resources :previews, only: %i[show update] # Only show and update

Expand Down
2 changes: 1 addition & 1 deletion skills/stimulus-controllers/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,5 +204,5 @@ end

## Related Skills
- [frontend-patterns](../frontend-patterns/SKILL.md) - HTML and CSS patterns
- [turbo-fetch](../turbo-fetch/SKILL.md) - Dynamic form updates
- [dynamic-forms](../dynamic-forms/SKILL.md) - Dynamic form updates, no Stimulus controller needed
- [testing-patterns](../testing-patterns/SKILL.md) - Testing JavaScript features
Loading
Loading