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
- event.preventDefault() is a Must: Failing to use t-on-dragover event.preventDefault() means that the browser will simply not trigger any drop events.
- 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.
- 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.