open-wc / lit-demos

Examples for using the lit-html library and LitElement base class
https://open-wc-lit-demos.stackblitz.io/basic
99 stars 19 forks source link

Drag and drop | draggable=true demo #4

Open jdu opened 5 years ago

jdu commented 5 years ago

Would be nice to see an example for this as I'm struggling pretty badly trying to get drag and drop to work in LitElement at the moment. I can get it working with Vanilla js easily, but LitElement is doing my head in trying to figure it out without having to import the whole Polymer Gesture library.

thepassle commented 5 years ago

<div draggable="true"></div> should work in LitElement, do you have a more specific example/repro I could take a look at?

Additionally: <div draggable=${true}></div> <- works <div draggable=${'true'}></div> <- works <div ?draggable=${true}></div> <- does not work

Apparently the draggable attribute is not an actual boolean attribute, but an enumerated attribute.

More info here: https://github.com/Polymer/lit-element/issues/565

jdu commented 5 years ago

Neither of the elements in the below become draggable either through setting the draggable attribute through connectedCallback or directly on the custom element. I have drag and drop peppered through the apps I work on in plain vanilla JS and React but for some reason my brain isn't wrapping around using LitElement custom elements...

import {html, css, LitElement} from "lit-element";

class DragTestInElement extends LitElement {
    static get styles() {
        return css`
            div {
                display: block;
                width: 120px;
                height: 40px;
                background: green;
            }
        `;
    }

    connectedCallback() {
        super.connectedCallback();
        this.setAttribute("draggable", true);
    }

    render() {
        return html`
            <div>DragInElement</div>
        `;
    }
}
window.customElements.define("drag-test-in", DragTestInElement);

class DragTestOnElement extends LitElement {
    static get styles() {
        return css`
            div {
                display: block;
                width: 120px;
                height: 40px;
                background: purple;
            }
        `;
    }
    render() {
        return html`
            <div>DragOnElement</div>
        `;
    }
}
window.customElements.define("drag-test-on", DragTestOnElement);

class DragArea extends LitElement {
    render() {
        return html`
            <div class="drag-area">
                <drag-test-on draggable="true"></drag-test-on>
                <drag-test-in></drag-test-in>

            </div>
        `;
    }
}
window.customElements.define("drag-area", DragArea);

let el = document.getElementById("application");
el.innerHTML = "";
let root = document.createElement("drag-area");
el.appendChild(root);
jdu commented 5 years ago

I've also tried a number of different naming variations on the ondragstart event, appending a plain vanilla DOM node with draggables and el.ondragstart = function() {...} works fine in the same example. As well I overrode createRenderRoot within the DragArea component to remove the shadow dom and render DragArea as a plain element tree in case it was something to do with nested Shadow DOMS and even then the drag-test-in and drag-test-on elements won't drag or fire the ondragstart event under those circumstance. Maybe a bug in LitElement/LitHTML not propagating the event or draggable property?

jdu commented 5 years ago

Right I've figured this out after poking and prodding it for a good hour or so.

class DragTestOnElement extends LitElement {
    static get styles() {
        return css`
            :host {
                width: 120px;
                height: 40px;
                background: purple;
                border: 1px solid black;
                display: block;
            }
        `;
    }
    render() {
        return html`
            <div></div>
        `;
    }
}
window.customElements.define("drag-test-on", DragTestOnElement);

class DragArea extends LitElement {
    static get styles() {
        return css`
            .drag-test {
                display: block;
                width: 40px;
                height: 40px;
                background: pink;
                border: 1px solid black;
            }
        `;
    }

    hitTest(e) {
        e.dataTransfer.setData("e", "TEST");
        console.log(e);
    }

    render() {
        return html`
            <style>
                .drag-test {
                    display: block;
                    width: 40px;
                    height: 40px;
                    background: pink;
                    border: 1px solid black;
                }
            </style>
            <div class="drag-area">
                <drag-test-on
                draggable="true"
                @dragstart=${this.hitTest}></drag-test-on>
            </div>
        `;
    }
}
window.customElements.define("drag-area", DragArea);

