Skip to content

Add Sortablejs as element (draggable objects!)#4656

Open
EmberLightVFX wants to merge 106 commits intozauberzeug:mainfrom
EmberLightVFX:Add-SortableJS
Open

Add Sortablejs as element (draggable objects!)#4656
EmberLightVFX wants to merge 106 commits intozauberzeug:mainfrom
EmberLightVFX:Add-SortableJS

Conversation

@EmberLightVFX
Copy link
Copy Markdown
Contributor

@EmberLightVFX EmberLightVFX commented Apr 24, 2025

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

@evnchn evnchn added the feature Type/scope: New or intentionally changed behavior label Apr 24, 2025
@evnchn
Copy link
Copy Markdown
Collaborator

evnchn commented Apr 24, 2025

not any vue adaptations as those were a bit old

Wise move. https://github.com/SortableJS/vue.draggable.next is 4 years old.

To clarify, I think your file sortable.complete.esm.js is the same one as in https://www.npmjs.com/package/sortablejs?activeTab=code, right?

This is because, eventually, we would need a script to fetch it automatically (check out npm.py and npm.json)

Currently the example contains a bunch of custom css. It would be best to add them into the sortable.py

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 nicegui-*. A bunch of other elements got the treatment there.

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.

Any way, I think I have gotten everything to work correctly.

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.

@EmberLightVFX
Copy link
Copy Markdown
Contributor Author

Updated with some missing examples and options for sortablejs.
I also added some default css in the css file :)
The only example not currenty working is the Thresholds one. Need to take a better look at it...

About the npm,py/json I would gladly take some help with that, or some instructions on how that part of NiceGUI works.
https://www.npmjs.com/package/sortablejs?activeTab=code is the correct one. I'm only using the file sortable.complete.esm.js that's under /sortable/modular/sortable.complete.esm.js as it contains all official plugins for sortable.

@EmberLightVFX
Copy link
Copy Markdown
Contributor Author

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.

@falkoschindler
Copy link
Copy Markdown
Contributor

Thanks for the pull request, @EmberLightVFX! Bringing SortableJS to NiceGUI would be an awesome feature!
I'll review your code as soon as possible. The update of npm.json shouldn't be a problem. I can do that. 👍🏻

@falkoschindler falkoschindler self-requested a review May 2, 2025 23:27
@falkoschindler falkoschindler added 🟠 major Priority: Important, but not urgent review Status: PR is open and needs review labels May 2, 2025
@evnchn evnchn mentioned this pull request May 3, 2025
@evnchn
Copy link
Copy Markdown
Collaborator

evnchn commented May 10, 2025

@EmberLightVFX Include the following in your npm.json:

  "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 sortable.complete.esm.js and place the file at nicegui/elements/lib/sortable/sortable.complete.esm.js

@EmberLightVFX
Copy link
Copy Markdown
Contributor Author

I were suppose to update my drop_zone branch but accidentally updated this one 😅 I'm not working on this one.
I would love some help on how to correctly set up the esm dependencie for NiceGUI 3!

@falkoschindler falkoschindler changed the title Add Sortablejs as element (dragable objects!) Add Sortablejs as element (draggable objects!) Nov 10, 2025
Copy link
Copy Markdown
Contributor

@falkoschindler falkoschindler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.options up to date? If not, setOption also collapses to basically a one-liner.
  • I wonder if we need to declare methods like sort, enable, disable, getOption, and setOption in JS. Maybe we can simply add a generic runSortableMethod (like run_grid_method in aggrid.js or run_editor_method in json_editor.js).
  • You often mix .id and .dataset.id like item: 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 to null might actually cause bugs, because null is 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 clear method 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 dictionary self.client.elements.

@falkoschindler falkoschindler added in progress Status: Someone is working on it and removed review Status: PR is open and needs review labels Nov 10, 2025
@falkoschindler
Copy link
Copy Markdown
Contributor

