Adding to a sortable bucketlist

The {sortable} package

{sortable} is an incredibly useful R package built upon the sortbale.js javascript library, allowing drag-and-drop features to be incorporated into shiny apps. The package works with two types of sortable lists:

  • rank list: items can be sorted within a list
  • bucket list: a single object containing multiple rank lists, allowing for drag-and-drop between lists.

The second option (bucket list) is useful when a user wishes to divide a set of values between two or more buckets. {sortable} integrates well within shiny but requires a few tweaks if data are added to the sortable object once it has been created.

Note

{sortable} includes a function, update_bucket_list() which is used to update header text but not list items.

Case One. Dragging Existing Values Between Lists

Here's a simple scenario. Suppose you have a list of objects in list 1 and you wish to subset to list 2, returning the values in list 2. Using {sortable} you could define a bucket list containing two rank lists (list 1 and list 2), populate list 1 and drag-and-drop values to list 2. In a shiny app this could be coded as follows:

 1library(shiny)
 2library(sortable)
 3
 4ui <- fluidPage(
 5  uiOutput("ui_sort"),
 6  verbatimTextOutput("txt_output")
 7)
 8
 9server <- function(input, output, session) {
10  
11  output$ui_sort <- renderUI({
12    
13    ## create styled list 1
14    list_1_tags <- lapply(LETTERS[1:5], function(x) {
15      tags$span(id = paste0('lab_', x), x, class = "label label-primary", `data-rank-id` = x)
16    })
17    
18    ## create styled list 2
19    list_2_tags <- lapply(LETTERS[6:8], function(x) {
20      tags$span(id = paste0('lab_', x), x, class = "label label-primary", `data-rank-id` = x)
21    })
22    
23    ## sortbale bucket list
24    sortable::bucket_list(
25      header = "Move values from list 1 to list 2",
26      group_name = "reorder_list",
27      sortable::add_rank_list(
28        input_id = "reorder_list_1",
29        text = "list 1",
30        labels = list_1_tags
31      ),
32      sortable::add_rank_list(
33        input_id = "reorder_list_2",
34        text = "list 2",
35        labels = list_2_tags
36      ))
37  })
38  
39  output$txt_output <- renderPrint({
40    print(input$reorder_list)
41  })
42  
43}
44
45shinyApp(ui, server)

In this example, every time list 1 or list 2 changes, input$reorder_list updates with the ordered values for each list.

Case Two. Adding a New Value to an Existing List

Updating the code above with an actionButton linked to insertUI allows us to add a new option to list 2 when the button is pressed. This approach works but you'll see that it does not update input$reorder_list until the list is updated (by moving an item).

 1library(shiny)
 2library(sortable)
 3
 4ui <- fluidPage(
 5  uiOutput("ui_sort"),
 6  actionButton("but_add", "Add"),
 7  verbatimTextOutput("txt_output")
 8)
 9
10server <- function(input, output, session) {
11  
12  output$ui_sort <- renderUI({
13    
14    ## create styled list 1
15    list_1_tags <- lapply(LETTERS[1:5], function(x) {
16      tags$span(id = paste0('lab_', x), x, class = "label label-primary", `data-rank-id` = x)
17    })
18    
19    ## create styled list 2
20    list_2_tags <- lapply(LETTERS[6:8], function(x) {
21      tags$span(id = paste0('lab_', x), x, class = "label label-primary", `data-rank-id` = x)
22    })
23    
24    ## sortbale bucket list
25    sortable::bucket_list(
26      header = "Move values from list 1 to list 2",
27      group_name = "reorder_list",
28      sortable::add_rank_list(
29        input_id = "reorder_list_1",
30        text = "list 1",
31        labels = list_1_tags
32      ),
33      sortable::add_rank_list(
34        input_id = "reorder_list_2",
35        css_id = "css_reorder_list_2",
36        text = "list 2",
37        labels = list_2_tags
38      ))
39  })
40  
41  observeEvent(input$but_add, {
42    ## insert a new value into list 2
43    val <- "ZZ"
44    new_tag <- tags$div(class = "rank-list-item", draggable = FALSE,
45                        tags$span(id = paste0('lab_', val), val, class = "label label-primary", `data-rank-id` = val)
46    )
47    insertUI(selector = "#css_reorder_list_2", where = "beforeEnd", ui = new_tag, immediate = TRUE)
48  })
49  
50  output$txt_output <- renderPrint({
51    print(input$reorder_list)
52  })
53  
54}
55
56shinyApp(ui, server)

