Enable Dark Mode!
how-to-implement-drag-and-drop-in-owl-components.jpg
By: Abhijith CK

How to Implement Drag-and-Drop in OWL Components

Technical Odoo 19 Odoo Community Odoo Enterprises

Drag-and-drop interfaces, such as Kanban boards, reorderable list view, or file upload panels, are essential components for any contemporary web application. In Odoo, although there is a built-in Kanban view from the backend, custom drag-and-drop boards within your own dashboard or planning widgets, or custom POS interfaces have to be coded.

In this blog, we'll demonstrate how to use the browser-native HTML5 Drag and Drop API within an OWL 3.0 widget. We'll create a simple task planner with tasks that are draggable within "To Do" and "Done" columns.

Step 1: The JavaScript Component Controller

It is time to design the OWL 3.0 Component class, which handles the reactive list of task cards and performs database updates via the ORM framework in case of dropping a card into a new stage.

// custom_planner/static/src/js/task_planner.js
/** @odoo-module **/
import { Component, useState } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
export class TaskPlanner extends Component {
    static template = "custom_planner.TaskPlanner";
    setup() {
        this.orm = useService("orm");
        
        // Define columns/stages
        this.columns = ["todo", "done"];
        
        // Define local reactive state
        this.state = useState({
            tasks: [
                { id: 1, name: "Draft Invoice Layout", stage: "todo" },
                { id: 2, name: "Configure Headless Chrome", stage: "todo" },
                { id: 3, name: "Optimize Database Indexes", stage: "done" }
            ],
            activeDragCardId: null,
            dragOverColumn: null
        });
    }
    // Fired on the card being dragged
    onDragStart(event, taskId) {
        this.state.activeDragCardId = taskId;
        // Store the task ID in the browser dataTransfer payload
        event.dataTransfer.setData("text/plain", taskId.toString());
        event.dataTransfer.effectAllowed = "move";
    }
    // Fired when entering a column target area
    onDragEnter(event, column) {
        event.preventDefault();
        this.state.dragOverColumn = column;
    }
    // Fired when hovering over the column
    onDragOver(event) {
        // Essential: Allow dropping on this element
        event.preventDefault();
    }
    // Fired when leaving a column area
    onDragLeave(event) {
        this.state.dragOverColumn = null;
    }
    // Fired when the card is dropped on a column
    async onDrop(event, targetColumn) {
        event.preventDefault();
        this.state.dragOverColumn = null;
        // Retrieve the task ID from the dataTransfer payload
        const taskId = parseInt(event.dataTransfer.getData("text/plain"));
        
        // Find and update the task stage in the local state
        const task = this.state.tasks.find(t => t.id === taskId);
        if (task && task.stage !== targetColumn) {
            const previousStage = task.stage;
            task.stage = targetColumn; // Updates the UI instantly due to OWL reactivity
            // Synchronize with the Odoo backend
            try {
                await this.orm.write("project.task", [taskId], {
                    stage_id: targetColumn === "done" ? 3 : 1 // Example stage mapping
                });
            } catch (error) {
                console.error("Failed to update task stage on server, reverting state:", error);
                task.stage = previousStage; // Rollback state if server write fails
            }
        }
    }
}

Step 2: The XML QWeb Template

It is time to design our template. We iterate over columns, bind drag-and-drop actions, and append a highlight class dynamically upon hovering over a column.

<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
    <t t-name="custom_planner.TaskPlanner" xml:space="preserve">
        <div class="d-flex gap-4 p-4 bg-light w-100 h-100 align-items-stretch" style="min-height: 400px;">
            
            <!-- Loop over Columns -->
            <t t-foreach="columns" t-as="col" t-key="col">
                <div class="card flex-grow-1 p-3 border-2 transition-all"
                     t-att-class="state.dragOverColumn === col ? 'border-primary shadow-lg bg-white' : 'border-secondary'"
                     t-on-dragenter="(e) => this.onDragEnter(e, col)"
                     t-on-dragover="this.onDragOver"
                     t-on-dragleave="this.onDragLeave"
                     t-on-drop="(e) => this.onDrop(e, col)">
                    
                    <h3 class="text-capitalize fw-bold text-dark border-bottom pb-2 mb-3">
                        <t t-esc="col"/> Column
                    </h3>
                    <!-- Render Draggable Cards inside this Column -->
                    <div class="d-flex flex-column gap-2">
                        <t t-foreach="state.tasks.filter(t => t.stage === col)" t-as="task" t-key="task.id">
                            <div class="p-3 bg-white border rounded shadow-sm cursor-grab"
                                 style="cursor: grab;"
                                 draggable="true"
                                 t-on-dragstart="(e) => this.onDragStart(e, task.id)">
                                <t t-esc="task.name"/>
                            </div>
                        </t>
                    </div>
                </div>
            </t>
        </div>
    </t>
</templates>

Key Takeaways & Best Practices

  1. event.preventDefault() is a Must: Failing to use t-on-dragover event.preventDefault() means that the browser will simply not trigger any drop events.
  2. Graceful Handling of Server Issues: Whenever drag and drop affects database states, ensure that there is try-catch on the RPC write operation. In case of failure of the write due to validation rules or database locks, catch the error and roll back the state to make sure that it remains in sync with the server.
  3. Make the Visual Feedback Responsive: Setting the reactive state in the dragenter and dragleave events works just as well because OWL takes care of fine-grained DOM patching. Don’t change the data binding during drag operations to ensure smooth feedback.

Using the HTML5 Drag-and-Drop API, which is built into native HTML5, in the OWL 3.0 component results in a light and very flexible way of implementing interactive interfaces in Odoo. This means you get the full power of drag-and-drop implementation offered by browsers without having to rely on some external JavaScript library. Also, you get all the advantages of OWL’s reactive state management.

In our case, we implemented a very simple task planner, which lets users move task cards from the To Do Done column. This is done instantaneously using OWL reactivity and then synchronized to the backend using the ORM service. This way, you will get a drag-and-drop experience, which is both interactive and at the same time stays consistent with the database.

Such an approach can be applied in many practical cases in Odoo, such as:

  • Custom Kanban boards
  • Warehouse picking planners
  • Production scheduling interfaces
  • POS order organizers
  • Dashboard widgets with reorderable cards
  • File upload drop zones

In light of this decoupling between interactions on the UI side and persistence of the ORM, drag-and-drop can be used effectively for creating innovative Odoo apps.

To read more about Overview of Advanced OWL Components In Odoo 19, refer to our blog Overview of Advanced OWL Components In Odoo 19.


Frequently Asked Questions

Why use the native HTML5 Drag and Drop API instead of a third-party library?Why use the native HTML5 Drag and Drop API instead of a third-party library?

The native API is part of the browser; it does not need any further dependencies and fits nicely with OWL components. In many scenarios that Odoo provides, the native drag-and-drop API will do the job just fine.

Why is event.preventDefault() required in onDragOver?

Drop actions are blocked by default. event.preventDefault() should be called in dragover to notify the browser that the element can accept drops, enabling the drop event.

How does OWL update the UI automatically when a task is moved?

The tasks list is kept in a reactive state via useState(). Whenever the stage value of a task changes, OWL will notice the change and update only the relevant DOM nodes.

If you need any assistance in odoo, we are online, please chat with us.



0
Comments



Leave a comment



WhatsApp