你能告诉一个MutationObserver只监听特定的输入吗?在pickerInput中选择整个选项组?

vhipe2zx  于 2023-09-27  发布在  其他
关注(0)|答案(1)|浏览(74)

这是受Is there a way to select an entire group of choices on a pickerInput from shinyWidgets?的启发,也是它的后续
所选的答案对于单个pickerInput非常有效,但是当第二个(“稍后”)答案出现在同一个应用程序中时,问题就会出现。例如,假设有两个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)

这样,点击第一个pickerInput中的一个组会更新第二个组,而点击第二个组则什么也不做。如何告诉MutationObserver只监听第二个pickerInput中的突变?
在我的实际用例中,我在两个不同的选项卡上有两个不同的pickerInput(从shinydashboard开始),我希望有这个功能。因此,也许告诉MutationObserver在哪个选项卡上查看就足够了?(参见How do I use shinyjs to identify if tab is active in shiny?,但作为一个JS初学者,我真的不知道如何使用它)。

更新

在我的用例中,我已经通过将req(input$sidebar == "tab_name")添加到observeEvent部分来设法稍微接近。现在它们都更新了正确的pickerInput,但是对于第二个,每次我点击第一个pickerInput中的组头后,该功能只工作一次。
所以,还是不行。我试图使用shinydashboard中的制表符得到一个可重现的示例,但由于某种原因,当我将它们引入MWE时,整个mutationObserver完全停止工作。

更新2

@thothal的回答奏效了。我将observeEvent部分重写为以下内容以获得相同的功能:

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)
  })
pod7payv

pod7payv1#

原创答案的作者在这里。实际上,你需要调整代码如下:
1.您的clickHandler应该返回单击的pickerInput的id,以便Shiny可以知道要更新哪个pickerInput
1.您的observer必须侦听不同的节点。类.innerfluidPage布局中工作得很好,因为显然.inner类在DOM加载时就存在了,但在shinydashboard不存在(我猜当JS触发时,这些部分还没有加载)。最简单的方法是监听body(它肯定会在那里),但这可能是一个昂贵的操作,b/c更窄的目标可能需要更少的资源。
话虽如此,一个工作解决方案看起来像这样(N.B. 您可以添加尽可能多的pickerInputs,处理程序应该像预期的那样工作):

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)

更新

为了允许在第二次单击时取消选择并保持选择持久,我们必须进行2次更改:
1.告诉JS发送每次点击(而不仅仅是新的点击):

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
}

1.适配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)
      })
})

现在的行为是,当(且仅当)没有选择任何子项时,单击将添加所有子项。如果至少选择了一个子项,则取消选择所有子项。最后,选择堆积起来。也就是说,以前的选择不会被丢弃,而是将新的选择添加到以前的选择中。

相关问题