In order to make this approach work we can manage the ordered list, list 2 through a shiny input, input$sort_list_2. This shiny variable is kept up to date through two methods:

  • sortable::sortable_options() updates input$sort_list_2 when list 2 is first initialized, a new item is dragged from list 1 or the order is changed.
  • When the new item is added, a javascript function is executed, updating input$sort_list_2 with the newly ordered list. The javascript function works but identifying the identifer of the list and looping through its members, populating input$sort_list_2.
 1library(shiny)
 2library(sortable)
 3
 4ui <- fluidPage(
 5  
 6  tagList(
 7    tags$head(tags$script(src = "script.js")),
 8    uiOutput("ui_sort"),
 9    actionButton("but_add", "Add"),
10    verbatimTextOutput("txt_output")
11  )
12  
13)
14
15l1 <- LETTERS[1:5]
16l2 <- LETTERS[6:8]
17
18server <- function(input, output, session) {
19  
20  output$ui_sort <- renderUI({
21    
22    list_1_tags <- lapply(l1, function(x) {
23      tags$span(id = paste0('lab_', x), x, class = "label label-primary", `data-rank-id` = x)
24    })
25    
26    list_2_tags <- lapply(l2, function(x) {
27      tags$span(id = paste0('lab_', x), x, class = "label label-primary", `data-rank-id` = x)
28    })
29    
30    sortable::bucket_list(
31      header = "Reorder values in 'ordered values' column",
32      group_name = "reorder_list",
33      sortable::add_rank_list(
34        input_id = "reorder_list_1",
35        text = "list 1",
36        labels = list_1_tags
37      ),
38      sortable::add_rank_list(
39        input_id = "reorder_list_2",
40        css_id = "css_reorder_list_2",
41        text = "list 2",
42        labels = list_2_tags,
43        options = sortable_options(
44          onSort = sortable_js_capture_input("sort_list_2"),
45          onLoad = sortable_js_capture_input("sort_list_2"))
46      ))
47  })
48  
49  observeEvent(input$but_add, {
50    print("adding ZZ")
51    val <- "ZZ"
52    new_tag <- tags$div(class = "rank-list-item", draggable = FALSE,
53                        tags$span(id = paste0('lab_', val), val, class = "label label-primary", `data-rank-id` = val)
54    )
55    insertUI(selector = "#css_reorder_list_2", where = "beforeEnd", ui = new_tag, immediate = TRUE)
56    session$sendCustomMessage('update_sortable', list(id = "css_reorder_list_2", shinyinput = "sort_list_2"))
57  })
58  
59  output$txt_output <- renderPrint({
60    print(input$sort_list_2)
61  })
62  
63}
64
65shinyApp(ui, server)
1Shiny.addCustomMessageHandler('update_sortable', function(x) {
2  if (typeof Shiny !== 'undefined') {
3    el = document.getElementById(x.id);
4    shinyinputname = x.shinyinput + ':sortablejs.rank_list'
5    Shiny.setInputValue(shinyinputname, $.map(el.children, function(child) {
6      return $(child).attr('data-rank-id') || $.trim(child.innerText);
7    }))
8  }
9})

Conclusion

The code above demonstrates a suitable approach to add items to a sortable bucket list in a shiny app. The concept can be extended to work with multiple lists, returning the content of each.