Sortable Widget

I want to build a page where which sortable elements (similiar to something like: Sortable | jQuery UI).

Then I want to be able to get the current order of those items to register watchers on that. Would this be possible with panel?

Hi @kring

You can use ReactiveHTML to wrap any js library.

I would recommend wrapping a non-jquery based one like SortableJS.

Try to build some small code that works, iterate from there and post your questions below :+1:

that looks great, will cheeck it out

1 Like

ReactiveHTML looks really powerfull, but I somehow don’t get it to work:

import panel as pn
pn.extension(js_files={'sortablelist': "https://raw.githack.com/SortableJS/Sortable/master/Sortable.js"})#"http://SortableJS.github.io/Sortable/Sortable.js"})

from panel.reactive import ReactiveHTML
import param
class Slideshow(ReactiveHTML):
    
    index = param.Integer(default=0)
    
    _template = """
  <div id="abc">title</div>
  <div id="simpleList" class="list-group">
    <div class="list-group-item">This is <a href="http://rubaxa.github.io/Sortable/">Sortable</a></div>
    <div class="list-group-item">It works with Bootstrap...</div>
    <div class="list-group-item">...out of the box.</div>
    <div class="list-group-item">It has support for touch devices.</div>
    <div class="list-group-item">Just drag some elements around.</div>
  </div>
     """
    _scripts = {
     'after_layout': """
    var el = document.getElementById('simpleList');
    var title = document.getElementById('abc');
    console.log("hallo", el, title)
    Sortable.create(el, { /* options */ });
     """
   }
        
Slideshow(width=800, height=300)

For some reason the document.getElementById does not find my HTML nodes. Any ideas why?

Hi @kring

If you inspect the html you will see that your ids are appended an integer. That is to make them unique if inserted multiple times.

But instead of using document.getElementById('simpleList') you should be able to refer to simpleList directly.

So you can do like below.

import panel as pn
pn.extension(js_files={'sortablelist': "https://raw.githack.com/SortableJS/Sortable/master/Sortable.js"})#"http://SortableJS.github.io/Sortable/Sortable.js"})

from panel.reactive import ReactiveHTML
import param
class Slideshow(ReactiveHTML):

    index = param.Integer(default=0)

    _template = """
  <div id="abc">title</div>
  <div id="simpleList" class="list-group">
    <div class="list-group-item">This is <a href="http://rubaxa.github.io/Sortable/">Sortable</a></div>
    <div class="list-group-item">It works with Bootstrap...</div>
    <div class="list-group-item">...out of the box.</div>
    <div class="list-group-item">It has support for touch devices.</div>
    <div class="list-group-item">Just drag some elements around.</div>
  </div>
     """
    _scripts = {
     'after_layout': """
    console.log("hallo", simpleList, abc)
    Sortable.create(simpleList, { /* options */ });
     """
   }

Slideshow(width=800, height=300).servable()

2 Likes

Next steps are probably configuration and reacting to events https://github.com/SortableJS/Sortable#options. Looking forward to see that in action :slight_smile:

Thanks a lot, got it to work :slight_smile:


import param
import panel as pn
pn.extension(js_files={'sortablelist': "https://raw.githack.com/SortableJS/Sortable/master/Sortable.js"})

class Sortable(pn.reactive.ReactiveHTML):

    rows = param.List(default=[0,1,2,3,4,5])

    _template = """
      <div id="simpleList" class="list-group">
          {% for row in rows %}
            <div class="list-group-item">Row number ${row}</div>
          {% endfor %}
      </div>
      <button id="save_btn" type="button" onclick="${save}">Save</button>
     """
    
    _scripts = {
     'after_layout': """Sortable.create(simpleList, {
         onEnd: function(evt) {
            var element = data.rows[evt.oldIndex];
            list = data.rows
            list.splice(evt.oldIndex, 1);
            list.splice(evt.newIndex, 0, element);
            data.rows = list
        }
     });"""
   }
    
    def save(self, event):
        print("Do some save magic with: ", self.rows)
        
s = Sortable(width=800, height=300)
s.servable()
1 Like

Awesome. I made a version that is a bit more general just in case someone needs it.

import param
import panel as pn

class Sortable(pn.reactive.ReactiveHTML):
    rows = param.List()

    _template = """
      <div id="simpleList" class="list-group">
          {% for row in rows %}
            <div class="list-group-item">${row}</div>
          {% endfor %}
      </div>
     """

    _scripts = {
      'after_layout': """Sortable.create(simpleList, {
          onEnd: function(evt) {
              var element = data.rows[evt.oldIndex];
              list = data.rows
              list.splice(evt.oldIndex, 1);
              list.splice(evt.newIndex, 0, element);
              data.rows = list
          }
      });"""
    }

    __javascript__=[
      "https://raw.githack.com/SortableJS/Sortable/master/Sortable.js"
    ]

