blog Drag and Drop and Django

Tags:
django web-components javascript

I wanted to have some drag and drop web components that I could use with my django apps. I am planning on using them in another of my many todo/scheduling prototypes. Basing heavily on the api docs for drag and drop, I start with some basic components.

I start with my <drag-source> component that I will use to wrap anything I want draggable.

class DragSource extends HTMLElement {
  constructor() {
    super();
    // I use a random ID here to make something quick/simple but in a real version
    // I should probably only set this if not already set.
    this.id = Math.random();
  }

  dragStart(e) {
    e.dataTransfer.setData("text/plain", e.target.id);
    e.dataTransfer.dropEffect = "move";
    setTimeout(() => {
      e.target.classList.add("hide");
    }, 0);
  }

  dragEnd(e) {
    e.target.classList.remove("hide");
  }

  connectedCallback() {
    this.addEventListener("dragstart", this.dragStart);
    this.addEventListener("dragend", this.dragEnd);
  }
}
customElements.define("drag-source", DragSource);

Next, I create a container that I will use to drop onto.

class DragTarget extends HTMLElement {
  constructor() {
    super();
  }

  dragOver(e) {
    e.preventDefault();
  }

  dragEnter(e) {
    e.preventDefault();
    e.target.classList.add("drag-over");
  }

  dragLeave(e) {
    e.target.classList.remove("drag-over");
  }

  drop(e) {
    e.target.classList.remove("drag-over");

    const id = e.dataTransfer.getData("text/plain");
    const draggable = document.getElementById(id);

    this.appendChild(draggable);

    e.dataTransfer.clearData();
  }

  connectedCallback() {
    this.addEventListener("dragover", this.dragOver);
    this.addEventListener("dragenter", this.dragEnter);
    this.addEventListener("dragleave", this.dragLeave);
    this.addEventListener("drop", this.drop);
  }
}
customElements.define("drag-target", DragTarget);

This lets me setup my page quite easily, with component names that communicate their purpose better. This handles the basic hookups, but is not actually submitting anything yet.

<drag-source>A</drag-source>
<drag-source>B</drag-source>
<drag-source>C</drag-source>

<drag-target><h1>Target 1</h1></drag-target>
<drag-target><h1>Target 2</h1></drag-target>

I decided I wanted to use data-* attributes for my drag-source to make it easy to populate from Django templates.

<drag-source
  data-url="{{ task.url }}"
  data-owner="{{ task.repository.owner }}"
  data-repo="{{ task.repository.name }}"
  data-issue="{{ task.number }}"
  draggable="true"
  class="card"
>
  Card 1
</drag-source>

Then I added an href element to my drag-target to configure the URL I want to POST to.

<drag-target href="{% url 'planner:schedule' day.isoformat %}"></drag-target>

I also need to add the csrf_token somewhere for Javascript to post along with requests

<script>
  const CSRF_TOKEN = "{{ csrf_token }}";
</script>

With all this in place, I can now update my drop() method to handle my request.

drop(e) {
    e.target.classList.remove("drag-over");


// When triggering the drop event, we get the element we're dropping
    const id = e.dataTransfer.getData("text/plain");
    const draggable = document.getElementById(id);

// We check that our drop-target actually has a defined href tag
    if (this.attributes.href !== undefined) {
      // We go ahead and let the visual update go through
      this.appendChild(draggable);
      // But also configure our POST request
      fetch(this.attributes.href.value, {
        method: "POST",
        // We convert the draggable element's dataset to a POST body
        body: new URLSearchParams(draggable.dataset).toString(),
        // And add in the headers that we'll need
        headers: {
          "x-csrftoken": CSRF_TOKEN,

          "Content-Type": "application/x-www-form-urlencoded",
        },
      })
        .then((result) => {
          // For now, we just reload on successful POSTs but later
          // we'l probably do something more interesting.
          location.reload();
        })
        .catch((error) => console.log(error));
    } else {
      console.log("Unable to drop on", e.target);
    }

    e.dataTransfer.clearData();
  }

There’s still more to cleanup and make more robust, but this quick prototype helped test my idea and it seems to work.