let el = document.getElementById("application");
let root = document.createElement("drag-area");
el.appendChild(root);

So there was a couple things I was doing wrong, that I'll document here in case others come across this, these may have been clear for someone working in LitElement but coming from Vanilla and React I didn't get it right away:

1) The component that is going to be dragged MUST have :host styles, for instance display: block; width: 40px; height: 40px; even if the inner element has styling and size. @click work on the component without styling the :host node but @dragstart won't for some reason. 2) Use the @ prefixed event markup in lit-html, unprefixed ondragstart, onDragStart, dragStart don't seem to work

I think this is still worth setting up an example of drag and drop as it's a fairly common use case for more complex events than click, hover, etc... and it's clearly been a pain point for me (a moderately experienced developer) to see what the issue in my implementation was.

thepassle commented 5 years ago

Sorry for the delay in getting back to you

I think this could be good to add to the demos under the Advanced section, and additionally it might be good as a write up for the faq like this one: https://open-wc.org/faq/rerender.html as well

Would you be willing to make a PR to add a demo and add the writeup to the faq?

ernsheong commented 5 years ago

See https://github.com/Polymer/lit-html/issues/460

Gist: You need to have the drag events mutate the underlying data as you move stuff around. You cannot ever allow the DOM to change due to your drop/move, every DOM change has to be driven from the model.

I'll share some code:

      case "start":
        this._draggedId = itemId;
        e.target.closest(".option-item").classList.add("drag-item");

        break;
      case "enter": {
        if (this._draggedId == null) return; // ignore drag from other places
        e.target.closest(".option-item").classList.add("drag-hover");

        this._moveItem(this._draggedId, itemId);

        // See https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations#droptargets
        e.preventDefault();
        break;
      }
      case "over": {
        // See https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations#droptargets
        e.preventDefault();
        break;
      }
      case "leave":
        if (this._draggedId == null) return; // ignore drag from other places
        e.target.closest(".option-item").classList.remove("drag-hover");

        break;
      case "drop": {
        if (this._draggedId == null) return; // ignore drag from other places
        e.target.closest(".option-item").classList.remove("drag-hover");
        e.target.closest(".option-item").classList.remove("drag-item");

        this._moveItem(this._draggedId, itemId, true);
        break;
      }
      case "end":
        if (this._draggedId == null) return; // ignore drag from other places
        e.target.closest(".option-item").classList.remove("drag-item");

        this._draggedId = null;
        break;

Some DOM:

    this._model.map(
          (m, idx) => html`
            <div
              @dragstart="${e => this.dndEvent("start", m.id, e)}"
              @dragenter="${e => this.dndEvent("enter", m.id, e)}"
              @dragover="${e => this.dndEvent("over", m.id, e)}"
              @dragend="${e => this.dndEvent("end", m.id, e)}"
              @dragleave="${e => this.dndEvent("leave", m.id, e)}"
              @drop="${e => this.dndEvent("drop", m.id, e)}"
              draggable="true"
            >Item ${m.id}</div>`;

Here, _moveItem is just mutating the underlying list.

Works well for dragging stuff within a list... still figuring out how to drag and drop between different lists, etc. but the principle is the same: data-driven DOM changes

caliny97 commented 5 years ago

@jdu- Thanks, your pointers saved my day. I wanted to contribute some other details to get dnd working. The dropzone element needs to have handlers for @drop & @dragover, make sure to call preventDefault() in the handler for dragover otherwise the drop won't fire.

rayanaradha commented 3 years ago

Look into this complete sample draggable web-component project which was built using LitElement with JavaScript. I implement this with help of @jdu code. I hope this project will help you on this problem.

https://github.com/rayanaradha/Draggable-web-component

This project contains 3 classes.

DragTestOnElement - Component for Draggable item. DragContainer - Component for Item Container. DragArea - UI In this project Draggable Items can be dragged and dropped between Item Containers.