如果选择取决于另一个输入且服务器 = 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 技术,它只能改变开始时存在的东西。如果启动应用程序时未提供选项,则无法更改。这就是它失败的原因。
详细解决方案
如果select2
在select1
上更新了after/based,我们可以在结算select1
后从shinystore
中取值然后赋值吗到 select2
?
答案是否定的。 否,因为原始 shinystore
没有为您提供任何 API 供 R-Javascript 通信以检索值。它只允许set,not get(不正确,看评论,但下面有助于理解shinystore的工作原理) .是的,是因为如果你了解 html5 localstorage 和 Shiny 的 JS-R 通信方式,我们可以编写自己的 API 来获取值。
工作流程如下:
- 应用启动,闪亮商店更新
select1
- 服务器检测到
select1
已更新,更新 select2
的选项
- 告诉客户端获取
select2
的值并从JS发送给R。(在shinyStore
中可以通过input$store$xxx
(输入ID)访问它。下面,我手动写出代码,向您展示如何将 return client-side 值转换为 input
值。)
- 获取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$store
是 shinyStore'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
-approach 在 input$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 解决方案可能更好。
这是此问题 (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 技术,它只能改变开始时存在的东西。如果启动应用程序时未提供选项,则无法更改。这就是它失败的原因。
详细解决方案
如果select2
在select1
上更新了after/based,我们可以在结算select1
后从shinystore
中取值然后赋值吗到 select2
?
答案是否定的。 否,因为原始 (不正确,看评论,但下面有助于理解shinystore的工作原理) .是的,是因为如果你了解 html5 localstorage 和 Shiny 的 JS-R 通信方式,我们可以编写自己的 API 来获取值。shinystore
没有为您提供任何 API 供 R-Javascript 通信以检索值。它只允许set,not get
工作流程如下:
- 应用启动,闪亮商店更新
select1
- 服务器检测到
select1
已更新,更新select2
的选项 - 告诉客户端获取
select2
的值并从JS发送给R。(在shinyStore
中可以通过input$store$xxx
(输入ID)访问它。下面,我手动写出代码,向您展示如何将 return client-side 值转换为input
值。) - 获取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$store
是 shinyStore'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
-approach 在 input$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 解决方案可能更好。