pn.extension(sizing_mode="stretch_width")

s = Sortable(rows=[
  "Panel is awesome 0",
  "Panel is awesome 1",
  "Panel is awesome 2",
  "Panel is awesome 3",
  "Panel is awesome 4",
  "Panel is awesome 5",
])

pn.template.FastListTemplate(
  site="Awesome Panel",
  title="Sortable Rows",
  main=[pn.Column(s, s.param.rows)],
  header_accentbasecolor="#f3e4df").servable()
2 Likes

If you don’t mind, I would like to ask a follow up question.

Let’s say I want to use the updated list in the Sortable and print it in the static text widget (or some other calculation).

How do I do that? What i’m trying looks like this

import param
import panel as pn
pn.extension(js_files={'sortablelist': "https://raw.githack.com/SortableJS/Sortable/master/Sortable.js"})

class Sortable(pn.reactive.ReactiveHTML):

    rows = param.List(default=[0,1,2,3,4,5])

    _template = """
      <div id="simpleList" class="list-group">
          {% for row in rows %}
            <div class="list-group-item">Row number ${row}</div>
          {% endfor %}
      </div>
      
     """
    _scripts = {
     'after_layout': """Sortable.create(simpleList, {
         onEnd: function(evt) {
            var element = data.rows[evt.oldIndex];
            list = data.rows
            list.splice(evt.oldIndex, 1);
            list.splice(evt.newIndex, 0, element);
            data.rows = list
        }
     });""",
   }
    
    @param.output(param.List)
    def list_out(self):
        return self.rows

s = Sortable(rows=[6,7,8,9,10,11])

@pn.depends('s')
def test(s):
    static_text.value = str(s.list_out())
    return None

static_text = pn.widgets.StaticText(name='Static Text', value = str(s.list_out()))

pn.Column(s,test(s),static_text,).show()

Does it make sense to use @pn.depends like this?

Sincerely,

Hi @thetorque

Welcome to the community :+1:

Your code is almost there. Depending on a string like 's' is something you would do on a class. Here you would depend on s.param.rows instead. Furthermore you should add watch=True to automatically run the function when the value of rows changes (instead of including it in the Column).

With s renamed to sortable, the code could look like

import param
import panel as pn
pn.extension(js_files={'sortablelist': "https://raw.githack.com/SortableJS/Sortable/master/Sortable.js"})

class Sortable(pn.reactive.ReactiveHTML):

    rows = param.List(default=[0,1,2,3,4,5])

    _template = """
      <div id="simpleList" class="list-group">
          {% for row in rows %}
            <div class="list-group-item">Row number ${row}</div>
          {% endfor %}
      </div>
      
     """
    _scripts = {
     'after_layout': """Sortable.create(simpleList, {
         onEnd: function(evt) {
            var element = data.rows[evt.oldIndex];
            list = data.rows
            list.splice(evt.oldIndex, 1);
            list.splice(evt.newIndex, 0, element);
            data.rows = list
        }
     });""",
   }
    
    @param.output(param.List)
    def list_out(self):
        return self.rows

sortable = Sortable(rows=[6,7,8,9,10,11])

static_text = pn.widgets.StaticText(name='Static Text')

@pn.depends(rows=sortable.param.rows, watch=True)
def update(rows=sortable.rows):
    static_text.value = str(rows)
update()

pn.Column(sortable,static_text,).servable()

Run it with

panel serve myscript.py --autoreload

This is fantastic ! Thank you very much.

I have another quick question about pipeline, namely, how do I display the 2 stages side-by-side instead of having to display one at a time? Also, is it possible to just display only stage 2 of the pipeline?

Let me know if I should start a new thread since it’s not related to Sortable.

1 Like

Hi @thetorque

Yes. Please open a new, specific question. Try to put in a minimum, reproducible code example or alternatively some screenshots. It really helps when you are on the other side trying to understand the question and help.

Here is a new years version of the sortable rows

sortable-rows2

import param
import panel as pn
pn.extension(js_files={'sortablelist': "https://raw.githack.com/SortableJS/Sortable/master/Sortable.js"})

class Sortable(pn.reactive.ReactiveHTML):

    rows = param.List(default=[0,1,2,3,4,5])

    _template = """
      <div id="simpleList" class="list-group" style="font-size:48px">
          {% for row in rows %}
            <div class="list-group-item">${row}</div>
          {% endfor %}
      </div>
      
     """
    _scripts = {
     'after_layout': """Sortable.create(simpleList, {
         onEnd: function(evt) {
            var element = data.rows[evt.oldIndex];
            list = data.rows
            list.splice(evt.oldIndex, 1);
            list.splice(evt.newIndex, 0, element);
            data.rows = list
        }
     });""",
   }
    
    @param.output(param.List)
    def list_out(self):
        return self.rows

