簡體   English   中英

"如果選擇依賴於另一個輸入並且 server = TRUE,則 shinyStore 無法恢復 selectizeInput 的選定值"

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

我們不必使用自定義 JavaScript 或添加更多依賴項來解決這個問題 - input$store<\/code>是shinyStore's<\/code>內置方法,用於從localStorage 對象<\/a>檢索數據並為我們提供會話啟動所需的所有信息(而且它已經在@www 在示例代碼中使用)。

shiny 中的會話對象<\/a>為服務器(除其他外)提供客戶端(或瀏覽器)信息 - 例如session$clientData$url_search<\/code>或此處感興趣的: session$input$store<\/code> 。

我們必須確保,當使用updateSelectizeInput<\/code>時,我們嘗試設置的選項在choices<\/code>中可用 - 例如:

不會工作。

此外,我們需要使用freezeReactiveValue<\/code>在會話開始恢復后停止觸發下游的其他觀察者,以避免再次覆蓋更新。

freezeReactiveValue<\/code>順便說一句。 在閃亮中使用update*<\/code>函數時幾乎總是適用的。 請參閱Mastering Shiny<\/em>中的相關章節<\/a>。

### 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$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)
  }, once = TRUE, ignoreInit = FALSE)
  
  observeEvent(input$Select1, {
    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){
      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)

原因

我們要解決的最大問題是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 來檢索值。 它只允許設置不允許獲取 是的,因為如果你了解 html5 localstorage 和 Shiny 的 JS-R 是如何通信的,我們可以編寫自己的 API 來獲取值。

這是工作流程:

  1. 應用啟動,閃亮的商店更新select1
  2. 服務器檢測到select1更新,更新select2的選項
  3. 告訴客戶端獲取select2的值並從 JS 發送到 R
  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 = "bind_1_2"
            )
        )
        # updated 2's choices based on 1
        # we don't update the selected choice yet, because we don't know
        # at this moment
        updateSelectizeInput(session, inputId = "Select2",
                             choices = dat$Letter[dat$Number %in% input$Select1],
                             server = TRUE)

    }, ignoreInit = TRUE)

R-JS 如何通信, 請閱讀此頁面 在這里,我得到的不是Select2而是bind_1_2的存儲值。 這是一個小小的安全機制。 這意味着我使用shinystoreselect1select2的當前值一起保存為插槽。 稍后我們在更新select2選定值時,存儲的select1值必須與當前select1值匹配。 我們要確保 1 和 2 配對。 例如,1 的值為1 -> 它將 a,b,c 與 2 配對。如果我存儲的 2 值為d但當前 1 的值為1 ,那么我們將 2 更新為d是不正確的。 因此,我們必須驗證當前 1 的值(不太可能發生,但以防萬一)。

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值,並將其作為可以直接observe的輸入值發送給 R shiny。 此處不再解釋詳細信息,如果您想了解更多信息,請閱讀上面的鏈接。

4、R

    observeEvent(input$shinystore_bind_1_2, {
        val1_2 <- unlist(strsplit(input$shinystore_bind_1_2, "-"))
        spsComps::onNextInput({
            updateSelectizeInput(
                session, 
                inputId = "Select2", 
                # validation here, if stored 1's value is not same with current 1's
                # value, we don't select anything
                selected = if(val1_2[1]  == input$Select1) val1_2[2] else NULL
            )
        })
    })

你會問什么是spsComps::onNextInput 簡而言之,閃亮的反應式是異步的。 如果我們立即更新 2 的選擇,它不會起作用,因為更新 2 的選擇還沒有完成。 我們必須等到我們更新了 2 的選擇,然后再更新 2 的選擇。 onNextInput是一個等待另一個輸入結算事件的回調。 閱讀本文了解詳情。 它還不是 CRAN 版本的spsComps ,首先從 github remotes::install_github("lz100/spsComps")安裝。

這是完整的代碼。 我還為bind_1_2更改了其他幾行,但核心代碼在上面列出。

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

# Load packages
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 = "bind_1_2"
            )
        )
        # 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_bind_1_2, {
        val1_2 <- unlist(strsplit(input$shinystore_bind_1_2, "-"))
        spsComps::onNextInput({
            updateSelectizeInput(
                session, 
                inputId = "Select2", 
                # validation here, if stored 1's value is not same with current 1's
                # value, we don't select anything
                selected = if(val1_2[1]  == input$Select1) val1_2[2] else NULL
            )
        })
    })
    
    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))
            updateStore(session, name = "bind_1_2", paste0(isolate(input$Select1), "-", 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)
            updateStore(session, name = "bind_1_2", NULL)
        }
    })
}

shinyApp(ui, server)

在此處輸入圖像描述

Javascript 解決方案<\/h4>

原始答案足夠長,我不想在那里添加更多內容。 正如評論中提到的,前兩個答案是使用input$store<\/code> 。 在這里,我為您提供了一個純<\/strong>Javascript 的解決方案。 是的,不需要服務器代碼版本。 這是代碼,閱讀內聯注釋以了解它是如何工作的。

我們沒有向服務器添加代碼,而是刪除了 2 個冗余觀察者。 這是完整的代碼。

在性能方面,與前 2 個沒有明顯差異,但如前所述,稍后可能對一些重度應用程序有用。 好消息是這段代碼在客戶端運行。 它運行的速度主要取決於您用戶的計算機,這減少了您的服務器負擔,只是一點點,但對於每個用戶來說,一點點可以總結為一個很大的數字。 想象一下,成千上萬的人同時使用該應用程序。 不好的是需要學JS,對初學者不太友好。

因此,這完全取決於您的實際需求。 如果您想要一些快速而簡短的解決方案,請使用我的第一篇文章; 如果您關心性能但不想使用 JS,請使用@ismirsehregal post; 如果你有一個很重的應用程序並且想要有很多用戶,這個 JS 解決方案可能會更好。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM