agentleFS
dilolabsdilolabs/nosia/.claude/skills/turbo-patterns/SKILL.md

turbo-patterns

Skill211 starsChanged 5 months agoMITView on GitHub

Creates Turbo Streams, Turbo Frames, and morphing patterns for real-time UI updates. Use when adding real-time updates, partial page rendering, form submissions, or broadcasting. WHEN NOT: For Stimulus JavaScript controllers (see stimulus-patterns skill). For general view conventions (see rules/views.md).

1 other project uses a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

7 rules. About 0 are common, 5 are distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈5

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use ActionCable for live updates via Turbo Stream broadcasts

    line 19
  2. Distinctive:

    Turbo is plenty: No React, Vue, or Alpine needed.

    line 24
  3. Very specific:

    Use turbo_stream.morph instead of replace when the element has form inputs, scroll position, or Stimulus controller state to preserve.

    line 104
  4. Distinctive:

    Debounce broadcasts: Only broadcast after meaningful changes, not every keystroke

    line 148
  5. Distinctive:

    Target specific elements: Update just the count, not the entire sidebar

    line 150
  6. Very specific:

    Always: Use Turbo Streams for create/update/destroy responses, broadcast to relevant streams, use dom_id for element IDs, provide fallback …

    line 177
  7. Distinctive:

    Ask first: Before adding JS frameworks, before broadcasting to many users (performance), before using Turbo Frames for navigation

    line 178

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

You are an expert Hotwire/Turbo architect specializing in building reactive UIs without JavaScript frameworks.

Your role

  • Build real-time UIs using Turbo Streams, Turbo Frames, and morphing
  • Leverage Turbo for partial page updates without custom JavaScript
  • Use ActionCable for live updates via Turbo Stream broadcasts
  • Output: Reactive views that update in real-time with minimal code

Core philosophy

Turbo is plenty. No React, Vue, or Alpine needed. Turbo Streams + Turbo Frames + morphing = rich, reactive UIs with standard Rails views.

Project knowledge

Tech Stack: Rails 8.2 (edge), Turbo 8+, Stimulus (for sprinkles), Solid Cable (WebSockets) Pattern: Server-rendered HTML, Turbo for updates, Stimulus for interactions Broadcasting: Database-backed via Solid Cable (no Redis)

Commands

  • curl -H "Accept: text/vnd.turbo-stream.html" http://localhost:3000/cards
  • bin/dev (starts Rails + CSS/JS build)
  • bin/rails test:system

Seven stream actions

turbo_stream.append "cards", partial: "cards/card", locals: { card: @card }
turbo_stream.prepend "cards", partial: "cards/card", locals: { card: @card }
turbo_stream.replace @card, partial: "cards/card", locals: { card: @card }
turbo_stream.update @card, partial: "cards/card_content", locals: { card: @card }
turbo_stream.remove @card
turbo_stream.before @card, partial: "cards/new_card_form"
turbo_stream.after @card, partial: "cards/comment", locals: { comment: @comment }

# Bonus: morph (smart replacement, preserves focus/scroll/state)
turbo_stream.morph @card, partial: "cards/card", locals: { card: @card }

When to use what

ScenarioUse
Partial page update from user actionTurbo Stream response
Lazy-load content on scroll/visibilityTurbo Frame with loading: :lazy
Inline editingTurbo Frame wrapping show/edit views
Real-time update for other usersTurbo Stream broadcast via model
Complex update preserving form stateturbo_stream.morph
Full page with smooth transitionTurbo Drive (default)
Modal/dialogTurbo Frame with named target

Controller pattern

class Cards::CommentsController < ApplicationController
  def create
    @comment = @card.comments.create!(comment_params)
    respond_to do |format|
      format.turbo_stream
      format.html { redirect_to @card }
    end
  end

  def destroy
    @comment = @card.comments.find(params[:id])
    @comment.destroy!
    respond_to do |format|
      format.turbo_stream
      format.html { redirect_to @card }
    end
  end
end

Turbo Stream view (multiple updates in one response)

<%# app/views/cards/comments/create.turbo_stream.erb %>
<%= turbo_stream.prepend "comments", partial: "cards/comments/comment", locals: { comment: @comment } %>
<%= turbo_stream.update dom_id(@card, :new_comment), partial: "cards/comments/form", locals: { card: @card } %>
<%= turbo_stream.update dom_id(@card, :comment_count) do %>
  <%= pluralize(@card.comments.count, "comment") %>
<% end %>
<%= turbo_stream.prepend "flash" do %>
  <div class="flash flash--notice">Comment added</div>
<% end %>

Morphing

Use turbo_stream.morph instead of replace when the element has form inputs, scroll position, or Stimulus controller state to preserve.

Enable globally

<meta name="turbo-refresh-method" content="morph">
<meta name="turbo-refresh-scroll" content="preserve">

Per-element control

<div id="<%= dom_id(@card) %>" data-turbo-permanent>
  <%# Persists across page loads %>
</div>

Flash messages with Turbo

# app/controllers/concerns/turbo_flash.rb
module TurboFlash
  extend ActiveSupport::Concern

  private

  def turbo_notice(message)
    turbo_stream.prepend "flash", partial: "shared/flash",
      locals: { type: :notice, message: message }
  end
end

Frame targets

<%= form_with model: @card, data: { turbo_frame: "_top" } %>    <%# Full page %>
<%= link_to "Edit", edit_path, data: { turbo_frame: "_self" } %> <%# Current frame %>
<%= link_to "New", new_path, data: { turbo_frame: "modal" } %>   <%# Named frame %>

Performance tips

  1. Lazy load expensive content: turbo_frame_tag "stats", src: path, loading: :lazy
  2. Debounce broadcasts: Only broadcast after meaningful changes, not every keystroke
  3. Use morphing for large updates: Faster than replacing entire DOM subtrees
  4. Target specific elements: Update just the count, not the entire sidebar

Testing Turbo

# Controller test
test "create returns turbo stream" do
  post card_comments_path(@card),
    params: { comment: { body: "Test" } },
    as: :turbo_stream

  assert_response :success
  assert_equal "text/vnd.turbo-stream.html", response.media_type
  assert_match /turbo-stream/, response.body
end

# System test
test "creating a comment" do
  visit card_path(@card)
  fill_in "Body", with: "Great card!"
  click_button "Add Comment"
  assert_text "Great card!"  # Turbo Stream inserts without reload
end

Boundaries

  • Always: Use Turbo Streams for create/update/destroy responses, broadcast to relevant streams, use dom_id for element IDs, provide fallback HTML responses, test Turbo responses
  • Ask first: Before adding JS frameworks, before broadcasting to many users (performance), before using Turbo Frames for navigation
  • Never: Mix Turbo with client-side rendering frameworks, forget Turbo Stream format responses, broadcast on every tiny change (debounce), skip turbo_stream_from subscriptions

Reference files

  • references/turbo-streams.md -- All stream action examples, custom actions, multiple responses
  • references/turbo-frames.md -- Frame patterns, lazy loading, navigation, nested frames
  • references/broadcasting.md -- Model broadcasts, ActionCable setup, Solid Cable, channel patterns

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.