14

Here is a simple reproducible example:

library(shiny)
library(shinyWidgets)

ui <- fluidPage(
    pickerInput("test",choices=list("A"=c(1,2,3,4,5),"B"=c(6,7,8,9,10)),multiple=TRUE),
    textOutput("testOutput")
)

server <- function(input, output) {
    output$testOutput <- renderText({paste(input$test)})
}

shinyApp(ui = ui, server = server)

What I'd like is to click A and have the pickerInput automatically select 1,2,3,4 and 5. Or if we click B, it automatically selects 6,7,8,9, and 10.

Desired output after clicking "A":

enter image description here

Any help is appreciated, thanks.

  • Do you also want to preserve the ability to select individual elements from each list (e.g., selecting 1,3,5 and 6,7,8)? – phalteman Jun 09 '21 at 17:53
  • Yes that would be ideal, I'd like to have the option to either select the entire group, or individual elements. Or for example select the entire group, and then deselect some elements –  Jun 09 '21 at 18:08
  • 1
    I couldn't figure out a way (though someone else still might!), and with a little research, it looks like this has been [asked before](https://stackoverflow.com/questions/55712090/is-there-a-way-to-select-all-of-a-sub-list-with-shiny-selectinput). However, it doesn't appear there's been much progress - the linked Github request is still open, so maybe the advice in the comments still stands. – phalteman Jun 09 '21 at 18:51
  • Thanks for looking, I'll check out that link. Also want to add, I'm open to using other packages or base shiny if there's no way to do it in shinyWidgets. –  Jun 09 '21 at 19:26
  • A promising path... https://stackoverflow.com/questions/60299185/how-to-pass-shinytree-values-to-drop-down-input-in-shiny – phalteman Jun 09 '21 at 19:40
  • For `shinywidgets` open issue is here https://github.com/dreamRs/shinyWidgets/issues/31 . Maybe it is possible with some custom javascript? – Ronak Shah Jun 26 '21 at 05:14

2 Answers2

8

You can use some JS to get the result:

library(shiny)
library(shinyWidgets)

