Drag & Drop in Phoenix LiveView? (+Custom messages?)

Has anyone looked into implementing drag & drop functionality with LiveView? It does not seem to be very obvious how it should be done.

It seems we may need phoenix_live_view.js to implement phx-dragstart, phx-dragover and phx-drop? Is something like this planned or might I have the wrong idea here?

Also, I wonder if it is or should be possible to fire a custom event from a line of JavaScript. It would seem that this would allow me to pretty easily implement drag & drop, albeit in a round-about, “non-native” way. It’s probably an awful idea for some reason, but perhaps something like:

<img id="thingy-1" src="foo.jpg" draggable="true" ondragstart='liveSocket.somePushEventMethod("startdrag", "1")'>

So much thanks to Chris, José, and this amazing community <3

3 Likes

phx-drop or similar – possibly. I plan to explore it at some point, but there are a lot of other things that will get higher priority. For the js interop, we will provide hooks in the near term to fire your own events or receive plain data alongside the LV. That latter is easy, it’s just a matter of finding a common and extensible API. Drag and drop specifically is harder because the ordering will necessarily require the server to render a collection of items. For example, if you simply dropped a few phx-drag* bindings on some static tags, it would be undone on the next render, so something like this would need to be done in a comprehension, with the order synced properly on based on the client events, so it’s a bigger undertaking to try to implement properly. JS interop for things like sending a sideline of chart data from the LV and having some charting JS hook into that and update a rendered graph – that will be easy and not far off.

5 Likes

Brilliant. Thank you so much for that blazing-quick reply, Chris.

I think that when the custom events stuff drops as you mention, I should be able to implement what I need by handling the dragging part purely in the client with my own, custom JS, then only firing the LV event on drop. I bet that will get me where I need to go without too much ugliness.

(My fallback plan is to implement an entirely custom AJAX call to tell the server what was dropped where, and from there I can push a LV update. May still investigate this soon.)

1 Like

You can use JS Interop.

Given this html

<div id="draggable" draggable="true" style="width:100px; height:100px; background-color:gold;">Draggable</div>
<div id="drop_zone"  style="width:300px; height:300px; background-color:pink;"> Drop Zone</div>

here is a simple example:

  1. Set the element draggable and set the phx-hook also it’s probably a good idea to give it an id .
 <div id="draggable" phx-hook="draggable_hook" draggable="true" >Draggable</div>
  1. Give the drop zone a hook.
<div phx-hook="drop_zone">Drop Zone</div>
  1. In apps.js add a draggable_hook object. In its mounted method add your dragstart event handler.
let Hooks = {}
Hooks.draggable_hook = {
    mounted() {
      this.el.addEventListener("dragstart", e => {
        e.dataTransfer.dropEffect = "move";
        e.dataTransfer.setData("text/plain", e.target.id); // save the elements id as a payload
      })
    }
  }
  1. Add your drop_zone hook and register event handlers for dragover and drop.
    mounted() {
   
      this.el.addEventListener("dragover", e => {
        e.preventDefault();
        e.dataTransfer.dropEffect = "move";
      })
   
      this.el.addEventListener("drop", e => {
        e.preventDefault();
        var data = e.dataTransfer.getData("text/plain");
        this.el.appendChild(e.view.document.getElementById(data));
      })
    }
  }

For more details on Drag and Drop events go to Modzilla’s docs

  1. Finally register your hooks with live view.
let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks})

here is the example live view:

#lib/example_web/live/dragndrop_live.ex

defmodule ExampleWeb.DragndropLive do
  use Phoenix.LiveView

  require Logger

  def mount(_session, socket) do
    Logger.info("MOUNT #{inspect(self())}")
    {:ok, socket}
  end

  def render(assigns) do
    Logger.info("RENDER #{inspect(self())}")
    ~L"""
    <div id="draggable" phx-hook="draggable_hook" draggable="true" style="width:100px; height:100px; background-color:gold;">Draggable</div>
    <div phx-hook="drop_zone"  style="width:300px; height:300px; background-color:pink;"> Drop Zone</div>
    """
  end

  def update(assigns, socket) do
    {:ok, socket}
  end

end
// assets/js/app.js

import css from "../css/app.css"
import "phoenix_html"
import {Socket} from "phoenix"
import socket from "./socket"


 import LiveSocket from "phoenix_live_view"
.
.let Hooks = {}

Hooks.draggable_hook = {
    mounted() {
      this.el.addEventListener("dragstart", e => {
        e.dataTransfer.dropEffect = "move";
        e.dataTransfer.setData("text/plain", e.target.id); // save the elements id as a payload
      })
    }
  }


Hooks.drop_zone = {
    mounted() {
   
      this.el.addEventListener("dragover", e => {
        e.preventDefault();
        e.dataTransfer.dropEffect = "move";
      })
   
      this.el.addEventListener("drop", e => {
        e.preventDefault();
        var data = e.dataTransfer.getData("text/plain");
        this.el.appendChild(e.view.document.getElementById(data));
      })
    }
  }
let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks})

liveSocket.connect()
18 Likes

I’d like to do this to simply reorder a list, in an app that has about half mvc views and half LiveViews.

I think I’m going to use this alpine.js approach because:

  • I already added alpine to my project to get a tailwind css datepicker widget
  • the code in the above link is all declarative, with (almost) no javascript.
3 Likes

How do you handle it on the phoenix side, would you be able to share?

I have not gotten to it – I’m working on a different app now. But I will, eventually.

Thinking about it now:

  1. create a JS hook (see above) that pushes an event to LiveView server
  2. modify the code in the alpine example (see my link above) to call the hook

OR

  1. Don’t call the hook every time an item is dropped; instead, do all the reordering, and have a “Save” button to explicitly call the hook and save the new orders of all items. I’d probably go with this, and store the child item orders as a Map field in the parent collection.

Now, I realize this will require writing some JS to create a JSON map to push in the event hook. :frowning:

1 Like

Found this, https://www.headway.io/blog/client-side-drag-and-drop-with-phoenix-liveview

1 Like