Dynamic Copy to Clipboard Button in Phoenix LiveView

After making lots of phoenix liveview apps I've found myself re-using the same code snippets all the time. I thought I'd start compiling these snippets so it's easier for myself and others to find them. Here's the first: copying text to the clipboard.

It's a bit tricky to get this right. My requirements are:

  1. Works across browsers
  2. The copied content keeps paragraph breaks in initial text
  3. The copy button changes from "Copy" to "Copied" after a moment or two
  4. I can re-use the copy button easily
  5. I can have multiple copy buttons on the same page and that's not annoying

I found solutions Fly's blog and the Phoenix docs, but they don't satisfy requirements 3 4 or 5. Here's here I do it.

The Code

Here's the code for the copy_button. You can paste this directly into your core_components.ex file. Note that the content attribute is the id of the element you want to copy. When this button is clicked, it sends a JS.dispatch event to the app.js file.

  attr :id, :string, required: true
  attr :content, :string, required: true

  @doc """
  Copy to clipboard button.
  """
  def copy_button(assigns) do
    ~H"""
    <button
      id={@id}
      content={@content}
      phx-click={JS.dispatch("phx:copy", to: "##{@content}")}
      type="button"
      class="rounded-md inline-flex items-center bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
    >
      Copy
    </button>
    """
  end

Next, we need to add the phx:copy event handler to our app.js file. It took some digging, but I discovered that the event dispatch sends the button element as the detail.dispatcher. This means we can set the button text without needing to pass in the button id.

window.addEventListener("phx:copy", (event) => {
  let button = event.detail.dispatcher;
  let text = event.target.innerText;

  navigator.clipboard.writeText(text).then(() => {
    button.innerText = "Copied!";
    setTimeout(() => {
      button.innerText = "Copy";
    }, 2000);
  });
});

Now, use it!

Finally, we can use the component in our liveview template. All it needs is an id and the id of the element we want to copy (content).

<div>
  <p id="stuff">Stuff I want to copy</p>
  <.copy_button id="copy-button" content="stuff" />
</div>

We can have multiple copy buttons on the same page and it won't be an issue.

PS

I realize that this blog itself doesn't have a copy button! That's because it's written in Next.js and I'm still figuring that out 😃.