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()

1 Like

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()
2 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.