40 · the JavaScript Turbo doesn't do

Each message now has a copy link button.

Click one. Nothing happens. There's no server round trip that makes sense here: the text is already on the page, and the clipboard belongs to the browser. This is the kind of thing Turbo leaves alone on purpose.

Look at the HTML, though. The button already says what it wants: data-action="clipboard#copy". It's waiting for someone to answer.

→ click copy link on any message
40 · Stimulus

Nine lines answer it.

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

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

  copy() {
    const text = this.sourceTarget.textContent
    navigator.clipboard.writeText(text)
  }
}

Load Stimulus and click again. In Rails, stimulus-rails registers every file in app/javascript/controllers, so the identifier clipboard finds clipboard_controller.js by name. No querySelector, no event listener written by hand.

→ switch it on, click copy link
40 · three words

Controller, target, action.

Hover each attribute to light up the element it marks in the page.

data-controller="clipboard"clipboard_controller.js is instantiated and attached to this element and everything inside it
data-clipboard-target="source"static targets = ["source"] → this.sourceTarget
data-action="clipboard#copy"on click, call copy(). (click is the default event for a button; write click->clipboard#copy to be explicit, input->…, keydown.esc->…)

Read the HTML alone and you can tell what it does. That's the design goal. Most frameworks build HTML from state held in JavaScript; Stimulus attaches behaviour to HTML the server already wrote.

40 · why it survives Turbo

Post a message. Then copy its link.

The new message arrived as a stream, with a data-controller on it. Watch its pencil note in the margin change: the controller connected the moment the element landed. Its copy button works with no setup.

Stimulus watches the whole document with a MutationObserver. Any element that appears, from a Drive visit, a frame, a stream, or your own code, gets its controllers connected. Any element that leaves gets them disconnected. This is why you never write turbo:load handlers again: there's nothing to re-initialise.

→ post, then click copy link on the new message