After thinking about this PR once again, I came to the following conclusion:

  1. I'm still not sure about the desired API. Currently we're mimicking SortableJS, and yes, we can drag and drop elements. But to actually use ui.sortable, e.g., to organize items in a database, the user has to convert IDs from generic events into useful information, resulting in rather complex user code.
  2. I'm still not convinced that SortableJS is the right library for us. It's debatable if the repository is "stale" or just "stable". But there are other strong contenders that need to be considered before regretting a poor decision it later.

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.

@falkoschindler falkoschindler added blocked Status: Blocked by another issue or dependency and removed in progress Status: Someone is working on it labels Nov 11, 2025
@EmberLightVFX
Copy link
Copy Markdown
Contributor Author

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 😄

@phifuh
Copy link
Copy Markdown

phifuh commented Nov 11, 2025

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.

@evnchn
Copy link
Copy Markdown
Collaborator

evnchn commented Nov 11, 2025

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 ui.beta API will change in minor version, such that ui.beta.sortablejs (sortablejs is intentional, to indicate that we are using SortableJS) can let us experiment.

Inspired by ChatGPT Python library.

@falkoschindler
Copy link
Copy Markdown
Contributor

Hi everyone,

Let me give a short update on what I've been thinking about the last few weeks.

Which library

SortableJS 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 design

I 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 .sortable() method that can be called from container elements like ui.column, ui.row, ui.card, ui.list, and a .sortable_item() method to mark children as draggable and to attach data:

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 .sortable namespace can also hold additional properties and methods, e.g. to activate and deactivate the sortable feature:

ui.button('Activate', on_click=column.sortable.activate)
ui.button('Deactivate', on_click=column.sortable.deactivate)

The .sortable() method can accept an event handler which can access the current data list of the child elements.

Beta namespace

@evnchn proposed a ui.beta namespace to hold experimental features. I'm not so sure about it because NiceGUI could more and more become a collection of unfinished features. Instead I think it would be better to release elements like ui.sortable as standalone packages so that users can try them out and provide feedback.

I'm planning to continue working on this feature in January. But of course, I can't promise any release date.

@falkoschindler falkoschindler added in progress Status: Someone is working on it and removed blocked Status: Blocked by another issue or dependency labels Dec 19, 2025
@phifuh
Copy link
Copy Markdown

phifuh commented Dec 19, 2025

API design

I 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 .sortable() method that can be called from container elements like ui.column, ui.row, ui.card, ui.list, and a .sortable_item() method to mark children as draggable and to attach data:

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 .sortable namespace can also hold additional properties and methods, e.g. to activate and deactivate the sortable feature:

ui.button('Activate', on_click=column.sortable.activate)
ui.button('Deactivate', on_click=column.sortable.deactivate)

The .sortable() method can accept an event handler which can access the current data list of the child elements.

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:
#5464 (comment)

Suggested Core APIs (MVP):

  • get_all_data()
  • insert_at_index(position, data)
  • get_data(by: element_name | id | position)
  • delete(by: element_name | id | position)
  • update(by: element_name | id | position)

These operations allow users to maintain a stable 1:1 relationship between UI elements and application data.
Common use cases, including:

  • Simple lists
  • Todo apps
  • Settings pages
  • Dynamic forms

More Advanced APIs

  • duplicate(by: element_name | id | position)
  • allow_nested_sortable()
  • set_dropzone(size, range, etc.)
  • undo_last_move() -> None
  • set_max_nested_depth(number)
  • get_max_depth()
  • get_path(by: element_name | id | position)
  • sort_by(xxx)
  • delete_all()

I believe these advanced APIs would provide tremendous value, especially since implementing nested structures is never fun.
Creating mechanisms such as blocking moves when max_depth is exceeded is exactly the kind of problem the framework is perfectly positioned to solve for users. Dropzones is another canidate for nested sortables.

I’m also considering a new project involving another sortable use case, where nested sortables visually represent dependencies.
Imagine a questionnaire: if a user answers A, a follow-up question is shown. This relationship could be naturally expressed using nested sortable functionality.

