Copy To Clipboard - LiveView or JavaScript

Hello, I am attempting to create a Copy To Clipboard Icon. I have looked at this so far: https://www.w3schools.com/howto/howto_js_copy_clipboard.asp which seems straightforward.

What is the best approach to this. The text I am trying to copy code is this:

   <% link = Routes.sample_url(@conn, :new, ref: @user.token) %>
          <input type="text" value="<%= link %>" id="myInput">

There is a variety of considerations when you copy to clipboard. Here are a few options that cover a few cases.


Also a button will be better suited.

Next, you will have to figure out IF you really need LiveView. Likely you want to extract text from an element and copyToClipboard, which doesn’t require any server interaction. I would suggest starting with a plain JS approach.

If you do need LV, you probably want a button with a phx-hook docs

Perhaps I should create a plain phoenix application with an example. Do you think that might help?

Edit:

Also a custom web component is available to use as well.

2 Likes

I found this post when I was looking to implement a Copy to Clipboard button in my Todo Tasks site.

The goal is to be able to copy the rendered HTML into any new email I send with Gmail in the browser.

Another goal is to not use any library, instead just a few lines of pure Javascript, thus I end up to found the solution here, and the result is:

LiveView Hook

let Hooks = {}
Hooks.CopyToClipboard = {
  mounted() {
    this.el.addEventListener("click", e => {

      // @link https://css-tricks.com/copy-paste-the-web/
      // Select the email link anchor text
      let html = document.querySelector('#todo-list-container');

      let range = document.createRange();
      range.selectNode(html);
      window.getSelection().addRange(range);

      try {
        
        // Now that we've selected the anchor text, execute the copy command
        let successful = document.execCommand('copy');
        let msg = successful ? 'successful' : 'unsuccessful';

        if (! successful) {
          alert('Copy to clipboard failed. Please select the area to copy and use ctrl + c shortcut keys.')
        }

      } catch(err) {
        alert('Copy to clipboard error. Please select the area to copy and use ctrl + c shortcut keys.')
      }

      // Remove the selections - NOTE: Should use
      // removeRange(range) when it is supported
      window.getSelection().removeRange(range);
    })
  }
}

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");

let liveSocket = new LiveSocket(
  "/live",
  Socket,
  {
    params: {
      _csrf_token: csrfToken
    },
    hooks: Hooks
  }
);

liveSocket.connect()

The Live View Template

<div id="todo-list-container">
  <!-- I use the inline `style="list-style: none"` to remove the list bullets when copy pasting to Gmail in the browser. -->
  <ul class="todo-list" style="list-style: none">
    <li>Todo 1</>
    <li>Todo 2</>
  </ul>

  <button class="button button-outline copy-to-clipboard" phx-hook="CopyToClipboard">Copy to Clipboard</button>

</div>

How it looks

Site

Gmail

Screenshot from 2020-04-21 01-02-26

Feedback

I welcome any feedback to improve how I have implemented the Copy to Clipboard button.

I’m still very new to LV but if you took the entire click handler out of the LV mount function, would it still work without needing the LV hook all together? The part I’m curious about is, what’s happening server side with this – it looks nothing would happen.

1 Like

We are in the same boat so :wink:

To be honest I don’t have thought of doing that…

You are right, nothing will happen server side, but the page for the Todo list is rendered by LiveView.

Thanks for your feedback :slight_smile:

There are reasons for using hooks to initialize js:

2 Likes

I wouldn’t go for LiveView for copy to clipboard scenario.

I’m using the following implementation in one of my projects:

<textarea readonly hidden>#{link}</textarea>
<span style="cursor:pointer;" onclick="copy(this)">copy link to clipboard</span>

<script>
function copy(button) {
  const element = button.previousElementSibling;
  element.hidden = false;
  element.value = element.value.trim();
  element.select();
  document.execCommand("copy");
  element.blur();
  element.hidden = true;
}
</script>
2 Likes

I have not tested your implementation, but yesterday I spent 3 hours trying different approaches likes yours, aka all based in textarea, and they don’t achieve what I want, that is to copy the rendered html as it is to a Gmail email, and textarea approaches only copy the printable html or the text on it.

Just to be clear LiveView was used only because the Todo List itself is rendered by LiveView, not because I thought it was the best approach to do the copy to clipboard button :wink:

So when attaching click handlers and other things that depend on the dom existing and being ready, it should always be done like this and in the end there’s no round trip to the server or double binds?

With Turbolinks, there were custom events to act upon such as:

document.addEventListener("turbolinks:load", function() {
  // Very similar to the dom ready event without turbolinks.
})

There’s like 10 other events too.

LV has phx:page-loading-stop but I don’t know how it works exactly, or if that’s the Turbolinks equiv of load. I just know we use it to render the progress bar.

So turbolinks only needs to deal with page changes. Liveview might also need to deal with parts of the dom coming and going without a full page change. Hooks are imo the simplest solution, because they’re automatically called each time a certain dom node is mounted or updated. No need to reinvent other tracking for it.

What would happen in a case where you wanted to attach a click handler to a global nav’s link, so you can use client side JS to toggle a drop down menu? Would it end up re-binding that event on every page transition since it’s on every page?

That’s exactly the reason why for all dom nodes managed by liveview I’d opt for phoenix hooks – because you don’t need to care about exactly those details. Liveview will call the hooks’ callbacks whenever needed. Everything not managed by liveview should not be touched by liveview over the course of a pages lifecycle, so you can treat them like before in js – attach the handlers after dom is ready.

1 Like

In this case the drop down menu is not managed by live view, since the LV docs mention using it for menus is a bad idea. Would you still use a hook then? Likewise, that would apply back to this clipboard example too.

We use a pure CSS menu with liveview, works great. I highly recommend Bulma, it’s a pure CSS framework that removes a lot of the JS interop issues you get when trying to use Bootstrap + LiveView.

1 Like

I’m using tailwind here and the JS bits typically depends on hiding or showing specific classes based on if a specific element is clicked. Example if #drop-down is clicked, then make sure the drop down menu is not hidden, and then on window click or escape, hide it.

This requires wiring up some custom JS event handlers.

If your dom nodes are outside the dom managed by liveview you can handle it however you like. Liveview won’t touch it.

Right, but that would mean not using a LV hook and then you run into dom-ready type issues, so in the end you would still use hooks here?

Similar to @webuhu, an alternative would be using Clipboard.writeText()

As an example:

<script>
function copy(item) {
  const element = item.nextElementSibling;
  navigator.clipboard.writeText(element.innerHTML)
}
</script>

Once you select an item/element/node you just get what you need (in this case the innerHTML text) and that’s it.

You can also do it by using Phoenix.LiveView.JS (phoenix_live_view >= v0.17)

The code is from official LiveView guide

<button phx-click={JS.dispatch("my_app:clipcopy", to: "#element-with-text-to-copy")}>
  Copy content
</button>

window.addEventListener("my_app:clipcopy", (event) => {
  if ("clipboard" in navigator) {
    const text = event.target.textContent;
    navigator.clipboard.writeText(text);
  } else {
    alert("Sorry, your browser does not support clipboard copy.");
  }
});
2 Likes