home / skills / thibautbaissac / rails_ai_agents / hotwire-patterns

hotwire-patterns skill

/skills/hotwire-patterns

This skill helps you implement and orchestrate Hotwire patterns (Turbo Frames, Streams, Stimulus) for real-time, interactive Rails UIs.

npx playbooks add skill thibautbaissac/rails_ai_agents --skill hotwire-patterns

Review the files below or copy the command above to add this skill to your agents.

Files (4)
SKILL.md
5.2 KB
---
name: hotwire-patterns
description: Implements Hotwire patterns with Turbo Frames, Turbo Streams, and Stimulus controllers. Use when building interactive UIs, real-time updates, form handling, partial page updates, or when user mentions Turbo, Stimulus, or Hotwire.
allowed-tools: Read, Write, Edit, Bash
---

# Hotwire Patterns for Rails 8

## Overview

Hotwire = HTML Over The Wire - Build modern web apps without writing much JavaScript.

| Component | Purpose | Use Case |
|-----------|---------|----------|
| **Turbo Drive** | SPA-like navigation | Automatic, no code needed |
| **Turbo Frames** | Partial page updates | Inline editing, tabbed content |
| **Turbo Streams** | Real-time DOM updates | Live updates, flash messages |
| **Stimulus** | JavaScript sprinkles | Toggles, forms, interactions |

## Quick Start

### Turbo Frames (Scoped Navigation)

```erb
<%# app/views/posts/index.html.erb %>
<%= turbo_frame_tag "posts" do %>
  <%= render @posts %>
  <%= link_to "Load More", posts_path(page: 2) %>
<% end %>

<%# Clicking "Load More" only updates content inside this frame %>
```

### Turbo Streams (Real-time Updates)

```erb
<%# app/views/posts/create.turbo_stream.erb %>
<%= turbo_stream.prepend "posts", @post %>
<%= turbo_stream.update "flash", partial: "shared/flash" %>
```

### Stimulus Controller

```javascript
// app/javascript/controllers/toggle_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["content"]

  toggle() {
    this.contentTarget.classList.toggle("hidden")
  }
}
```

```erb
<div data-controller="toggle">
  <button data-action="toggle#toggle">Toggle</button>
  <div data-toggle-target="content">Hidden content</div>
</div>
```

## Workflow Checklist

```
Hotwire Implementation:
- [ ] Identify update scope (full page vs partial)
- [ ] Choose pattern (Frame vs Stream vs Stimulus)
- [ ] Implement server response
- [ ] Add client-side markup
- [ ] Test with and without JavaScript
- [ ] Write system spec
```

## When to Use Each Pattern

| Scenario | Pattern | Why |
|----------|---------|-----|
| Inline edit | Turbo Frame | Scoped replacement |
| Form submission | Turbo Stream | Multiple updates |
| Real-time feed | Turbo Stream + ActionCable | Push updates |
| Toggle visibility | Stimulus | No server needed |
| Form validation | Stimulus | Client-side feedback |
| Infinite scroll | Turbo Frame + lazy loading | Paginated content |
| Modal dialogs | Turbo Frame | Load on demand |
| Flash messages | Turbo Stream | Append/update |

## References

- See [turbo-frames.md](reference/turbo-frames.md) for frame patterns
- See [turbo-streams.md](reference/turbo-streams.md) for stream patterns
- See [stimulus.md](reference/stimulus.md) for controller patterns

## Testing Hotwire

### System Specs

```ruby
# spec/system/posts_spec.rb
require 'rails_helper'

RSpec.describe "Posts", type: :system do
  before { driven_by(:selenium_chrome_headless) }

  it "updates post inline with Turbo Frame" do
    post = create(:post, title: "Original")

    visit posts_path
    within("#post_#{post.id}") do
      click_link "Edit"
      fill_in "Title", with: "Updated"
      click_button "Save"
    end

    expect(page).to have_content("Updated")
    expect(page).not_to have_content("Original")
  end

  it "adds comment with Turbo Stream" do
    post = create(:post)

    visit post_path(post)
    fill_in "Comment", with: "Great post!"
    click_button "Add Comment"

    within("#comments") do
      expect(page).to have_content("Great post!")
    end
  end
end
```