To give more inspiration, I’d like to share the current state of the project.
In the video, you can see:

  • Fast loading from JSON
  • A nested sortable with two different item types:
    • ui.expansions, which act as nested containers to group items (rename, add, delete)
    • ui.rows, which are normal items with fewer options in the vertical menu (rename, delete)
  • Dropzones for items inside ui.expansions
  • A maximum depth constraint preventing more than four nested layers (Shown in the bottom of the video as ui.notfication, a little bit hard to see if the video isnt fullscreen)
  • A flat sortable where nesting is explicitly prevented

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

@EmberLightVFX
Copy link
Copy Markdown
Contributor Author

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 evnchn added help wanted Status: Author is inactive, others are welcome to jump in and removed in progress Status: Someone is working on it labels Feb 17, 2026
@phifuh
Copy link
Copy Markdown

phifuh commented Feb 18, 2026

@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.

  1. We are still working on the sortable branch and when adding about 100items into the sortable tree the app gets a little bit laggy. (This can most likely be fixed) but when this happens we get a reloading because handshake failed for clientID xxxx. Nicegui isnt picking up on the disconnect/handshake errors, because I asume its happnings after a client disconnection inside js? I think it would be very valueable to have that error bubble up or have some sort of heartbeat, even the official website has somethings the "disconnect" toast and I am assuming you guys neither have logs on that?

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 handshake issue and if we can help tracking it down.

  1. When running sortable with nicegui 3.2 we can update a badge which is hooked up to singleclass (Validation storage, which has delta errors/warnings). When running exactly the same code with nicegui 3.6 + sortable branch this ui element wont update. So my question is, was there some "breaking" change from v3.2 -> 3.6 which could have an influence on this?
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 ReportValidationStorage.update(error_delta, warning_delta)

@evnchn
Copy link
Copy Markdown
Collaborator

evnchn commented Feb 19, 2026

I still haven't got myself into this PR, but:

  • If your JS lags hard enough that you see reloading because handshake failed for clientID xxxx then you may have blocked the JS main thread long enough that Socket.IO connection cannot be sustained. While Use Web Worker heartbeat to keep connection alive during blocking dialogs #5784 will salvage the situation, it is not ideal.
  • Nothing comes to mind for big changes in 3.2 through 3.6 which could break your ReportValidationStorage. I would recommend that you can bisect by yourself to land on a specific PR which broke things.

@phifuh
Copy link
Copy Markdown

phifuh commented Feb 19, 2026

I still haven't got myself into this PR, but:

  • If your JS lags hard enough that you see reloading because handshake failed for clientID xxxx then you may have blocked the JS main thread long enough that Socket.IO connection cannot be sustained. While Use Web Worker heartbeat to keep connection alive during blocking dialogs #5784 will salvage the situation, it is not ideal.
  • Nothing comes to mind for big changes in 3.2 through 3.6 which could break your ReportValidationStorage. I would recommend that you can bisect by yourself to land on a specific PR which broke things.

@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?
As mentioned in: #4758 (reply in thread) we have the communication broker -> validate / update ui flow.

@falkoschindler
Copy link
Copy Markdown
Contributor

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 -- git bisect should help narrow it down. Either way, both deserve their own issues or discussion threads rather than being discussed here.

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. nicegui-sortable or similar). This would:

  • Provide official releases so users don't have to depend on a feature branch
  • Give room for focused discussions about sortable-specific issues (like the two above)
  • Allow the community to iterate and improve independently

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.

@Denubis
Copy link
Copy Markdown
Contributor

Denubis commented Feb 20, 2026

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.

@falkoschindler falkoschindler removed the help wanted Status: Author is inactive, others are welcome to jump in label Apr 3, 2026
@falkoschindler falkoschindler modified the milestones: Next, 3.11 Apr 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Type/scope: New or intentionally changed behavior 🟠 major Priority: Important, but not urgent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants