--- name: hotwire-turbo-stimulus description: This skill should be used when the user asks about Hotwire, Turbo Drive, Turbo Frames, Turbo Streams, Stimulus controllers, frontend interactivity, server-rendered HTML updates, websocket updates, progressive enhancement, data attributes, importing JavaScript modules, or building interactive UIs without React/Vue. Also use when discussing the "HTML over the wire" approach or NO BUILD philosophy. Examples: Context: User wants to update part of a page without full reload user: "How can I update just the product list without refreshing the entire page?" assistant: "I'll show you Turbo Frames for partial page updates." This relates to Turbo Frames for scoped page updates. Context: User needs real-time updates user: "How do I push updates to users when new comments are added?" assistant: "Turbo Streams over WebSockets is perfect for this. Let me explain." This involves Turbo Streams with Action Cable for real-time broadcasts. Context: User wants JavaScript interactivity user: "I need a dropdown menu that opens on click" assistant: "This is a perfect use case for a Stimulus controller. Let me show you." This relates to Stimulus for lightweight JavaScript interactions. --- # Hotwire/Turbo/Stimulus: Modern Rails Frontend ## Overview Hotwire (HTML Over The Wire) is Rails' answer to frontend complexity. Instead of shipping JSON to a heavy JavaScript framework, Hotwire delivers HTML directly from the server. **Hotwire consists of:** - **Turbo Drive**: Fast navigation without full page reloads - **Turbo Frames**: Update specific page sections - **Turbo Streams**: Real-time HTML updates via WebSockets or HTTP - **Stimulus**: Lightweight JavaScript controllers for sprinkles of interactivity Together, they provide rich, reactive UIs with minimal JavaScript and no build step. ## Philosophy Hotwire reflects Rails 8's core principles: - **NO BUILD**: No webpack, no complex toolchains - **Server-rendered**: HTML comes from Rails, not JavaScript - **Progressive enhancement**: Start with HTML, add JavaScript as needed - **Minimal JavaScript**: Write only what browsers can't do - **Integrated**: Works seamlessly with Rails conventions Most applications need less JavaScript than you think. Hotwire proves it. ## Turbo Drive ### What It Does Turbo Drive intercepts link clicks and form submissions, replacing full page loads with AJAX requests that update the page content. **Without Turbo Drive:** ``` Click link → Browser requests page → Full page reload → JavaScript re-initializes ``` **With Turbo Drive:** ``` Click link → AJAX request → Replace → Fast transition ``` Benefits: - Instant navigation - Preserved scroll position - CSS/JS stay loaded - Smooth transitions ### How It Works Automatically enabled when you include Turbo: ```javascript // app/javascript/application.js import "@hotwired/turbo-rails" ``` Now all links and forms use Turbo Drive automatically: ```erb <%= link_to "Products", products_path %> <%= form_with model: @product do |f| %> <% end %> ``` ### Disabling Turbo Drive For specific links/forms: ```erb <%= link_to "External", "https://example.com", data: { turbo: false } %> <%= form_with model: @product, data: { turbo: false } do |f| %> <% end %> ``` ### Turbo Drive Progress Bar Built-in progress indicator for navigation: ```css /* Customize progress bar */ .turbo-progress-bar { height: 5px; background-color: #0076ff; } ``` ## Turbo Frames ### What They Do Turbo Frames let you update specific page sections without affecting the rest of the page. **Traditional approach:** ``` Update product → Full page reload → Entire page re-renders ``` **With Turbo Frames:** ``` Update product → Only product frame updates → Rest of page untouched ``` ### Basic Usage ```erb

Products

<%= turbo_frame_tag "new_product" do %> <%= link_to "New Product", new_product_path %> <% end %>
<%= render @products %>
<%= turbo_frame_tag "new_product" do %>

New Product

