Skip to content

kv9898/shiny_sortable

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Shiny-sortable

A Python library to add SortableJS widgets to Shiny UI, which is a Python wrapper of the SortableJS library.

Install

pip install shiny-sortable

Basic Usage

The easiest way to build a sortable shiny widget is to use the @make() decorator. There is a playground for the example below at my website.

The @make() decorator has three optional arguments:

  1. ID (str): The keyword argument name to extract the input ID from, defaults to 'inputID', if not found, defaults to the first argument.
  2. dataID (str): The attribute used to store data-id on sortable items.
  3. updatable (bool): Whether the sortable items should be updatable.

Let's use an ordered list as an example:

from shiny import *
import shiny_sortable as sortable

@sortable.make()
def sortable_list(inputID): # add `inputID` as the first argument
    list = ui.tags.ol(
        ui.tags.li("Item 1", **{'data-id': '1'}),
        ui.tags.li("Item 2", **{'data-id': '2'}),
        ui.tags.li("Item 3", **{'data-id': '3'}),
        id=inputID
    )
    return list

This allows a sortable list when it is added to the UI:

app_ui = ui.page_fluid(
    sortable_list("list"),
)
app = App(app_ui, None)

The list will look like this:

A sortable list

Alternatively, you can use your own keyword arguments:

@sortable.make(ID="SomeRandomID", dataID="SomeDataId")
def sortable_list(SomeRandomID): # use `SomeRandomID` as the first argument
    list = ui.tags.ol(
        ui.tags.li("Item 1", SomeDataId = '1'),
        ui.tags.li("Item 2", SomeDataId = '2'),
        ui.tags.li("Item 3", SomeDataId = '3'),
        id=SomeRandomID
    )
    return list

The order of the items can be retrieved as a Shiny input:

from shiny import *
import shiny_sortable as sortable

@sortable.make()
def sortable_list(inputID):
    list = ui.tags.ol(
        ui.tags.li("Item 1", **{'data-id': '1'}),
        ui.tags.li("Item 2", **{'data-id': '2'}),
        ui.tags.li("Item 3", **{'data-id': '3'}),
        id=inputID
    )
    return list

app_ui = ui.page_fluid(
    sortable_list("list"),
    ui.output_text_verbatim(id = "text")
)

def server(input, output, session):
    list_order = reactive.value("")
    @output
    @render.text
    def text():
        return list_order()

    @reactive.effect
    @reactive.event(input.list)
    def _():
        list_order.set(input.list())

app = App(app_ui, server)

This mini-app runs like this:

alt text

Moreover, we can make the sortable widget updatable by passing updatable=True to the @make() decorator. This allows the order to be updated by using the update() function.

For example, we can add a "Reset" button to the UI which will reset the list order to the initial state of 123.

from shiny import *
import shiny_sortable as sortable

@sortable.make(updatable=True)
def sortable_list(inputID):
    list = ui.tags.ol(
        ui.tags.li("Item 1", **{'data-id': '1'}),
        ui.tags.li("Item 2", **{'data-id': '2'}),
        ui.tags.li("Item 3", **{'data-id': '3'}),
        id=inputID
    )
    return list

app_ui = ui.page_fluid(
    sortable_list("list"),
    ui.output_text_verbatim(id = "text"),
    ui.input_action_button("reset", "Reset")
)

def server(input, output, session):
    list_order = reactive.value("")
    @output
    @render.text
    def text():
        return list_order()

    @reactive.effect
    @reactive.event(input.list)
    def _():
        list_order.set(input.list())

    @reactive.effect
    @reactive.event(input.reset)
    async def _():
        await sortable.update(session, "list", ["1", "2", "3"])


app = App(app_ui, server)

This mini-app runs like this:

alt text

Not that for updating the sortable widget, async and await are required, since session.send_custom_message() is used behind the scenes.

Custom Usage

There are also three internally used functions which can be used to create custom widgets.

  • dep(): Creates and returns a SortableJS HTML dependency. Behind the scenes, it does:
    sortable_dep = HTMLDependency(
         name="SortableJS",
         version="1.15.3",
         source={
             "href": "https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.3"
         },
         script={"src": "Sortable.min.js"},
     )
     return sortable_dep
  • input(): Returns a script tag for initializing a Sortable instance on an element. The tag looks like:
    script = f"""
     var el_{inputID} = document.getElementById('{inputID}');
     if (el_{inputID}) {{
         var sortable_{inputID} = new Sortable(el_{inputID}, {{
             dataIdAttr: '{dataID}',
             animation: 150,
             ghostClass: 'sortable-ghost',
             chosenClass: 'sortable-chosen',
             onSort: function (evt) {{
                 var order = sortable_{inputID}.toArray();
                 Shiny.setInputValue("{inputID}", order);
             }}
         }});
     }} else {{
         console.error("Element with id '{inputID}' not found");
     }}
     """
     return tags.script(script)
  • output(): Returns a script tag for handling updates to the Sortable instance order. The tag looks like:
    script = f"""
     Shiny.addCustomMessageHandler("sortable_update_{outputID}", function(message) {{
         if (typeof sortable_{outputID} !== 'undefined') {{
             sortable_{outputID}.sort(message.order);
             Shiny.setInputValue("{outputID}", message.order);
         }} else {{
             console.error("sortable_{outputID} is not defined. Cannot update order.");
         }}
     }});
     """
     return tags.script(script)

Releases

No releases published

Packages

No packages published

Languages