sortable = Sortable(rows=["Panel", "New", "Year", "Happy", "Friends", ])

static_text = pn.widgets.StaticText(name='')

@pn.depends(rows=sortable.param.rows, watch=True)
def update(rows=sortable.rows):
    static_text.value = str(rows)
update()

accent="#7575FF"

pn.template.FastListTemplate(
  site="Awesome Panel", title="Community Question: Sortable list",
  main=[sortable, static_text],
  accent_base_color=accent, header_background=accent
).servable()
3 Likes

Dear @Marc , if I would like to change/add/delete the elements in the list in existing Sortable, for example, there is a button where every time I press it adds the next number into the list. How should I approach this?

I will make another thread about pipeline.

Happy New Year to you too !!

It seems like by doing a simple

sortable.rows = [0,1,2,3]

will update the list displayed in the static text. However, the code in the template–namely

    _template = """
      <div id="simpleList" class="list-group">
          {% for row in rows %}
            <div class="list-group-item">${row}:{{row_dict[row|string]}} </div>
          {% endfor %}
      </div>
      
     """

Probably need to get run again to update the list shown in the Sortable? How to do this?

Hi @thetorque

You can use this version

import param
import panel as pn
pn.extension(js_files={'sortablelist': "https://raw.githack.com/SortableJS/Sortable/master/Sortable.js"})

class Sortable(pn.reactive.ReactiveHTML):

    rows = param.List(default=[0,1,2,3,4,5])

    _template = """
      <div id="simpleList" class="list-group" style="font-size:48px"></div>
     """
    _scripts = {
    'render': """
var html = ""
data.rows.forEach(setRows);

function setRows(value, index, array) {
  html += `<div class="list-group-item">${value}</div>`
}
simpleList.innerHTML = html
""",
    'after_layout': """state.sortable = Sortable.create(simpleList, {
        onEnd: function(evt) {
        var element = data.rows[evt.oldIndex];
        list = data.rows
        list.splice(evt.oldIndex, 1);
        list.splice(evt.newIndex, 0, element);
        data.rows = list
    }
    });""",
    "rows": """
state.sortable.destroy()
self.render()
self.after_layout()
"""
   }
    
    @param.output(param.List)
    def list_out(self):
        return self.rows

sortable = Sortable(rows=["Original", "values"])

update_button = pn.widgets.Button(name="Update")

@pn.depends(value=update_button, watch=True)
def update(value):
    sortable.rows=["Some", "new", "values"]

accent="#7575FF"

pn.template.FastListTemplate(
  site="Awesome Panel", title="Community Question: Sortable list",
  main=[update_button, sortable, sortable.param.rows],
  accent_base_color=accent, header_background=accent
).servable()

I’ve created a bug report/ feature request here ReactiveHTML does not rerender loop in _template · Issue #3059 · holoviz/panel (github.com)

Thank you so much.

This explains a lot.
I stumbled upon this post while implementing my own sortable widget and experiencing the mishap with getelementbyid()

While this seems to help me with the issue I’ve been having, I’m not sure I understand where these id-based objects are coming from(how they’re set).
I’m a bit new to Bokeh’s shadow DOM and the scoping that’s going on.

I took a shot at debugging inside the after_layout script call with the debugger but can’t seem to find the ‘draggable_list’ object or where it is located in the local this scope of the function call that has access to it. When I console.log the draggable list, it does return the element with the draggable-list-(someid) id.

    _scripts = {
        'after_layout': """
function myFunction() {
    console.log(draggable_list);
    debugger; //
    // 
}
myFunction();

"""

I’ve dug through the documentation and I think the only place this was mentioned was in the Building Custom Components doc in the Scripts section about <nodes> - however it didn’t make it clear to me that we can access the elements by simply using their IDs.

My question is, how is the assignment happening? Is there further technical documentation of this somewhere?

Below is a picture of the local scope visible from within the after_layout script function call. I don’t see the binding anywhere, or how it’s accessed within said function.

1 Like

I appreciate this question and agree that bokeh’s dynamic ID in the DOM is difficult to work with, some documentation or advice on working with them would be very helpful.

I’m also wondering if this continues to be the best way to create a sortable widget? I vote adding this to the multi-choice widget as part of the standard panel library would be a valuable addition. :slight_smile:

A built-in sortable list widget would be cool.
However, I think that the ability to create custom components has recently become much more powerful now that you can easily generate very functional HTML/CSS?JS with generative models(ChatGPT, Claude, LlaMA).
If Panel can hook into that new superpower we’ve all just gained, I think it can multiply its utility tenfold.
Personally, I’ve read the ReactiveHTML documentation about 4+ times and still need to look back on it to grasp what’s happening. :sweat_smile: