Add Sortablejs as element (draggable objects!)#4656
Add Sortablejs as element (draggable objects!)#4656EmberLightVFX wants to merge 106 commits intozauberzeug:mainfrom
Conversation
Wise move. https://github.com/SortableJS/vue.draggable.next is 4 years old. To clarify, I think your file This is because, eventually, we would need a script to fetch it automatically (check out
Almost. If you have a good bit of reasonable default for styles, they would commonly go to https://github.com/zauberzeug/nicegui/blob/main/nicegui/static/nicegui.css Typically, styles are applied to elements with class It would be great to include just the mininal style so that the sortable doesnt look like hot garbage, but not anything else on top. After all, the user may also want to apply their own styles as well.
I'm fine to take your word for it now, but eventually there should be tests. Not sure how we may "drag" stuff with ChromeDriver, though. @falkoschindler correct me if I am wrong. |
|
Updated with some missing examples and options for sortablejs. About the npm,py/json I would gladly take some help with that, or some instructions on how that part of NiceGUI works. |
|
I have now fixed the threshold example to fully work and also made it possible to drag cards into other cards in the nested example. The only thing left is the npm part and I would love some help with that as I have no idea how you would set it up correctly. |
|
Thanks for the pull request, @EmberLightVFX! Bringing SortableJS to NiceGUI would be an awesome feature! |
|
@EmberLightVFX Include the following in your "sortable": {
"package": "sortablejs",
"destination": "nicegui/elements/lib",
"keep": [
"package/modular/sortable.complete.esm.js"
],
"rename": {
"package/modular/": ""
},
"version": "1.15.6"
}Hopefully, this should download |
|
I were suppose to update my drop_zone branch but accidentally updated this one 😅 I'm not working on this one. |
falkoschindler
left a comment
There was a problem hiding this comment.
Hi @EmberLightVFX,
Thanks for the update! I just did a new code review of sortable.js and sortable.py:
To reduce the overall code length, I removed things like
- trivial comments,
- unnecessary line break,
- indentation (e.g. using early exits, or removing unnecessary try-catch blocks),
- unused methods,
- local variables that can be inlined,
- if-conditions that can be rewritten using
?.
Some more general thoughts:
- Do we really need to keep
this.optionsup to date? If not,setOptionalso collapses to basically a one-liner. - I wonder if we need to declare methods like
sort,enable,disable,getOption, andsetOptionin JS. Maybe we can simply add a genericrunSortableMethod(likerun_grid_methodin aggrid.js orrun_editor_methodin json_editor.js). - You often mix
.idand.dataset.idlikeitem: evt.item.id || evt.item.dataset.id || null. Can it really be both? I think we should decide which ID to use and stick with it. Overall the code reads very defensive (lots of try-catch and|| null). But sometimes a fallback tonullmight actually cause bugs, becausenullis not an ID and not useful for identifying anything. - The term "item" is often used to store an ID. Can we use "id" or "item_id" instead?
- The
clearmethod seems very inefficient because it sends N messages to the client. Calling a single "remove_all" method would be better. - But isn't there a bigger problem with synchronizing child elements with sortable items? What if the user programmatically deletes a child element? This would break the sortable instance in JS, wouldn't it?
- Do we really need a method like
get_child_by_id? If we yes, than it should move into element.py or client.py, because it works for any element. But I'd rather not work with HTML IDs in user code. And if we'd use the integer element ID as a key, we could directly use the dictionaryself.client.elements.
|
After thinking about this PR once again, I came to the following conclusion:
In order to keep this discussion focussed on this PR and to reach a broader audience, I posted these two open questions on the corresponding feature request: Let's pause development on this PR and continue the discussion over there until we've agreed on the desired API and library. |
|
Sounds all good for me! I made this element for a project of mine but if it should be part of NiceGUI it should follow NiceGUIs needs! If anything it could end up being an external NiceGUI addon that people could pip install. I would just need to know how to make that work with NiceGUI 😄 |
|
I can definitely see both the pros and cons of exploring other frameworks. In one of our projects, we rely heavily on both flat and nested sortables, and honestly, we couldn’t live without them. So, in the worst case, I really hope this could at least live on as a plugin or addon. From my perspective, even if SortableJS might seem a bit “stale,” it’s still extremely capable and stable. It already offers a lot of functionality out of the box and works reliably. So I’m wondering, what concrete new needs would NiceGUI have that SortableJS doesn’t already cover? I’m even tempted to make a short video showcase, because I genuinely think ui.sortable is awesome and @EmberLightVFX has put a lot of great work into it and It adds a ton of value right now. Even if a different framework were to be chosen in the future, sortable.js could be marked as end-of-life with a legacy period before being moved into a standalone plugin or addon without official support. Realistically, adopting a new drag-and-drop library, designing a new API, reviewing, testing, and polishing it would probably take at least 6–12 months. So having a solid working solution now, even if, at some point in 2026, there’s a decision to adopt a different framework that might be implemented in 2027 seems completely reasonable and pragmatic. In short, I think it’s worth keeping and using this solution for the time being. It’s functional, stable, and valuable today, while still leaving the door open for future improvements. |
|
Maybe we can get a feel of both our API design and whether SortableJS is good enough, if we have more people using it. I propose Inspired by ChatGPT Python library. |
|
Hi everyone, Let me give a short update on what I've been thinking about the last few weeks. Which librarySortableJS is probably good enough for now and we don't need to change it. If we find a good abstraction and the user doesn't have to deal with implementation details, we can always replace the underlying JavaScript library with something else in the future. API designI still don't like the fact that the current implementation requires the user to deal with generic events and map IDs to actual data items. PR #5464 tried to simplify things and gathered valuable feedback. Right now I'm leaning towards a with ui.column().sortable() as column:
ui.label('A').sortable_item(data='A')
ui.label('B').sortable_item(data='B')
with ui.label('C').sortable_item(data='C', handle='.handle'):
ui.icon('drag_handle').classes('handle')The ui.button('Activate', on_click=column.sortable.activate)
ui.button('Deactivate', on_click=column.sortable.deactivate)The Beta namespace@evnchn proposed a I'm planning to continue working on this feature in January. But of course, I can't promise any release date. |
Happy to hear that SortableJS is back in the mix! 🎉 I fully agree that NiceGUI should do the heavy lifting and define a clean, stable API for users. To be honest, I’m not satisfied with the current secondary data.dict() approach in either the nested or flat sortable implementations in our project. Since we’re close to Christmas, allow me to dream big and rephrase what I already outlined here: Suggested Core APIs (MVP):
These operations allow users to maintain a stable 1:1 relationship between UI elements and application data.
More Advanced APIs
I believe these advanced APIs would provide tremendous value, especially since implementing nested structures is never fun. I’m also considering a new project involving another sortable use case, where nested sortables visually represent dependencies. To give more inspiration, I’d like to share the current state of the project.
I think the APIs outlined above provide significant value and will make it much easier for users to build powerful, flexible sortable lists in the future 🛩️ 2025-12-19.21-46-22.mov |
|
I'm super glad to see conversations about this! I feel I'm a bit low on energy on this PR and I don't have much time to put into it for at least a couple of months so I'm more than happy to give this PR over to @phifuh if you're up for it? |
|
@evnchn @falkoschindler I found 2 issues, but I am not sure if they are related to the sortable branch or have a deeper link. So before opning new issues I thought I ping u guys here.
Currently we are running a instance of the sortable treecontroller, so its pretty noticable when the whole data vanish :D This could again be fixed by using storage but I am still curios about the
from collections.abc import Callable
from typing import ClassVar
class ReportValidationStorage:
"""Tracks total errors/warnings via delta updates using classmethods.
Templates call update() with delta values when their validation state changes.
Workspace subscribes via add_listener() to react to changes.
Example:
ReportValidationStorage.update(error_delta=-1, warning_delta=0)
total = ReportValidationStorage.total_errors()
"""
_total_errors: ClassVar[int] = 0
_total_warnings: ClassVar[int] = 0
_listeners: ClassVar[list[Callable[[], None]]] = []
@classmethod
def reset_issues(cls) -> None:
"""Reset the amount of warnings and errors to 0, called by 'load json data'."""
cls._total_warnings = 0
cls._total_errors = 0
@classmethod
def add_listener(cls, callback: Callable[[], None]) -> None:
"""Subscribe to validation state changes.
Args:
callback: Function to call when totals change (no arguments).
"""
if callback not in cls._listeners:
cls._listeners.append(callback)
@classmethod
def remove_listener(cls, callback: Callable[[], None]) -> None:
"""Unsubscribe from validation state changes.
Args:
callback: The callback to remove.
"""
if callback in cls._listeners:
cls._listeners.remove(callback)
@classmethod
def update(cls, error_delta: int, warning_delta: int) -> None:
"""Update totals with delta values and notify listeners.
Args:
error_delta: Change in error count (+1, -1, 0, etc.)
warning_delta: Change in warning count (+1, -1, 0, etc.)
"""
if error_delta == 0 and warning_delta == 0:
return # No change, skip notification
cls._total_errors += error_delta
cls._total_warnings += warning_delta
# Ensure we don't go negative (safety check)
cls._total_errors = max(0, cls._total_errors)
cls._total_warnings = max(0, cls._total_warnings)
cls._notify()
@classmethod
def _notify(cls) -> None:
"""Notify all listeners of state change."""
for listener in cls._listeners:
listener()
@classmethod
def total_errors(cls) -> int:
"""Get the total error count across all templates."""
return cls._total_errors
@classmethod
def total_warnings(cls) -> int:
"""Get the total warning count across all templates."""
return cls._total_warnings
@classmethod
def has_errors(cls) -> bool:
"""Check if there are any errors."""
return cls._total_errors > 0
@classmethod
def has_warnings(cls) -> bool:
"""Check if there are any warnings."""
return cls._total_warnings > 0 def update_validation_badges(self) -> None:
"""Update the error/warning chips from ReportValidationStorage. O(1) operation."""
errors = ReportValidationStorage.total_errors()
warnings = ReportValidationStorage.total_warnings()
if self._error_chip is not None:
error_text = f"{errors} Kritiske fejl"
self._error_chip.set_text(error_text)
self._error_chip.set_visibility(False)
if errors > 0:
self._error_chip.set_visibility(True)
if self._warning_chip is not None:
warning_text = f"{warnings} Advarsler"
self._warning_chip.set_text(warning_text)
self._warning_chip.set_visibility(False)
if warnings > 0:
self._warning_chip.set_visibility(True)& we simply do |
|
I still haven't got myself into this PR, but:
|
@evnchn Thanks I will give it a try to identify the change. Regarding the WebSocket overload, we currently validate each template sequentially. I wondered whether running the validations asynchronously and in parallel could help, even though we have only one FastAPI worker. In theory, some threads would remain idle. However, I’m concerned that this approach might flood the WebSocket even more. So the question is: how can we reduce the strain on the WebSocket while still delivering the necessary UI updates? |
|
Thanks for reporting these, @phifuh. As @evnchn noted, issue 1 is likely a general Socket.IO/WebSocket resilience problem (see #5784), and issue 2 sounds like a NiceGUI version regression unrelated to sortable -- Which brings me to a broader point about this PR. With 100+ commits, 2500+ lines, and 70+ comments covering many side topics, this PR has become very hard to track and maintain. I'm still working on a concept for a proper sortable/draggable API built into NiceGUI, and this PR in its current form probably won't be merged as-is. The API surface isn't where it needs to be for long-term stability, and since I only find time for this every now and then, I can't give a timeline for when it'll be ready. That said, I know people are already using and relying on this code, and I don't want to leave you stranded on an unreleased branch. Therefore I'd propose to move the current sortable implementation to a separate repository (e.g.
Meanwhile, I'll continue working on the built-in NiceGUI API at my own pace, drawing on the lessons learned here. When that's ready, we can look at how to provide a smooth migration path. |
|
I'm also using a versioned instance of this in my code, so having this be a proper extension would be very nice. Currently struggling to get the e2e draggables testing properly. |
This PR adds Sortable to NiceGUI! (https://sortablejs.github.io/Sortable/)
I used the original sortable.js and not any vue adaptations as those were a bit old and were made out of multiple files so I have no idea how to implement them.
Any way, I think I have gotten everything to work correctly. I copied Sortable's examples into the NiceGUI sortable example so you can try out all the different options you can use.
Currently the example contains a bunch of custom css. It would be best to add them into the sortable.py but I would like to know what the best way would be to implement it?
Feature request: #932