<%= form_with model: @product do |f| %> <%= f.text_field :name %> <%= f.submit %> <% end %> <% end %> ``` When clicking "New Product", only the `new_product` frame updates—the rest of the page stays. ### dom_id Helper Rails provides `dom_id` for consistent frame IDs: ```erb <%= turbo_frame_tag dom_id(@product) do %> <%= render @product %> <% end %> ``` ### Frame Navigation ```erb <%= turbo_frame_tag "products" do %> <% @products.each do |product| %> <%= link_to product.name, product_path(product) %> <% end %> <% end %> ``` ### Breaking Out of Frames Navigate the full page from within a frame: ```erb <%= link_to "View All", products_path, data: { turbo_frame: "_top" } %> ``` ### Lazy Loading Frames Load content on demand: ```erb <%= turbo_frame_tag "lazy_content", src: lazy_products_path do %> Loading... <% end %> ``` See `references/turbo-frames.md` for advanced patterns. ## Turbo Streams ### What They Do Turbo Streams deliver targeted HTML updates after form submissions or via WebSockets. **Seven Stream Actions:** - **append**: Add to bottom of target - **prepend**: Add to top of target - **replace**: Replace entire target - **update**: Replace target's content (keeps the target element) - **remove**: Delete target - **before**: Insert before target - **after**: Insert after target ### After Form Submission ```ruby # app/controllers/products_controller.rb def create @product = Product.new(product_params) respond_to do |format| if @product.save format.turbo_stream { render turbo_stream: turbo_stream.prepend("products", @product) } format.html { redirect_to @product } else format.turbo_stream { render turbo_stream: turbo_stream.replace(dom_id(@product), partial: "form", locals: { product: @product }) } format.html { render :new, status: :unprocessable_entity } end end end ``` ```erb
<%= render @products %>
<%= turbo_frame_tag "new_product" do %> <%= render "form", product: @product %> <% end %> ``` When form submits, new product prepends to #products list without page reload. ### Broadcast Updates (Real-Time) Stream changes to all connected users: ```ruby # app/models/product.rb class Product < ApplicationRecord after_create_commit -> { broadcast_prepend_to "products", target: "products" } after_update_commit -> { broadcast_replace_to "products" } after_destroy_commit -> { broadcast_remove_to "products" } end ``` ```erb <%= turbo_stream_from "products" %>
<%= render @products %>
``` Now when any user creates/updates/deletes a product, all connected users see the change instantly. ### Multiple Stream Actions ```ruby # app/views/products/create.turbo_stream.erb <%= turbo_stream.prepend "products", @product %> <%= turbo_stream.update "counter", Product.count %> <%= turbo_stream.replace "flash", partial: "shared/flash" %> ``` See `references/turbo-streams.md` for broadcasting patterns. ## Stimulus ### What It Does Stimulus is a modest JavaScript framework for adding behavior to HTML. **Philosophy:** - HTML is the source of truth - Controllers connect to HTML via data attributes - Small, focused controllers - Progressive enhancement ### Basic Structure ```javascript // app/javascript/controllers/dropdown_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = ["menu"] toggle() { this.menuTarget.classList.toggle("hidden") } } ``` ```erb
``` ### Data Attributes Stimulus uses three data attributes: 1. **data-controller**: Connects element to Stimulus controller 2. **data-{controller}-target**: Marks element as a target 3. **data-action**: Connects event to controller method ### Targets Reference elements in controllers: ```javascript export default class extends Controller { static targets = ["input", "output", "button"] connect() { console.log(this.inputTarget) // First matching element console.log(this.inputTargets) // All matching elements console.log(this.hasInputTarget) // Boolean check } } ``` ```erb
``` ### Actions Connect events to methods: ```erb
``` ### Values Pass data to controllers: ```javascript export default class extends Controller { static values = { url: String, count: Number, active: Boolean } connect() { console.log(this.urlValue) console.log(this.countValue) console.log(this.activeValue) } urlValueChanged(value, previousValue) { // Called when value changes } } ``` ```erb
``` See `references/stimulus-controllers.md` for controller patterns. ## Integration Patterns ### Turbo Frames + Stimulus ```erb
<%= turbo_frame_tag "stats", src: stats_path do %> Loading... <% end %>
``` ```javascript // auto_refresh_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { static values = { interval: Number } static targets = ["frame"] connect() { this.startRefreshing() } disconnect() { this.stopRefreshing() } startRefreshing() { this.refreshTimer = setInterval(() => { this.element.querySelector('turbo-frame').reload() }, this.intervalValue) } stopRefreshing() { if (this.refreshTimer) { clearInterval(this.refreshTimer) } } } ``` ### Inline Editing ```erb <%= turbo_frame_tag dom_id(product) do %>
<%= product.name %>
<% end %> ``` ## Common Patterns ### Modal with Turbo Frame ```erb <%= turbo_frame_tag "modal" %> <%= link_to "New Product", new_product_path, data: { turbo_frame: "modal" } %> ``` Form renders inside modal frame. ### Infinite Scroll ```erb
<%= turbo_frame_tag "products", src: products_path(page: 1) %>
``` ### Live Search ```erb <%= form_with url: search_products_path, method: :get, data: { turbo_frame: "results", turbo_action: "advance" } do |f| %> <%= f.search_field :q, data: { action: "input->debounce#search" } %> <% end %> <%= turbo_frame_tag "results" do %> <% end %> ``` ## Further Reading For deeper exploration: - **`references/turbo-frames.md`**: Complete Turbo Frames guide with patterns - **`references/turbo-streams.md`**: Broadcasting and real-time updates - **`references/stimulus-controllers.md`**: Building Stimulus controllers For code examples (in `examples/`): - **`autosave_controller.js`**: Auto-save form data - **`character_counter_controller.js`**: Live character counting - **`clipboard_controller.js`**: Copy to clipboard functionality - **`confirm_controller.js`**: Confirmation dialogs - **`dropdown_controller.js`**: Interactive dropdown menus - **`form_controller.js`**: Form handling and validation - **`infinite_scroll_controller.js`**: Infinite scroll pagination - **`modal_controller.js`**: Modal dialogs with Stimulus - **`nested_form_controller.js`**: Dynamic nested form fields - **`remote_form_controller.js`**: AJAX form submissions - **`search_controller.js`**: Real-time search filtering - **`slideshow_controller.js`**: Image carousel/slideshow - **`tabs_controller.js`**: Tab navigation - **`toggle_controller.js`**: Toggle visibility patterns ## Summary Hotwire provides: - **Turbo Drive**: Fast navigation without full reloads - **Turbo Frames**: Scoped page updates - **Turbo Streams**: Real-time HTML over WebSockets - **Stimulus**: Lightweight JavaScript controllers - **NO BUILD**: No webpack, no complex tooling - **Server-rendered**: HTML from Rails, not JSON APIs Master Hotwire and build rich, interactive applications with minimal JavaScript.