For part of a Shiny application I am building, I need to have the user select a directory. The directory path is stored in a reactive variable. The directory can either be selected by the user from a file window or the path can be manually entered by textInput
. I have figured out how to do this, but I don't understand why the solution I have works! A minimal example of the working app:
library(shiny) ui <- fluidPage( actionButton("button1", "First Button"), textInput("inText", "Input Text"), actionButton("button2", "Second Button"), textOutput("outText"), textOutput("outFiles") ) server <- function(input, output) { values <- reactiveValues(inDir = NULL) observeEvent(input$button1, {values$inDir <- tcltk::tk_choose.dir()}) observeEvent(input$button2, {values$inDir <- input$inText}) inPath <- eventReactive(values$inDir, {values$inDir}) output$outText <- renderText(inPath()) fileList <- reactive(list.files(path=inPath())) output$outFiles <- renderPrint(fileList()) } shinyApp(ui, server)
The first thing I tried was to just use eventReactive
and assign the two sources of input to the reactive variable:
server <- function(input, output) { inPath <- eventReactive(input$button1, {tcltk::tk_choose.dir()}) inPath <- eventReactive(input$button2, {input$inText}) output$outText <- renderText(inPath()) fileList <- reactive(list.files(path=inPath())) output$outFiles <- renderPrint(fileList()) }
The effect of this as far as I can tell is that only one of the buttons does anything. What I don't really understand is why this doesn't work. What I thought would happen is that the first button pushed would create inPath
and then subsequent pushes would update the value and trigger updates to dependent values (here output$outText
). What exactly is happening here then?
The second thing I tried, which was almost there, was based off of this answer:
server <- function(input, output) { values <- reactiveValues(inDir = NULL) observeEvent(input$button1, {values$inDir <- tcltk::tk_choose.dir()}) observeEvent(input$button2, {values$inDir <- input$inText}) inPath <- reactive({if(is.null(values$inDir)) return() values$inDir}) output$outText <- renderText(inPath()) fileList <- reactive(list.files(path=inPath())) output$outFiles <- renderPrint(fileList()) }
This works correctly except that it shows an "Error: invalid 'path' argument" message for list.files
. I think this may mean that fileList
is being evaluated with inPath = NULL
. Why does this happen when I use reactive
instead of eventReactive
?
Thanks!
1 Answers
Answers 1
You could get rid of the inPath
reactive and just use values$inDir
instead. With req()
you'll wait until values are available. Otherwise you'll get the same error (invalid 'path' argument).
The reactive
triggers right away, while the eventReactive
will wait until the given event occurs and the eventReactive is called.
And if(is.null(values$inDir)) return()
won't work correctly, as it will return NULL if values$inDir
is NULL, which is then passed to list.files
. And list.files(NULL)
gives the error: invalid 'path' argument
.
Replace it with req(values$inDir)
and you won't get that error.
And your example with 2 inPath
- eventReactive's won't work, as the first one will be overwritten by the second one, so input$button1
won't trigger anything.
library(shiny) ui <- fluidPage( actionButton("button1", "First Button"), textInput("inText", "Input Text"), actionButton("button2", "Second Button"), textOutput("outText"), textOutput("outFiles") ) server <- function(input, output) { values <- reactiveValues(inDir = NULL) observeEvent(input$button1, {values$inDir <- tcltk::tk_choose.dir()}) observeEvent(input$button2, {values$inDir <- input$inText}) output$outText <- renderText(values$inDir) fileList <- reactive({ req(values$inDir); list.files(path=values$inDir) }) output$outFiles <- renderPrint(fileList()) } shinyApp(ui, server)
You could also use an eventReactive
for button1
and an observeEvent
for button2
, but note that you need an extra observe({ inPath() })
to make it work. I prefer the above solution, as it is more clear what's happening and also less code.
server <- function(input, output) { values <- reactiveValues(inDir = NULL) inPath = eventReactive(input$button1, {values$inDir <- tcltk::tk_choose.dir()}) observe({ inPath() }) observeEvent(input$button2, {values$inDir <- input$inText}) output$outText <- renderText(values$inDir) fileList <- reactive({ req(values$inDir); list.files(path=values$inDir) }) output$outFiles <- renderPrint(fileList()) }
And to illustrate why if(is.null(values$inDir)) return()
won't work, consider the following function:
test <- function() { if (TRUE) return() } test()
Although the if-condition evaluates to TRUE, there is still gonna be a return value (in this case NULL), which will be passed on to the following functions and in your case list.files
, which will cause the error.
0 comments:
Post a Comment