--- title: FastHTML and SortableJS For Sortable Todo Lists permalink: /futureproof/fasthtml-sortablejs-todo/ description: In my quest to build the perfect to-do list app with FastHTML, I tackled drag-and-drop sorting with SortableJS. After overcoming a pesky script tag issue and submitting a bug report, I dove into optimizing database updates with MiniDataAPI. By creating a batch update function and minimizing data transfer, I ensured efficient sorting. Finally, I set up a server-side endpoint to handle the updates and seamlessly integrate the front-end and back-end, resulting in a smooth and responsive user experience. This article explores how to integrate SortableJS with FastHTML for creating sortable to-do lists. It covers client-side drag-and-drop functionality, efficient backend updates using MiniDataAPI, and setting up JavaScript and Python endpoints for real-time updates. meta_description: Implement drag-and-drop sortable lists using FastHTML and SortableJS. Integrates client-side JavaScript with a Python backend endpoint for persistent MiniDataAPI updates. meta_keywords: FastHTML, SortableJS, drag and drop, sortable list, todo list, reorder list, JavaScript, Python, MiniDataAPI, HTMX, backend endpoint, client-side sorting, database update, AJAX sort, script module, batch update, persistent order layout: post sort_order: 1 --- ## Refining the Web Development Experience with Drag-and-Drop Functionality Well, the port from yesterday hasn't technically occurred yet, however the framework is so much cleaner for "receiving" the port. I've made nice Application placeholders in navigation and with blank pages. Conceptually, it's easy to see now how I plug in new apps. And I've ton a ton of refinements on the UI to make it solid and sexy. It's really amazing what the snappiness of HTMX allows, and it's time to do drag-and-drop to get rid of "yeah-but's". I'm not a big fan of webdev, but when you do it you've got to be pretty buttoned up to modern expectations, or you're crap. Nobody wants to deal with yet another crappy interface with so many sexy new bells and whistles in tech all the time. And I think if I leave drag-and-drop sorting out of this, I'm setting users up to be disappointed given how such lists permeate everything. ## Exploring FastHTML Advanced App Walk-Through for Modern Web Development For this, we go back to the [FastHTML Advanced App Walk-through](https://www.youtube.com/watch?v=kfEpk6njb4s) **Describe the bug** SortableJS() requires presence of Script(type='module') **Minimal Reproducible Example** External scripts can be included through the `fast_html()` wrapper, like so: ```python {% raw %} app, rt = fast_app( hdrs=(SortableJS()) ) {% endraw %} ``` ...but this results in: ```html {% raw %} Botifython / Another / Home import {Sortable} from 'https://cdn.jsdelivr.net/npm/sortablejs/+esm'; proc_htmx('.sortable', Sortable.create); {% endraw %} ``` ## JavaScript Import Error Fixed by Module Tag Addition ...which if you look close you will see that the JavaScript import is not wrapped in a ` {% endraw %} ``` It's a hack. **Expected behavior** ```python {% raw %} app, rt = fast_app( hdrs=(SortableJS()) ) {% endraw %} ``` ...should result in: ```html {% raw %} {% endraw %} ``` **Environment Information** - fasthtml version: 0.6.9 **Confirmation** Please confirm the following: - [✅] I have read the FAQ (https://docs.fastht.ml/explains/faq.html) - [👨‍🎨] I have provided a minimal reproducible example - [💯] I have included the versions of fastlite, fastcore, and fasthtml - [🔥] I understand that this is a volunteer open source project with no commercial support. --- Okay, so I [submitted that on GitHub](https://github.com/AnswerDotAI/fasthtml/issues/510). I'm glad I found a fix. Keep in mind you can always look at the code directly. [How JavaScript is included in FastHTML](https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/js.py) ## Todo List Stage Set for Drag-Sortable Priorities I have as you can see set the stage for a drag-sortable Todo list. The priority int field is already even in the table. When using SortableJS with MiniDataAPI, updating multiple records' sortorder in a single operation isn't directly supported. However, we can implement an efficient solution that minimizes database calls and maintains data integrity. Here's a recommended approach: ## Batch Update Function Create a custom function to handle batch updates: ```python {% raw %} def batch_update_sortorder(table, id_order_pairs): with table.db.transaction(): # Assuming transaction support for item_id, new_order in id_order_pairs: table.update(id=item_id, sortorder=new_order) {% endraw %} ``` This function: 1. Uses a transaction to ensure all updates are atomic. 2. Iterates through the new order, updating each item individually. ## Client-Side Optimization To reduce the number of updates: 1. **Track only changed positions**: On the client-side, compare the old and new positions of items after a drag operation. 2. **Send minimal data**: Only send the IDs and new positions of items that actually changed. ```javascript {% raw %} sorter.on('sort', function(evt) { let changes = []; sorter.el.children.forEach((item, index) => { if (parseInt(item.dataset.sortorder) !== index) { changes.push({id: item.dataset.id, sortorder: index}); } }); if (changes.length > 0) { sendToServer(changes); } }); {% endraw %} ``` ## Server-Side Implementation On the server, process the changes: ```python {% raw %} @app.route('/update_order', methods=['POST']) def update_order(): changes = request.json['changes'] batch_update_sortorder(todos, [(change['id'], change['sortorder']) for change in changes]) return jsonify(success=True) {% endraw %} ``` ## Optimizations 1. **Incremental Updates**: For large lists, update only the range of affected items: ```python {% raw %} def incremental_update(table, moved_item_id, old_position, new_position): with table.db.transaction(): if new_position < old_position: # Moving up items = table(f"sortorder >= {new_position} AND sortorder < {old_position}", order_by='sortorder') for i, item in enumerate(items, start=new_position): table.update(id=item.id, sortorder=i) else: # Moving down items = table(f"sortorder > {old_position} AND sortorder <= {new_position}", order_by='sortorder') for i, item in enumerate(items, start=old_position+1): table.update(id=item.id, sortorder=i-1) table.update(id=moved_item_id, sortorder=new_position) {% endraw %} ``` {:start="2"} 2. **Periodic Reordering**: Occasionally reorder all items to prevent gaps: ```python {% raw %} def reorder_all(table): items = table(order_by='sortorder') with table.db.transaction(): for i, item in enumerate(items): table.update(id=item.id, sortorder=i) {% endraw %} ``` By implementing these strategies, you can efficiently handle SortableJS reordering with MiniDataAPI, minimizing database operations while maintaining data consistency. This approach balances the limitations of MiniDataAPI with the need for efficient updates in a drag-and-drop interface. --- ## Drag-and-Drop Event Already Underway With Scriptable Tag Enabled **Me**: The drag-and-drop event is already occurring. I already have the scriptable tag in place: ```html {% raw %} Botifython / First Profile3 / Todo