【问题标题】:shinyStore cannot restore the selected values of the selectizeInput if the choices depend on another input and server = TRUE如果选择依赖于另一个输入并且 server = TRUE,则 shinyStore 无法恢复 selectizeInput 的选定值
【发布时间】:2021-09-18 07:29:36
【问题描述】:

这是我之前问过的这个问题 (shinyStore cannot restore the selected values of the selectizeInput if the choices are depends on another input) 的后续问题。我已经找到了答案(https://stackoverflow.com/a/68290227/7669809)。但是,现在我意识到我的答案并不完整。请看下面的代码。这个和我之前的问答是一样的,只是我给第一个updateSelectizeInput设置了server = TRUE,导致本地存储不工作。如果我可以使用server = TRUE,那就太好了,因为在我的真实示例中,selectizeInput 的选择很多。

### This script creates an example of the shinystore package

# Load packages
library(shiny)
library(shinyStore)

ui <- fluidPage(
  headerPanel("shinyStore Example"),
  sidebarLayout(
    sidebarPanel = sidebarPanel(
      initStore("store", "shinyStore-ex1"),
      selectizeInput(inputId = "Select1", label = "Select A Number",
                     choices = as.character(1:3),
                     options = list(
                       placeholder = 'Please select a number',
                       onInitialize = I('function() { this.setValue(""); }'),
                       create = TRUE
                     ))
    ),
    mainPanel = mainPanel(
      fluidRow(
        selectizeInput(inputId = "Select2", 
                       label = "Select A Letter",
                       choices = character(0),
                       options = list(
                         placeholder = 'Please select a number in the sidebar first',
                         onInitialize = I('function() { this.setValue(""); }'),
                         create = TRUE
                       )),
        actionButton("save", "Save", icon("save")),
        actionButton("clear", "Clear", icon("stop"))
      )
    )
  )
)

server <- function(input, output, session) {
  
  dat <- data.frame(
    Number = as.character(rep(1:3, each = 3)),
    Letter = letters[1:9]
  )
  
  observeEvent(input$Select1, {
    updateSelectizeInput(session, inputId = "Select2", 
                         choices = dat$Letter[dat$Number %in% input$Select1],
                         # Add server = TRUE make the local storage not working
                         server = TRUE)
  }, ignoreInit = TRUE)
  
  observe({
    if (input$save <= 0){
      updateSelectizeInput(session, inputId = "Select1", selected = isolate(input$store)$Select1)
    }
  })
  
  observe({
    if (input$save <= 0){
      req(input$Select1)
      updateSelectizeInput(session, inputId = "Select2", selected = isolate(input$store)$Select2)
    }
  })
  
  observe({
    if (input$save > 0){
      updateStore(session, name = "Select1", isolate(input$Select1))
      updateStore(session, name = "Select2", isolate(input$Select2))
    }
  })

  observe({
    if (input$clear > 0){
      updateSelectizeInput(session, inputId = "Select1",
                           options = list(
                             placeholder = 'Please select a number',
                             onInitialize = I('function() { this.setValue(""); }'),
                             create = TRUE
                           ))
      updateSelectizeInput(session, inputId = "Select2",
                           choices = character(0),
                           options = list(
                             placeholder = 'Please select a number in the sidebar first',
                             onInitialize = I('function() { this.setValue(""); }'),
                             create = TRUE
                           ))

      updateStore(session, name = "Select1", NULL)
      updateStore(session, name = "Select2", NULL)
    }
  })
}

shinyApp(ui, server)

【问题讨论】:

    标签: r shiny shinystore


    【解决方案1】:

    简单的解决方案

    最简单的解决方案之一是:

    只需将您的 observeEvent(input$Select1, ... 观察者更改为这个

        once_flag <- reactiveVal(TRUE)
        observeEvent(input$Select1, {
            updateSelectizeInput(
                session, inputId = "Select2", server = TRUE,
                choices = dat$Letter[dat$Number %in% input$Select1], 
                selected = if(once_flag()) input$store$Select2 else NULL
            )
            once_flag(FALSE)
        }, ignoreInit = TRUE)
    

    你就完成了。多么简单!一切都在同一个observeEvent 中解决,只需调用一次更新。 once_flag 是为了确保只设置一次Select2 值。第二次以及当你更改Select1 时,我们不会设置Select2 的值。

    感谢@ismirsehregal 等其他用户的更正,所以我可以想出上面的简单解决方案。由于shinyStore 直接通过input$store 为您提供了这个包装器API,因此您不需要像我在下面那样编写API,但工作流程和背后的内容是相同的。如果您对此解决方案的工作原理感兴趣,请继续阅读。

    原答案

    原因

    我们要解决的最大问题是server = TRUE,阅读帮助文件我们知道store choices on the server-side, and load the select options dynamically on searching。这意味着您的客户 (UI) 一开始并不知道有哪些选项。 HTML5 localstore(shinystore 背后的东西)是一种客户端技术,它只能改变一开始就存在的东西。如果启动应用程序时未提供选项,则无法更改。这就是它失败的原因。

    详细解决方案

    如果select2是在select1之后/基于select1更新的,我们可以在解决select1之后从shinystore检索值,然后将值分配给select2吗?

    答案是否定的。 没有,因为原始的shinystore 没有为您提供任何用于 R-Javascript 通信的 API 来检索值。它只允许 setnot get(不正确,请参阅 cmets,但下面有助于了解 shinystore 的工作原理)。是的,因为如果你了解 html5 localstorage 和 Shiny 的 JS-R 是如何通信的,我们可以编写自己的 API 来获取值。

    这是工作流程:

    1. 应用启动,闪亮的商店更新select1
    2. 服务器检测到select1 更新,更新select2 的选项
    3. 告诉客户端获取select2的值并从JS发送到R。(在shinyStore你可以通过input$store$xxx访问它(输入ID)。下面我手动写代码给你看他们如何将客户端值返回到 input 值。)
    4. 获取 R 中的值并更新 select2 的选择

    让我们看看它在代码中是如何工作的:

    1-2,R

        observeEvent(input$Select1, {
            # detect 1 changed 
            # send signal to client to get stored 2's value
            session$sendCustomMessage(
                "shinyStore_getvalue",
                list(
                    namespace = "shinyStore-ex1",
                    key = "Select2"
                )
            )
            # updated 2's choices based on 1
            updateSelectizeInput(session, inputId = "Select2",
                                 choices = dat$Letter[dat$Number %in% input$Select1],
                                 server = TRUE)
            
        }, ignoreInit = TRUE)
    

    R-JS 如何通信,read this page

    3、JS

            Shiny.addCustomMessageHandler('shinyStore_getvalue', function(data) {
                var val = localStorage.getItem(`${data.namespace}\\${data.key}`);
                if(val === null) return false;
                val = JSON.parse(val);
                if(val.data === undefined) return false;
                Shiny.setInputValue(`shinystore_${data.key}`, val.data);
            });
    

    获取查询到的shinystore值并将其作为输入值发送给R shiny,可以直接为observe。此处不再详细说明,如果您想了解更多信息,请阅读上面的链接。

    4,R

        observeEvent(input$shinystore_Select2, {
            updateSelectizeInput(
                session, 
                inputId = "Select2", 
                choices = dat$Letter[dat$Number %in% input$Select1],
                server = TRUE,
                selected = input$shinystore_Select2
            )
        }, once = TRUE)
    

    添加once只设置一次值。

    详细解决方案的完整代码

    library(shiny)
    library(shinyStore)
    ui <- fluidPage(
        headerPanel("shinyStore Example"),
        tags$script(HTML(
            '
            Shiny.addCustomMessageHandler(\'shinyStore_getvalue\', function(data) {
                var val = localStorage.getItem(`${data.namespace}\\\\${data.key}`);
                if(val === null) return false;
                val = JSON.parse(val);
                if(val.data === undefined) return false;
                Shiny.setInputValue(`shinystore_${data.key}`, val.data);
            });
            '
        )),
        sidebarLayout(
            sidebarPanel = sidebarPanel(
                initStore("store", "shinyStore-ex1"),
                selectizeInput(inputId = "Select1", label = "Select A Number",
                               choices = as.character(1:3),
                               options = list(
                                   placeholder = 'Please select a number',
                                   onInitialize = I('function() { this.setValue(""); }'),
                                   create = TRUE
                               ))
            ),
            mainPanel = mainPanel(
                fluidRow(
                    selectizeInput(inputId = "Select2", 
                                   label = "Select A Letter",
                                   choices = character(0),
                                   options = list(
                                       placeholder = 'Please select a number in the sidebar first',
                                       onInitialize = I('function() { this.setValue(""); }'),
                                       create = TRUE
                                   )),
                    actionButton("save", "Save", icon("save")),
                    actionButton("clear", "Clear", icon("stop"))
                )
            )
        )
    )
    
    server <- function(input, output, session) {
        
        dat <- data.frame(
            Number = as.character(rep(1:3, each = 3)),
            Letter = letters[1:9]
        )
        
        observeEvent(input$Select1, {
            # detect 1 changed 
            # send signal to client to get stored 2's value
            session$sendCustomMessage(
                "shinyStore_getvalue",
                list(
                    namespace = "shinyStore-ex1",
                    key = "Select2"
                )
            )
            # # updated 2's choices based on 1
            updateSelectizeInput(session, inputId = "Select2",
                                 choices = dat$Letter[dat$Number %in% input$Select1],
                                 server = TRUE)
            
        }, ignoreInit = TRUE)
        
        observeEvent(input$shinystore_Select2, {
            updateSelectizeInput(
                session, 
                inputId = "Select2", 
                choices = dat$Letter[dat$Number %in% input$Select1],
                server = TRUE,
                selected = input$shinystore_Select2
            )
        }, once = TRUE)
        
        observe({
            if (input$save <= 0){
                updateSelectizeInput(session, inputId = "Select1", selected = isolate(input$store)$Select1)
            }
        })
        
        observe({
            if (input$save <= 0){
                req(input$Select1)
                updateSelectizeInput(session, inputId = "Select2", selected = isolate(input$store)$Select2)
            }
        })
        
        observe({
            if (input$save > 0){
                updateStore(session, name = "Select1", isolate(input$Select1))
                updateStore(session, name = "Select2", isolate(input$Select2))
            }
        })
        
        observe({
            if (input$clear > 0){
                updateSelectizeInput(session, inputId = "Select1",
                                     options = list(
                                         placeholder = 'Please select a number',
                                         onInitialize = I('function() { this.setValue(""); }'),
                                         create = TRUE
                                     ))
                updateSelectizeInput(session, inputId = "Select2",
                                     choices = character(0),
                                     options = list(
                                         placeholder = 'Please select a number in the sidebar first',
                                         onInitialize = I('function() { this.setValue(""); }'),
                                         create = TRUE
                                     ))
                updateStore(session, name = "Select1", NULL)
                updateStore(session, name = "Select2", NULL)
            }
        })
    }
    
    shinyApp(ui, server)
    

    【讨论】:

    • 无意冒犯@lz100,您为闪亮社区做得很好-但是,我想指出您通过Shiny.setInputValue 传递给闪亮的服务器功能的localStorage 数据@ 987654357@ 电话是多余的。可以通过input$store 访问它。因此,您关于shinyStore 的陈述:“它只允许设置,不允许获取” 是不正确的。
    • @ismirsehregal 很好。你是对的,我阅读了 shinyStore 导出的函数,但没有找到 get 函数,所以我认为没有。通常,当您设计 set 函数时,您希望将其与 get 函数配对,但他们以不同的方式做到了。无论如何,谢谢你,我更新了答案并设法在同一个observeEvent内完成所有事情!
    • 谢谢,我知道,但这不一样。我称之为部分once。我们需要更新事件重复运行,但selected 参数只运行一次。当用户更改Select1 时,我们稍后仍需要此更新事件来更新Select2。试试我的代码,你会看到什么是“部分”。
    【解决方案2】:

    我们不必使用自定义 JavaScript 或添加更多依赖项来解决此问题 - input$storeshinyStore's 的内置方法,用于从 localStorage object 检索数据并为我们提供会话启动所需的所有信息(在示例代码中,@www 已经在使用它)。

    shiny 中的 session object 为服务器(除其他外)提供客户端(或浏览器)信息 - 例如。 session$clientData$url_search 或感兴趣的:session$input$store

    当使用updateSelectizeInput 时,我们必须确保我们尝试设置的选项在choices 中可用 - 例如像这样:

    updateSelectizeInput(session, inputId = "myID", selected = 12, choices = 1:10)

    不会工作。

    此外,我们需要使用freezeReactiveValue在会话开始恢复后停止触发下游的其他观察者,以避免再次覆盖更新。

    freezeReactiveValue 顺便说一句。在闪亮中使用update* 函数时几乎总是适用的。请参阅 Mastering Shiny 中的this related chapter

    ### This script creates an example of the shinystore package
    
    # Load packages
    library(shiny)
    library(shinyStore)
    
    ui <- fluidPage(
      headerPanel("shinyStore Example"),
      sidebarLayout(
        sidebarPanel = sidebarPanel(
          initStore("store", "shinyStore-ex1"),
          selectizeInput(inputId = "Select1", label = "Select A Number",
                         choices = as.character(1:3),
                         options = list(
                           placeholder = 'Please select a number',
                           onInitialize = I('function() { this.setValue(""); }'),
                           create = TRUE
                         ))
        ),
        mainPanel = mainPanel(
          fluidRow(
            selectizeInput(inputId = "Select2", 
                           label = "Select A Letter",
                           choices = character(0),
                           options = list(
                             placeholder = 'Please select a number in the sidebar first',
                             onInitialize = I('function() { this.setValue(""); }'),
                             create = TRUE
                           )),
            actionButton("save", "Save", icon("save")),
            actionButton("clear", "Clear", icon("stop"))
          )
        )
      )
    )
    
    server <- function(input, output, session) {
      
      dat <- data.frame(
        Number = as.character(rep(1:3, each = 3)),
        Letter = letters[1:9]
      )
      
      storeInit <- observeEvent(input$store, {
        freezeReactiveValue(input, "Select1") # required
        freezeReactiveValue(input, "Select2") # not required but should be used before calling any update function which isn't intended to trigger further reactives
        updateSelectizeInput(session, inputId = "Select1", selected = input$store$Select1)
        updateSelectizeInput(session, inputId = "Select2", selected = input$store$Select2, choices = dat$Letter[dat$Number %in% input$store$Select1], server = TRUE)
        storeInit$destroy() # destroying observer, as it is only needed once per session
      }, once = TRUE, ignoreInit = FALSE)
      
      observeEvent(input$Select1, {
        freezeReactiveValue(input, "Select2") # not required but good practice
        updateSelectizeInput(session, inputId = "Select2", 
                             choices = dat$Letter[dat$Number %in% input$Select1],
                             server = TRUE)
      }, ignoreInit = TRUE)
      
      observe({
        if (input$save > 0){
          updateStore(session, name = "Select1", isolate(input$Select1))
          updateStore(session, name = "Select2", isolate(input$Select2))
        }
      })
      
      observe({
        if (input$clear > 0){
          freezeReactiveValue(input, "Select1") # not required but good practice
          freezeReactiveValue(input, "Select2") # not required but good practice
          updateSelectizeInput(session, inputId = "Select1",
                               options = list(
                                 placeholder = 'Please select a number',
                                 onInitialize = I('function() { this.setValue(""); }'),
                                 create = TRUE
                               ))
          updateSelectizeInput(session, inputId = "Select2",
                               choices = character(0),
                               options = list(
                                 placeholder = 'Please select a number in the sidebar first',
                                 onInitialize = I('function() { this.setValue(""); }'),
                                 create = TRUE
                               ))
          
          updateStore(session, name = "Select1", NULL)
          updateStore(session, name = "Select2", NULL)
        }
      })
    }
    
    shinyApp(ui, server)
    


    编辑:给出答案的比较

    现在@lz100 也使用input$store 而不是Shiny.addCustomMessageHandler,这两个答案彼此近似。 归结为在@lz100 的更新答案(once_flag)中使用reactiveVal,在我的答案中使用freezeReactiveValue

    我想指出为什么我认为使用freezeReactiveValue 是更简洁的方法:

    once_flag-approachinput$Select1 更新后触发(observeEvent 参数ignoreInit = TRUE)并间接依赖于input$store。所有其他依赖于input$Select1 的观察者都被不必要地触发了两次(第一次在初始化时,第二次在更新时)。

    这是相应的反应日志(0.0321s 到第一次空闲):

    once_flag-方法的另一个缺陷(就目前而言)是observeEvent 将在每次更改input$Select1 时触发,即使没有正在进行恢复(返回NULL 但浪费资源)。

    freezeReactiveValue-approach 在第一次调用应用程序 (once = TRUE, ignoreInit = FALSE) 时直接监听 input$store 的变化,防止下游触发,这稍微快一些(0.0212 秒到第一次空闲) :

    随着应用程序的增长,这些影响可能与初始化时间更相关 - 因此,我支持上面链接的建议,将 update* 函数与 freezeReactiveValue 配对。

    【讨论】:

    • 很好的挖掘,非常棒的工作。但是,原始代码中存在两次触发的东西。每次让它开火两次不是我的补充。 OP 询问如何使server=TRUE 工作,所以我尝试使其成为一个简短易读的解决方案,并没有过多考虑性能。费用在observe ... req(input$Select1) 中,我们可以在第一次之后调用suspend 方法禁用它,然后费用就没有了,还有其他方法可以修复它。由于OP没有要求这个,我没有触及这部分。对我来说,0.02s 是一个可以接受的成本。
    • @lz100 在我上面的编辑中,我已经试图强调,在这种特殊情况下,使用freezeReactiveValue 的速度增益并不大,但它可能会在以后得到回报。这是关于良好的编程实践,在这种情况下,它是解决问题的一种可能方法的一部分。我们使用 input$store 与自定义 JS 的答案的主要区别是更大的事情,这让我发布了另一个答案。
    • 知道了,就像你说的input$store vs JS 更大,所以我想也许我们可以通过纯 JS 来解决它,哈哈。请参阅此线程的第三篇文章。不需要服务器代码。我测试了性能,但没有发现任何差异。
    【解决方案3】:

    Javascript 解决方案

    原始答案已经够长了,我不想在那里添加更多内容。如 cmets 中所述,前两个答案使用input$store。在这里,我为您提供了一个 Javascript 解决方案。是的,不需要服务器代码版本。这是代码,请阅读内联 cmets 以了解其工作原理。

    // get shinyStore value
    function getStore(namespace, key) {
        var val = localStorage.getItem(`${namespace}\\${key}`);
        if(val === null) return false;
        val = JSON.parse(val);
        if(val.data === undefined) return false;
        return val.data;
    }
    
    $(function(){
        var s1 = $('#Select1');
        var s2 = $('#Select2');
        var s1Stored = getStore('shinyStore-ex1', 'Select1');
        var s2Stored = getStore('shinyStore-ex1', 'Select2');
        // If select1 is set we continue, otherwise stop
        if(s1Stored !== false) {
            // Listen to shiny select1 init event
            // Here we use `one` listener to make sure it only runs one time
            s1.one('shiny:bound', function(){
                var s1Binding = s1.data('shiny-input-binding');
                // set select1 value to our stored value
                s1Binding.setValue(this, s1Stored);
            });
        }
        // if select2 is stored we, continue to set select2
        if(s2Stored !== false) {
            // Here we use 2 `one` nested change listener, select2 is a little trickier. 
            // the first time value change is due Shiny init. The second time is we changed select1 and serverside
            // sends new choices to client, caused the default to set. 
            // We can change to the stored value afterwards,
            // so we set the value exactly after the second time.
            s2.one('change', function(){
                s2.one('change', function(){
                    var s2Binding = s2.data('shiny-input-binding');
                    s2Binding.setValue(s2.get(0), s2Stored);
                });
            });
        }
    });
    
    

    我们没有向服务器添加代码,而是删除了 2 个冗余观察者。这是完整的代码。

    ### This script creates an example of the shinystore package
    
    # Load packages
    library(shiny)
    library(shinyStore)
    myscript <- tags$script(HTML("
    function getStore(namespace, key) {
        var val = localStorage.getItem(`${namespace}\\\\${key}`);
        if(val === null) return false;
        val = JSON.parse(val);
        if(val.data === undefined) return false;
        return val.data;
    }
    
    $(function(){
        var s1 = $('#Select1');
        var s2 = $('#Select2');
        var s1Stored = getStore('shinyStore-ex1', 'Select1');
        var s2Stored = getStore('shinyStore-ex1', 'Select2');
        if(s1Stored !== false) {
            s1.one('shiny:bound', function(){
                var s1Binding = s1.data('shiny-input-binding');
                s1Binding.setValue(this, s1Stored);
            });
        }
        if(s2Stored !== false) {
            s2.one('change', function(){
                s2.one('change', function(){
                    var s2Binding = s2.data('shiny-input-binding');
                    s2Binding.setValue(s2.get(0), s2Stored);
                });
            });
        }
    });
    
    "))
    
    ui <- fluidPage(
        headerPanel("shinyStore Example"),
        sidebarLayout(
            sidebarPanel = sidebarPanel(
                initStore("store", "shinyStore-ex1"),
                selectizeInput(inputId = "Select1", label = "Select A Number",
                               choices = as.character(1:3),
                               options = list(
                                   placeholder = 'Please select a number',
                                   onInitialize = I('function() { this.setValue(""); }'),
                                   create = TRUE
                               ))
            ),
            mainPanel = mainPanel(
                fluidRow(
                    selectizeInput(inputId = "Select2", 
                                   label = "Select A Letter",
                                   choices = character(0),
                                   options = list(
                                       placeholder = 'Please select a number in the sidebar first',
                                       onInitialize = I('function() { this.setValue(""); }'),
                                       create = TRUE
                                   )),
                    actionButton("save", "Save", icon("save")),
                    actionButton("clear", "Clear", icon("stop"))
                )
            )
        ),
        myscript
    )
    
    server <- function(input, output, session) {
        
        dat <- data.frame(
            Number = as.character(rep(1:3, each = 3)),
            Letter = letters[1:9]
        )
        
        observeEvent(input$Select1, {
            updateSelectizeInput(session, inputId = "Select2", 
                                 choices = dat$Letter[dat$Number %in% isolate(input$Select1)],
                                 # Add server = TRUE make the local storage not working
                                 server = T)
        }, ignoreInit = TRUE)
        
        observe({
            if (input$save > 0){
                updateStore(session, name = "Select1", isolate(input$Select1))
                updateStore(session, name = "Select2", isolate(input$Select2))
            }
        })
        
        observe({
            if (input$clear > 0){
                updateSelectizeInput(session, inputId = "Select1",
                                     options = list(
                                         placeholder = 'Please select a number',
                                         onInitialize = I('function() { this.setValue(""); }'),
                                         create = TRUE
                                     ))
                updateSelectizeInput(session, inputId = "Select2",
                                     choices = character(0),
                                     options = list(
                                         placeholder = 'Please select a number in the sidebar first',
                                         onInitialize = I('function() { this.setValue(""); }'),
                                         create = TRUE
                                     ))
                
                updateStore(session, name = "Select1", NULL)
                updateStore(session, name = "Select2", NULL)
            }
        })
    }
    
    shinyApp(ui, server)
    

    在性能方面,与前 2 个没有明显区别,但如前所述,稍后可能对一些重度应用程序有用。好消息是这段代码在客户端运行。它运行的速度主要取决于您用户的计算机,这减少了您的服务器负担,只是一点点,但对于每个用户来说,一点点可以总结为一个很大的数字。想象一下,成千上万的人同时使用该应用程序。不好的是需要学JS,对初学者不太友好。

    所以,这完全取决于您的实际需求。如果您想要一些快速而简短的解决方案,请使用我的第一篇文章;如果您关心性能但不想使用 JS,请使用@ismirsehregal post;如果你有一个很重的应用程序并希望拥有大量用户,那么这个 JS 解决方案可能会更好。

    【讨论】:

      猜你喜欢
      • 2021-09-16
      • 1970-01-01
      • 1970-01-01
      • 2020-10-09
      • 2012-01-30
      • 2019-03-08
      • 2022-11-14
      • 1970-01-01
      • 2014-02-21
      相关资源
      最近更新 更多