Working with drag and drop interfaces is an integral part of UI management in Odoo 19. This version of Odoo provides good support for sorting operations in the web client. You can reorder a number of records in the system or move elements across the screen to use them easily within custom views.
This blog provides a detailed overview of the useSortable hook in Odoo 19, along with a practical implementation of sorting custom embedded actions. In this blog, we explain how to implement custom sorting in an OWL component, i.e., sorting embedded actions using a hook. We first create an OWL component inside the Odoo 19 environment. The hook is initialized in the setup using enable: true to activate it.
With the help of Odoo’s web core module sortable_owl, we can create a sortable environment by calling useSortable(). The reference is linked to the container using useRef(), and then the details of each draggable record are managed by the hook for each item.
In some cases, the standard list view may not be enough for your needs, and a custom sortable solution may need to be implemented to meet specific requirements. This provides the developer with more control over the DOM structure and other sorting options. The following code snippet provides an example of how a custom sortable hook for Odoo 19 can be implemented.
// Initialize the drag and drop hook
useSortable({
enable: true,
ref: this.root,
elements: ".o_draggable",
cursor: "move",
delay: 200,
tolerance: 10,
onWillStartDrag: (params) => this._sortActionStart(params),
onDrop: (params) => this._sortActionDrop(params),
});
This function enables drag and drop functionality for a container in Odoo 19.
- useSortable({ ... }): This line of code initializes a sortable object for the sortable_owl module.
- enable: true: indicates that the drag and drop feature is active.
- ref: this.root: Wraps the hook around the main container reference defined in the setup.
- elements: ".o_draggable": Only targets elements with this specific class.
- cursor: "move": Changes the mouse pointer to a move icon when dragging.
- delay: 200 — Adds a delay before dragging starts.
- tolerance: 10 — Requires slight movement before dragging begins.
The drag action gets delayed to prevent accidental clicks. There are several options available for handling accidental drags. Here are a few options to choose from:
- delay: This waits a specific amount of milliseconds before starting the drag.
- tolerance: This requires the mouse to move a certain number of pixels before dragging starts.
- handle: This restricts the drag action to a specific icon class.
We import from sortable_owl and not sortable because the OWL version automatically removes all drag listeners when the component is destroyed. If you used sortable directly, you'd have to clean up the listeners yourself on every unmount — and forgetting that causes memory leaks.
import { Component, useRef, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { useSortable } from "@web/core/utils/sortable_owl";export class SortableDemo extends Component {
setup() {
this.root = useRef("root");
// Local state to hold our mock data
this.state = useState({
actions: [
{ id: 1, name: "Review pull requests" },
{ id: 2, name: "Update Odoo 19 modules" },
{ id: 3, name: "Write documentation" },
{ id: 4, name: "Deploy to staging server" },
]
});
// Initialize the drag and drop hook
useSortable({
enable: true,
ref: this.root,
elements: ".o_draggable",
cursor: "move",
delay: 200,
tolerance: 10,
onWillStartDrag: (params) => this._sortActionStart(params),
onDrop: (params) => this._sortActionDrop(params),
});
}
_sortActionStart({ element, addClass }) {
// Highlight the element while dragging
addClass(element, "text-bg-warning");
}
_sortActionDrop({ element, previous }) {
const elementId = Number(element.dataset.id);
// 1. Get the current order of IDs
const order = this.state.actions.map((el) => el.id);
// 2. Remove the dragged element from its old position
const elementIndex = order.indexOf(elementId);
order.splice(elementIndex, 1);
// 3. Insert it into the new position
if (previous) {
const prevIndex = order.indexOf(Number(previous.dataset.id));
order.splice(prevIndex + 1, 0, elementId);
} else {
order.splice(0, 0, elementId); // Dropped at the very top
}
// 4. Update the state array to reflect the new order
this.state.actions.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));
}
}Finally, the sorting code captures the drag events in a format that provides the positional data using Odoo 19’s parameter tracking feature. Here’s how it works:
- onWillStartDrag calls _sortActionStart to add a CSS class while dragging.
- The addClass function is provided by useSortable itself as part of the callback params. It safely adds a CSS class to the element without you needing to touch classList directly.
- It then listens for the user to release the mouse button.
- It then calls _sortActionDrop to handle the final placement of the element.
So, in summary, _sortActionDrop() generates the dropped element data in a format that gets processed as an object containing the current and the previous sibling elements. This gets processed to update the sequence in the database.
The data is extracted using element.dataset.id from the dragged item. addClass(element, "text-bg-warning"). This sets the visual styling for the item currently being dragged.
Finally, we need an XML template to define the structure and link our draggable items to their record IDs:
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="use_sortable_demo.SortableDemoTemplate" owl="1">
<div class="o_action p-4 bg-view h-100 overflow-auto">
<h2 class="mb-3">useSortable Hook Testing Area</h2>
<p class="text-muted mb-4">Click and hold an item for 200ms, then drag to reorder.</p>
<div t-ref="root" class="border p-3 rounded bg-100" style="max-width: 500px;">
<t t-foreach="state.actions" t-as="action" t-key="action.id">
<div class="o_draggable d-flex align-items-center p-3 mb-2 bg-white border rounded shadow-sm"
t-att-data-id="action.id">
<span class="fa fa-arrows me-3 text-muted drag-handle" style="cursor: grab;"/>
<span t-esc="action.name" class="fw-bold"/>
</div>
</t>
</div>
</div>
</t>
</templates>
- t-ref="root": This is for passing the reference of the container to identify the specific sortable area.
- class="o_draggable": This is for specifying the class to be used for the draggable items.
- t-att-data-id="action.id": This is for specifying the field that contains the record ID.
Drag and drop operations in Odoo 19 give developers the flexibility to build dynamic interfaces that fit exactly what a business process needs. The useSortable hook handles everything from listener setup to teardown, so the developer can focus entirely on the reorder logic rather than managing the DOM. It covers a wide range of use cases — from simple list reordering to cross-container drag between Kanban columns — making it a reliable choice for any custom sorting requirement in Odoo 19.
To read more about Overview of Advanced OWL Components In Odoo 19, refer to our blog Overview of Advanced OWL Components In Odoo 19.