This is the third part of our ongoing tutorial series about the Odoo 14 JavaScript Framework. In the second part, we created our new type of View in OWL that displayed a hierarchical vision of parent/child type Models. The nodes are clickable and can open the child elements.

This where we left of in the second part of the tutorial

In this follow-up, we will add the drag and drop feature that will allow us to move the items and change their hierarchy.

There are some rules though:

  • A parent item shouldn't be dragged and dropped into one of its own children.
  • Moving an item should recalculate the count of the old parent and the new target parent should be refresh.
  • An item cannot be dropped into itself.

To handle that we need to first take a look at the HTML Drag and Drop API

The HTML Drag And Drop API

In this section, I will briefly explain the HTML Drag and Drop API but if you would like a more in-depth view, take a look at this MDN Documentation.

The first step to make a DOM element draggable is adding the draggable attribute to the element and set it to true:

<div id="draggableDiv" draggable="true">This element can be dragged.</div>

The DragEvent interface

This element will then already be draggable in the browser. On top of that, it will fire multiple events. We will not cover all the events but only the ones we are interested in:

  • dragstart event when the dragging begins. After a prolonged click on the draggable element, it "pops out" and can be dragged around.
  • dragenter and dragleave events are events that happen on the element being dragged onto. You can know when the dragged element is entering or leaving a zone represented by another element, and make that zone react accordingly.
  • dragover events trigger every few hundred milliseconds when an element is being dragged over. An element that is a valid drop target should handle that event.
  • drop event when the item is dropped (mouse released). If an element is a valid drop target it should handle that event.

Handling event means that we will have to ev.preventDefault() the events coming into our handler function. Fortunately for us, OWL provides us with shortcuts for that.

The DataTransfer interface

The DataTransfer is an object holding data during the drag and drop events, it can hold multiple items.

We will use this DataTransfer to pass around the tree item being drag and dropped. To do so, the interface gives u access to 2 functions:

DataTransfer.setData()Set the data for a given type. If data for the type does not exist, it is added at the end, such that the last item in the types list will be the new format. If data for the type already exists, the existing data is replaced in the same position.

event.dataTransfer.setData("text/plain", "hello world");

DataTransfer.getData()Retrieves the data for a given type, or an empty string if data for that type does not exist or the data transfer contains no data.

let result = event.dataTransfer.getData("text/plain");
// result: "hello world"

Let's dive into our TreeItem Component now to make everything clearer.

Making our TreeItem draggable

Now that we know how everything works, we will update our TreeItem Component. The draggable element will be the whole <div> (without the children) and it will also be the drop zone.

Let's update static/src/components/tree_item/TreeItem.xml to reflect that:

<t t-name="owl_tutorial_views.TreeItem" owl="1">
    <div class="tree-item-wrapper">
        <div 
            draggable="true" 
            t-on-dragstart="onDragstart" 
            t-on-drop.stop.prevent="onDrop" 
            t-on-dragover.prevent="onDragover" 
            t-on-dragenter.prevent="onDragenter" 
            t-on-dragleave.prevent="onDragleave" 
            t-attf-class="list-group-item list-group-item-action d-flex justify-content-between align-items-center owl-tree-item {{ state.isDraggedOn ? 'list-group-item-warning': '' }}"
        >
            <a href="#" t-on-click.stop.prevent="toggleChildren" t-if="props.item.child_id.length > 0">
                <t t-esc="props.item.display_name"/>
                <i t-attf-class="pl-2 fa {{ state.childrenVisible ? 'fa-caret-down': 'fa-caret-right'}}" ></i>
            </a>
            <span t-else="">
                <t t-esc="props.item.display_name"/>
            </span>
            <span 
                t-if="props.countField !== '' and props.item.hasOwnProperty(props.countField)" 
                class="badge badge-primary badge-pill" 
                t-esc="props.item[props.countField]">
            </span>
        </div>
        <t t-if="props.item.child_id.length > 0">
            <div class="d-flex pl-4 py-1 flex-row treeview" t-if="props.item.children and props.item.children.length > 0 and state.childrenVisible">
                <div class="list-group">
                    <t t-foreach="props.item.children" t-as="child_item">
                        <TreeItem item="child_item"/>
                    </t>
                </div>
            </div>
        </t>
    </div>
</t>

We can directly use .prevent shortcuts to not have to write event.preventDefault() inside the handler function.

Notice that we also added a CSS class that triggers by checking the state of the TreeItem here {{ state.isDraggedOn ? 'list-group-item-warning': '' }}.

This will make the item become yellow-ish when dragged over. Let's now check each function we have to implement.

Handling the dragstart event

The dragstart represents the first step of the whole drag and drop operation. This is where we will set our TreeItem Object as the data to be transferred during the drag and drop.

We want to transfer the full object but the DataTransfer API setData only let us transfer strings so we will have to JSON.stringify our object first. So inside our src/components/tree_item/TreeItem.js:

onDragstart(event) {
	event.dataTransfer.setData("TreeItem", JSON.stringify(this.props.item));
}
We transfer a custom format "TreeItem"

As you can see, we can transfer custom formats of data, ie "TreeItem" in our example. With that done, we will be able to get the item being moved at the end of the chain of operations.

Handling dragenter and dragleave to give visual feedback to the user.

These two events only trigger once and they will toggle the list-group-item-warning CSS class to give the user visual feedback on the component he is dragging over.

onDragenter() {
  Object.assign(this.state, { isDraggedOn: true });
}

onDragleave() {
  Object.assign(this.state, { isDraggedOn: false });
}

The code is self-explanatory on this one, let's move on to the main event.

Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to Coding Dodo - Odoo, Python & JavaScript Tutorials.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.