Tools: Implementing Copy-to-Clipboard in Rails 8: The Modern Way

Tools: Implementing Copy-to-Clipboard in Rails 8: The Modern Way

1. Generate the Controller ## 2. The JavaScript Code ## 3. Usage Examples (ERB) ## Scenario A: Copying from an Input Field ## Scenario B: Copying Static Text ## Important Notes for Production Here is a robust, modern Stimulus controller for Rails 8 to handle copying text to the clipboard. It uses the modern navigator.clipboard API and includes user feedback (changing the button text to "Copied!" temporarily). Run the Rails generator command: Update app/javascript/controllers/clipboard_controller.js. This implementation handles copying from input fields, textareas, or standard HTML elements (like a div or span). This is useful for API keys or shareable URLs. This is useful for copying codes or IDs displayed in a div or span. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to ? It will become hidden in your post, but will still be visible via the comment's permalink. as well , this person and/or CODE_BLOCK: bin/rails generate stimulus clipboard CODE_BLOCK: bin/rails generate stimulus clipboard CODE_BLOCK: bin/rails generate stimulus clipboard COMMAND_BLOCK: import { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = ["source", "button"] static values = { successDuration: { type: Number, default: 2000 } } connect() { // Save the original button text so we can revert it later if (this.hasButtonTarget) { this.originalText = this.buttonTarget.innerText } } copy(event) { event.preventDefault() const text = this.textToCopy() // Copy to clipboard navigator.clipboard.writeText(text).then(() => { this.showSuccess() }).catch(() => { console.error("Failed to copy text") }) } // Helper to determine what text to copy textToCopy() { if (this.sourceTarget.nodeName === "INPUT" || this.sourceTarget.nodeName === "TEXTAREA") { return this.sourceTarget.value } else { return this.sourceTarget.textContent.trim() } } // Visual feedback showSuccess() { if (!this.hasButtonTarget) return this.buttonTarget.innerText = "Copied!" this.buttonTarget.disabled = true setTimeout(() => { this.buttonTarget.innerText = this.originalText this.buttonTarget.disabled = false }, this.successDurationValue) } } COMMAND_BLOCK: import { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = ["source", "button"] static values = { successDuration: { type: Number, default: 2000 } } connect() { // Save the original button text so we can revert it later if (this.hasButtonTarget) { this.originalText = this.buttonTarget.innerText } } copy(event) { event.preventDefault() const text = this.textToCopy() // Copy to clipboard navigator.clipboard.writeText(text).then(() => { this.showSuccess() }).catch(() => { console.error("Failed to copy text") }) } // Helper to determine what text to copy textToCopy() { if (this.sourceTarget.nodeName === "INPUT" || this.sourceTarget.nodeName === "TEXTAREA") { return this.sourceTarget.value } else { return this.sourceTarget.textContent.trim() } } // Visual feedback showSuccess() { if (!this.hasButtonTarget) return this.buttonTarget.innerText = "Copied!" this.buttonTarget.disabled = true setTimeout(() => { this.buttonTarget.innerText = this.originalText this.buttonTarget.disabled = false }, this.successDurationValue) } } COMMAND_BLOCK: import { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = ["source", "button"] static values = { successDuration: { type: Number, default: 2000 } } connect() { // Save the original button text so we can revert it later if (this.hasButtonTarget) { this.originalText = this.buttonTarget.innerText } } copy(event) { event.preventDefault() const text = this.textToCopy() // Copy to clipboard navigator.clipboard.writeText(text).then(() => { this.showSuccess() }).catch(() => { console.error("Failed to copy text") }) } // Helper to determine what text to copy textToCopy() { if (this.sourceTarget.nodeName === "INPUT" || this.sourceTarget.nodeName === "TEXTAREA") { return this.sourceTarget.value } else { return this.sourceTarget.textContent.trim() } } // Visual feedback showSuccess() { if (!this.hasButtonTarget) return this.buttonTarget.innerText = "Copied!" this.buttonTarget.disabled = true setTimeout(() => { this.buttonTarget.innerText = this.originalText this.buttonTarget.disabled = false }, this.successDurationValue) } } CODE_BLOCK: <div data-controller="clipboard"> <!-- The Source --> <input type="text" value="https://myapp.com/invite/123" readonly class="border p-2 rounded" data-clipboard-target="source"> <!-- The Trigger --> <button type="button" class="bg-blue-500 text-white p-2 rounded" data-action="clipboard#copy" data-clipboard-target="button"> Copy Link </button> </div> CODE_BLOCK: <div data-controller="clipboard"> <!-- The Source --> <input type="text" value="https://myapp.com/invite/123" readonly class="border p-2 rounded" data-clipboard-target="source"> <!-- The Trigger --> <button type="button" class="bg-blue-500 text-white p-2 rounded" data-action="clipboard#copy" data-clipboard-target="button"> Copy Link </button> </div> CODE_BLOCK: <div data-controller="clipboard"> <!-- The Source --> <input type="text" value="https://myapp.com/invite/123" readonly class="border p-2 rounded" data-clipboard-target="source"> <!-- The Trigger --> <button type="button" class="bg-blue-500 text-white p-2 rounded" data-action="clipboard#copy" data-clipboard-target="button"> Copy Link </button> </div> CODE_BLOCK: <div data-controller="clipboard" data-clipboard-success-duration-value="1000"> <p> Discount Code: <strong data-clipboard-target="source">SUMMER2025</strong> </p> <button type="button" data-action="clipboard#copy" data-clipboard-target="button"> Copy Code </button> </div> CODE_BLOCK: <div data-controller="clipboard" data-clipboard-success-duration-value="1000"> <p> Discount Code: <strong data-clipboard-target="source">SUMMER2025</strong> </p> <button type="button" data-action="clipboard#copy" data-clipboard-target="button"> Copy Code </button> </div> CODE_BLOCK: <div data-controller="clipboard" data-clipboard-success-duration-value="1000"> <p> Discount Code: <strong data-clipboard-target="source">SUMMER2025</strong> </p> <button type="button" data-action="clipboard#copy" data-clipboard-target="button"> Copy Code </button> </div> - HTTPS Requirement: The navigator.clipboard API allows writing to the clipboard only in a Secure Context. This means it works on localhost, but on production, your site must be served over HTTPS. If you use HTTP, the copy function will fail. - Icons: If your button contains an icon (like an SVG) instead of text, this.buttonTarget.innerText = "Copied!" will remove the icon. Solution: Instead of changing innerText, toggle a CSS class (e.g., hidden) on two different span elements inside the button (one for the icon, one for the "Copied" text). - Solution: Instead of changing innerText, toggle a CSS class (e.g., hidden) on two different span elements inside the button (one for the icon, one for the "Copied" text). - Solution: Instead of changing innerText, toggle a CSS class (e.g., hidden) on two different span elements inside the button (one for the icon, one for the "Copied" text).