### Request Specs for Turbo Stream

```ruby
# spec/requests/posts_spec.rb
RSpec.describe "Posts", type: :request do
  describe "POST /posts" do
    let(:valid_params) { { post: { title: "Test" } } }

    it "returns turbo stream response" do
      post posts_path, params: valid_params,
           headers: { "Accept" => "text/vnd.turbo-stream.html" }

      expect(response.media_type).to eq("text/vnd.turbo-stream.html")
      expect(response.body).to include("turbo-stream")
    end
  end
end
```

## Common Patterns

### Inline Editing with Frame

```erb
<%# _post.html.erb %>
<%= turbo_frame_tag dom_id(post) do %>
  <article>
    <h2><%= post.title %></h2>
    <%= link_to "Edit", edit_post_path(post) %>
  </article>
<% end %>

<%# edit.html.erb %>
<%= turbo_frame_tag dom_id(@post) do %>
  <%= form_with model: @post do |f| %>
    <%= f.text_field :title %>
    <%= f.submit "Save" %>
    <%= link_to "Cancel", @post %>
  <% end %>
<% end %>
```

### Flash Messages with Stream

```ruby
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  after_action :flash_to_turbo_stream, if: -> { request.format.turbo_stream? }

  private

  def flash_to_turbo_stream
    flash.each do |type, message|
      flash.now[type] = message
    end
  end
end
```

### Lazy Loading Frame

```erb
<%= turbo_frame_tag "comments", src: post_comments_path(@post), loading: :lazy do %>
  <p>Loading comments...</p>
<% end %>
```

## Debugging Tips

1. **Frame not updating?** Check frame IDs match exactly
2. **Stream not working?** Verify `Accept` header includes turbo-stream
3. **Stimulus not firing?** Check controller name matches file name
4. **Events not working?** Use `data-action="event->controller#method"`

Overview

This skill implements Hotwire patterns for Rails 8.1 using Turbo Frames, Turbo Streams, and Stimulus controllers. It provides practical patterns, test examples, and a workflow checklist to build interactive UIs with minimal custom JavaScript. Use it to structure partial updates, real-time DOM changes, and small client-side behaviors.

How this skill works

The skill inspects UI intentions and suggests the right Hotwire pattern: Turbo Frames for scoped partial updates and navigation, Turbo Streams for server-pushed DOM changes, and Stimulus for lightweight client interactions. It supplies sample markup, controller snippets, and test examples (system and request specs) so you can implement and verify each pattern. It also includes debugging tips and a checklist to ensure reliable behavior with and without JavaScript.

When to use it

  • Inline edit or scoped replacement of a page region (use Turbo Frame).
  • Form submission that needs multiple DOM updates (use Turbo Stream).
  • Real-time feeds, notifications, or live updates (use Turbo Streams with ActionCable).
  • Small UI interactions like toggles or validation hints (use Stimulus).
  • Lazy-loading paginated content or comments (Turbo Frame with src and loading).

Best practices

  • Choose the smallest scope that solves the interaction (frame over full-page reload).
  • Return the correct response format: HTML for frames, text/vnd.turbo-stream.html for streams.
  • Test with and without JavaScript and add system specs that exercise real flows.
  • Match frame IDs and dom_id exactly; validate Accept header in request specs.
  • Keep Stimulus controllers small and focused; use data-action and data-target conventions.

Example use cases

  • Inline post editing: load the edit form inside a turbo_frame_tag and replace only that post element.
  • Adding comments: POST returns a turbo_stream to prepend the new comment and update flash.
  • Infinite scroll: a frame wraps the list with a lazy src that fetches next page when needed.
  • Modal dialogs: load modal content on demand in a Turbo Frame to avoid heavy JS modals.
  • Flash messages: after_action converts flash into turbo_stream updates for consistent UX.

FAQ

What response format should my controller return for Turbo Streams?

Return 'text/vnd.turbo-stream.html' and render a .turbo_stream.erb template containing turbo_stream actions.

Why does my Turbo Frame not update?

Verify the turbo_frame_tag ID matches the returned frame ID exactly and that the response contains only the frame markup.