如果选择取决于另一个输入且服务器 = TRUE,则 shinyStore 无法恢复 selectizeInput 的选定值

shinyStore cannot restore the selected values of the selectizeInput if the choices depend on another input and server = TRUE

这是此问题 () I asked before. I have figured out the answer () 的后续问题。但是,现在我意识到我的回答并不完整。请看下面的代码。这个和我之前的问答是一样的,只是我把第一个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)

简单的解决方案

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

把你的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 背后的东西)是一项client-side 技术,它只能改变开始时存在的东西。如果启动应用程序时未提供选项,则无法更改。这就是它失败的原因。

详细解决方案

如果select2select1上更新了after/based,我们可以在结算select1后从shinystore中取值然后赋值吗到 select2?

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

工作流程如下:

  1. 应用启动,闪亮商店更新select1
  2. 服务器检测到 select1 已更新,更新 select2 的选项
  3. 告诉客户端获取select2的值并从JS发送给R。(在shinyStore中可以通过input$store$xxx(输入ID)访问它。下面,我手动写出代码,向您展示如何将 return client-side 值转换为 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。此处不详述,再说一遍,如果您想了解更多,请阅读上面的link。

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)

我们不必使用自定义 JavaScript 或添加更多依赖项来解决此问题 - input$storeshinyStore's built-in 从 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 顺便说一句。在 shiny 中使用 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.0321 秒到第一个空闲):

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

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

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

配对

Javascript解决方案

原来的答案已经够长了,我不想再添加更多的东西了。如评论中所述,前两个答案使用的是input$store。在此,我为大家提供一个Javascript的解决方案。是的,不需要服务器代码是版本。这是代码,阅读内联注释以了解其工作原理。

// 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,对新手不太友好

因此,这完全取决于您的 real-world 需求。如果您想要一些快速而简短的解决方案,请使用我的第一个 post;如果你关心性能但不想使用 JS,请使用@ismirsehregal post;如果你有一个重量级的应用程序并且想要有很多用户,这个 JS 解决方案可能更好。