This is inspired by and a follow up to Is there a way to select an entire group of choices on a pickerInput from shinyWidgets?
The selected answer works wonderfully for a single pickerInput
, but problems arise as soon as a second ("later") one is in the same app. For example, consider a setup with two pickerInput
:
library(shiny)
library(shinyWidgets)
js <- HTML("
$(function() {
let observer = new MutationObserver(callback);
function clickHandler(evt) {
Shiny.setInputValue('group_select', $(this).children('span').text(),{priority: \"event\"});
}
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), "C" = c(11,12,13,14,15))
ui <- fluidPage(
tags$head(tags$script(js)),
pickerInput("test", choices = choices, multiple = TRUE,options = list('actions-box' = TRUE)),
textOutput("testOutput"),
pickerInput("test_2", choices = choices, multiple = TRUE,options = list('actions-box' = TRUE)),
textOutput("testOutput_2")
)
server <- function(input, output, session) {
output$testOutput <- renderText({paste(input$test)})
output$testOutput_2 <- renderText({paste(input$test_2)})
observeEvent(input$group_select, {
req(input$group_select)
if(all(choices[[input$group_select]] %in% input$test_2)){
sel <- input$test_2[!(input$test_2 %in% choices[[input$group_select]])]
}else{
sel <- union(input$test_2, choices[[input$group_select]])
}
updatePickerInput(session, "test_2", selected = sel)
})
}
shinyApp(ui = ui, server = server)
With this, clicking on a group in the first pickerInput
updates the second one while clicking on a group in the second one does nothing. How do I tell the MutationObserver
to listen for and only for mutations in the second pickerInput
?
In my actual usecase, I have two different pickerInput
on two different tabs (from shinydashboard
) that I'd like to have this functionality for. So maybe it is enough to tell the MutationObserver
on which tab to look? (See How do I use shinyjs to identify if tab is active in shiny? , but being a JS beginner, I am not really sure how I can use this).
UPDATE
I've managed to get ever so slightly closer in my usecase by adding req(input$sidebar == "tab_name")
to the observeEvent
parts. Now they both update the correct pickerInput
, but for the second one, the functionality works only once after every time I've clicked on a group header in the first pickerInput
.
So, still not there. I'm trying to get a reproducible example using tabs from shinydashboard
, but for some reason as soon as I introduce them to the MWE, the whole mutationObserver
stops working altogether.
UPDATE 2
@thothal's answer worked. I rewrote the observeEvent
part to the following to get the same functionality:
observeEvent(input$group_select, {
req(input$group_select)
if(all(choices[[input$group_select[[1]]]] %in% input[[names(input$group_select)]])){
sel <- input[[names(input$group_select)]][!(input[[names(input$group_select)]] %in% choices[[input$group_select[[1]]]])]
}else{
sel <- union(input[[names(input$group_select)]],choices[[input$group_select[[1]]]])
}
updatePickerInput(session,names(input$group_select),selected = sel)
})
Author of the original answer here. Actually, you need to adapt the code as follows:
clickHandler
should return the id of the clicked pickerInput
such that Shiny
can know which pickerInput
to update.observer
must listen on a different node. The class .inner
worked well with fluidPage
layouts as apparently the .inner
class is existing when the DOM is loaded, but not with shinydashboard
(I guess that the parts are not yet loaded when the JS fires). The easiest is to listen to body
(which will be there for sure), but it may be an expensive operation, b/c a narrower target may require less resources.Having said that, a working solution looks like this (N.B. You can add as many pickerInputs
and the handler should work as expected):
library(shiny)
library(shinyWidgets)
library(shinydashboard)
library(purrr)
js <- HTML("
$(function() {
let observer = new MutationObserver(callback);
function clickHandler(evt) {
let id = $(this).parents('.bootstrap-select').children('select').attr('id');
let res = {};
res[id] = $(this).children('span').text();
Shiny.setInputValue('group_select', res);
}
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($('body')[0], options);
})
")
choices <- list("A" = c(1, 2, 3, 4, 5), "B" = c(6, 7, 8, 9, 10))
ui <- dashboardPage(
dashboardHeader(),
dashboardSidebar(pickerInput("test1", choices = choices, multiple = TRUE),
pickerInput("test2", choices = choices, multiple = TRUE)),
dashboardBody(tags$head(tags$script(js)),
verbatimTextOutput("testOutput")),
title = "Multipicker"
)
server <- function(input, output, session) {
output$testOutput <- renderPrint({
input$group_select
})
observeEvent(input$group_select, {
req(input$group_select)
iwalk(input$group_select, ~ updatePickerInput(session, .y, selected = choices[[.x]]))
})
}
shinyApp(ui = ui, server = server)
In order to allow for deselecting upon second click and keeping the selection persistent, we have to make 2 changes:
function clickHandler(evt) {
let id = $(this).parents('.bootstrap-select').children('select').attr('id');
let res = {};
res[id] = $(this).children('span').text();
Shiny.setInputValue('group_select', res, {priority: 'event'}); // change here
}
observer
observeEvent(input$group_select, {
req(input$group_select)
iwalk(input$group_select, function(.x, .y) {
sel <- input[[.y]]
new <- choices[[.x]]
if (length(intersect(sel, new))) {
## unselect already selected items
sel <- setdiff(sel, new)
} else {
sel <- union(sel, new)
}
updatePickerInput(session, .y, selected = sel)
})
})
The behaviour is now that a click will add all the sub-items if (and only if) none of the sub items is already selected. If there is at least one sub-item selected all subitems are deselected. Finally, selections pile up. That is previous selections are not discarded but a new selection is added to the previous.