skills/hotwire-patterns/SKILL.md
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.
npx skillsauth add fernandezbaptiste/rails_ai_agents hotwire-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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 |
<%# 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 %>
<%# app/views/posts/create.turbo_stream.erb %>
<%= turbo_stream.prepend "posts", @post %>
<%= turbo_stream.update "flash", partial: "shared/flash" %>
// 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")
}
}
<div data-controller="toggle">
<button data-action="toggle#toggle">Toggle</button>
<div data-toggle-target="content">Hidden content</div>
</div>
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
| 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 |
# 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
# 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
<%# _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 %>
# 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
<%= turbo_frame_tag "comments", src: post_comments_path(@post), loading: :lazy do %>
<p>Loading comments...</p>
<% end %>
Accept header includes turbo-streamdata-action="event->controller#method"development
Creates ViewComponents for reusable UI elements with TDD. Use when building reusable UI components, extracting complex partials, creating cards/tables/badges/modals, or when user mentions ViewComponent, components, or reusable UI.
development
Guides Test-Driven Development workflow with Red-Green-Refactor cycle. Use when the user wants to implement a feature using TDD, write tests first, follow test-driven practices, or mentions red-green-refactor.
data-ai
Configures Solid Queue for background jobs in Rails 8. Use when setting up background processing, creating background jobs, configuring job queues, or migrating from Sidekiq to Solid Queue.
testing
Creates service objects following single-responsibility principle with comprehensive specs. Use when extracting business logic from controllers, creating complex operations, implementing interactors, or when user mentions service objects or POROs.