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.