js <- HTML("
$(function() {
  let observer = new MutationObserver(callback);

  function clickHandler(evt) {
    Shiny.setInputValue('group_select', $(this).children('span').text());
  }

  function callback(mutations) {
    for (let mutation of mutations) {
      if (mutation.type === 'childList') {
        $('.dropdown-header').on('click', clickHandler).css('cursor', 'pointer');
        
      }
    }
  }

  let options = {
    childList: true,
  };

  observer.observe($('.inner')[0], options);
})
")

choices <- list("A" = c(1, 2, 3, 4, 5), "B" = c(6, 7, 8, 9, 10))

ui <- fluidPage(
   tags$head(tags$script(js)),
   pickerInput("test", choices = choices, multiple = TRUE),
   textOutput("testOutput")
)

server <- function(input, output, session) {
   output$testOutput <- renderText({paste(input$test)})
   
   observeEvent(input$group_select, {
      req(input$group_select)
      updatePickerInput(session, "test", selected = choices[[input$group_select]])
   })
}

shinyApp(ui = ui, server = server)

Explanation

Idea is that you set an onClick event for the header line, where you set an input variable, upon which you can react in Shiny.

The whole MutationObserver construct is a crude workaround, because I could not get a (delegated) event listener working.

What I observed is that (not bring an JavaScriptspecialist):

  • The content of the dropdown is not generated before the first click. Hence, a direct event listener like $('.dropdown-header').on() woudl not work, because the element is not yet existing.
  • Event delegation a la $(document).on('click', '.dropdown-header', ...) did not work either. I assume that somewhere there is a stopPropagation preventing that the event is bubbling up.

Thus, I used the MutationObserver to add the ('.drodown-header') listener the moment it is created. Not the most beautiful nor a resource preserving solution, but at least a working one. Maybe, you can find out how to properly set the event listener w/o the MutationObsever.


Update

If you want to keep all existing selections, you would change the observeEvent as follows:

observeEvent(input$group_select, {
   req(input$group_select)
   sel <- union(input$test, choices[[input$group_select]])
   updatePickerInput(session, "test", selected = sel)
})


More Background 2022

As this answer was referenced by another question and there was a question in the comments, why we need the MutationObserver in the first place, I finally did look up the source code of the input bootstrap-select.js and my intuition was right, clicks on the .dropdown-header are actively prevented from bubbling up:

this.$menuInner.on('click', '.divider, .dropdown-header', function (e) {
  e.preventDefault();
  e.stopPropagation();
  if (that.options.liveSearch) {
    that.$searchbox.trigger('focus');
  } else {
    that.$button.trigger('focus');
  }
});
thothal
  • 16,690
  • 3
  • 36
  • 71
  • This is interesting. Thanks for the answer. I also noted that selecting either of the headers (i.e., "A" or "B') un-selects any active choices in the other header. Is that due to one of the bullets you noted? I know essentially nothing about JS, so I'm probably missing something basic here... – phalteman Jun 28 '21 at 16:46
  • Well, this is by design. `JavaScript` sends (in this example) eihter `A` or `B` to Shiny. `shiny` decides then to select all elements associated to `A`. If you want another behaviour, it is rather easy to change the `oberserver`... – thothal Jun 28 '21 at 20:12
  • I added an update to show how you would chaneg the code to keep existing selections. – thothal Jun 29 '21 at 06:44
  • 1
    Excellent! This is what I was looking for. I figured there was some way to do it in JS that I was unfamiliar with. I've accepted your answer. As a side note, is there some way to deselect a group the same way? Thank you very much! –  Jun 29 '21 at 18:17
  • 1
    Sure, you can now simply listen to `input$group_select` and do whatever you wan tin your server. It will always return the group label. You may want to change the JS though to `Shiny.setInputValue(..., ..., {priority: "event"}`to make sure `JS` tells `shin` each change. – thothal Jun 29 '21 at 18:30
  • 1
    Thank you! I was able to add deselecting with your advice. added `{priority: \"event\"}` (had to escape the quotation marks). And then replaced the `sel` line with the following: `if (all(choices[[input$group_select]] %in% input$test)) { sel <- input$test[!(input$test %in% choices[[input$group_select]])] } else { sel <- union(input$test, choices[[input$group_select]]) }` Hope that helps others with the same problem –  Jun 29 '21 at 19:40
  • 1
    Works like a charm for one `pickerInput`. Has anybody found a way to get this to work for two different `pickerInput` on different tabs in the same shiny app? Not knowing JS, I cannot really tell from the code where to specify different dropdown menus if that's possible at all. – king_of_limes Aug 24 '22 at 08:09
3

Ok here's a shot at something for your situation using jsTreeR. The code works and does what I think you're looking for, but it's not as pretty as shinyWidgets. I imagine there's a way of combining this approach (largely taken from the jsTreeR example in the documentation), and the approach to create bindings in this post to create something that looks nice and has the functionality you're looking for.

library(shiny)
library(jsTreeR)
library(jsonlite)

#create nodes
nodes <- list(
  list(
    text="List A",
    type="root",
    children = list(
      list(
        text = "Option 1",
        type = "child"
      ),
      list(
        text = "Option 2",
        type = "child"
      ),
      list(
        text = "Option 3",
        type = "child"
      ),
      list(
        text = "Option 4",
        type = "child"
      ),
      list(
        text = "Option 5",
        type = "child"
      )
    )
  ),
  list(
    text="List B",
    type="root",
    children = list(
      list(
        text = "Option 6",
        type = "child"
      ),
      list(
        text = "Option 7",
        type = "child"
      ),
      list(
        text = "Option 8",
        type = "child"
      ),
      list(
        text = "Option 9",
        type = "child"
      ),
      list(
        text = "Option 10",
        type = "child"
      )
    )
  )
)

types <- list(
  root = list(
    icon = "none"
  ),
  child = list(
    icon = "none"
  )
)

#Use in shiny context - example taken from documentation for jsTreeR
ui <- fluidPage(
  br(),
  fluidRow(
    column(width = 4,
           jstreeOutput("jstree")
    ),
    column(width = 4,
           tags$fieldset(
             tags$legend("Selections - JSON format"),
             verbatimTextOutput("treeSelected_json")
           )
    ),
    column(width = 4,
           tags$fieldset(
             tags$legend("Selections - R list"), 
             verbatimTextOutput("treeSelected_R")
           )
    )
  )
)

server <- function(input, output) {
  output[["jstree"]] <- renderJstree(
    jstree(nodes, checkboxes = TRUE, multiple=TRUE, types=types)
  ) 
  
  output[["treeSelected_json"]] <- renderPrint({
    toJSON(input[["jstree_selected"]], pretty = TRUE, auto_unbox = TRUE)
  })
  
  output[["treeSelected_R"]] <- renderPrint({
    input[["jstree_selected"]]
  })
  
}


shinyApp(ui, server)

Note that there's no data attached to the nodes - this just gets the right UI functionality. You'll have to attach values to the nodes that could then be used in downstream calculations.

phalteman
  • 3,442
  • 1
  • 29
  • 46
  • Okay this is interesting and I've voted up your answer. It's almost there, is there a way to put this in a dropdown or remove the folder icons? Ideally I'd like to populate the options by a vector or list, instead of typing in "Option 1","Option 2", etc. This is the closest I've seen, spent a few hours looking at options in both shiny and js. –  Jun 10 '21 at 17:48
  • 1
    Included an edit above to get rid of the folder icons - but getting it to show in a dropdown will probably require working with those shiny bindings as shown in that other answer from @StéphaneLaurent. And I'm sure that an `lapply()` approach could generate the right list structure, but I'm not terribly proficient with that kind of thing. – phalteman Jun 10 '21 at 19:37