Copying without a page reload
A button that changes nothing on the page except the clipboard
A lesson's image manager shows a generated image URL next to a small copy icon. Clicking the icon puts the URL on the clipboard, a toast confirms it, and nothing else on the page moves. No form submits, no request goes to the server, and no Turbo navigation happens. The whole feature lives in one small file, this app's app/javascript/controllers/ui/clipboard_controller.js:
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["source"]
static values = { message: String }
copy(event) {
if (event) {
event.preventDefault()
}
const text = this.sourceTarget.value || this.sourceTarget.textContent
if (this.sourceTarget.select) {
this.sourceTarget.select()
}
// navigator.clipboard is undefined outside a secure context and in
// browsers without the async Clipboard API. Calling writeText on it
// would throw before any promise exists, skipping the fallback below,
// so turn that case into a rejection the .catch() can handle.
const write = navigator.clipboard?.writeText
? navigator.clipboard.writeText(text)
: Promise.reject(new Error("Clipboard API unavailable"))
write.then(() => {
this.showToast()
}).catch(err => {
console.error('Failed to copy text: ', err)
try {
// execCommand reports failure by returning false, not by throwing.
if (document.execCommand('copy')) this.showToast()
} catch (err) {
console.error('Fallback: Oops, unable to copy', err)
}
})
}
showToast() {
// builds and appends a toast element; lesson 6 covers this method in full
}
}
Everything above matches the file in this app, except showToast's body, which is cut here (the comment marks the cut) because lesson 6 quotes it in full. This course reads the rest line by line, then follows the controller to the three pages that attach it to HTML.
Why this needs JavaScript at all
A plain <a href="..."> link changes the page, and so does a form submit. Writing to the clipboard is different: it's a browser capability with no HTML element of its own, no <clipboard> tag. Something has to run in the browser in response to a click and call a browser API. That's Stimulus's job here: attach a small class to a piece of the DOM and give it one thing to do when clicked.
What a Stimulus controller is, in this file's terms
export default class extends Controller is the whole shape. Controller, imported from @hotwired/stimulus, is the base class every Stimulus controller extends. static targets = ["source"] says the controller expects one named element inside it, called source: the element whose text gets copied. static values = { message: String } reads the toast's text from the page, so each page can pass a translated message (lesson 6). copy(event) is an ordinary instance method, called when the wiring from lesson 3 fires a click. Nothing here is specific to Rails or unusual JavaScript. It's a small class with one public method, imported and registered like every controller under app/javascript/controllers/.
What this course covers
Lesson 2 opens the HTML that connects this controller to a page: the data-controller and data-*-target attributes. Lesson 3 covers data-action, including a difference between two elements in the same markup that's worth understanding. Lesson 4 covers navigator.clipboard.writeText(), the modern Clipboard API this controller calls first. Lesson 5 covers the document.execCommand('copy') fallback: why it's deprecated, why the code still calls it, and how the code makes sure it gets the chance. Lesson 6 opens showToast() in full, a method that builds an HTML toast and reuses another controller's dismiss behavior. Lesson 7 reads the other pages that use this controller and closes the course.
Keep going with lesson 2
Targets: wiring the source element
Shows how data-controller and data-ui--clipboard-target attach the controller and produce this.sourceTarget for the input.
A free account also gives you the quizzes, your progress and notes, and the discussion.