--- title: "Changing scenes" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Changing scenes} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` ```{r setup} library(scenes) ``` When a `{shiny}` app loads, it receives a `request` object, with properties such as `QUERY_STRING` (the `?x=1&y=2` part of a url) and `HTTP_COOKIE` (the names and values of cookies). Use `{scenes}` to switch between alternative `{shiny}` UIs depending on properties of that `request` object. ## Why? It's possible to process the `request` using a UI *function,* instead of a standard `shiny::tagList()`. So why not process the `request` through such a function? I created `{scenes}` to write a single login process for the apps I produce for the [R4DS Online Learning Community](https://www.rfordatasci.com/). The goal was to create each UI without having to think about login, and then wrap those UIs in the common login framework. That process became the [`{shinyslack}`](https://github.com/r4ds/shinyslack) package. Perhaps you have your own login process. Or perhaps you want to show completely different UIs to different customer segments visiting the same URL, depending on a cookie or a query parameter. `{scenes}` exists to enables these workflows. ## A toy example Here I'll demonstrate a simple example of changing UIs based on various `request` parameters. You can see a deployed version of this app at . ### The UIs First we'll create four simple UIs. You can ignore the specifics of these UIs for now, but you might want to come back to see how they work once you run the app. In a real `{scenes}` app, these should each be a `shiny::tagList()` UI or UI function. ```{r UIs} # ui1 loads if none of the requirements are met. ui1 <- shiny::tagList( shiny::p("This is UI 1."), shiny::a("Add '?code' to the URL to see UI 2.", href = "?code") ) # ui2 allows us to create the cookie requirement for ui3. ui2 <- cookies::add_cookie_handlers( shiny::tagList( shiny::p("This is UI 2."), shiny::actionButton("cookie_simple", "Store Simple Cookie"), shiny::p("Press the button to see UI 3.") ) ) # ui3 allows us to update that cookie to one that will pass validation. ui3 <- cookies::add_cookie_handlers( shiny::tagList( shiny::p("This is UI 3."), shiny::actionButton("cookie_valid", "Store Valid Cookie"), shiny::p("Press the button to see UI 4.") ) ) # ui4 only loads when everything is all set. It has a button to reset things. ui4 <- cookies::add_cookie_handlers( shiny::tagList( shiny::p("This is UI 4."), shiny::actionButton("reset", "Reset"), shiny::p("Press the button to go back to UI 2.") ) ) ``` We'll use *actions* to decide which of those UIs to display. ### Scenes and actions In `{scenes}`, a `shiny_scene` associates a UI with one or more `scene_actions` that are used to choose it. In this case, we'll display our UIs in these four situations: - Display `ui4` when the user has a particular cookie set and the value of that cookie successfully passes a validation function. - Display `ui3` when the user has that cookie set, but their value doesn't validate. - Display `ui2` when the user has a particular parameter in the URL query string. - Display `ui1` when none of those cases are true. In a real app, this final UI would likely be the login screen, or perhaps an error page. In this toy example, our cookies are "valid" if they have a certain value. That value changes sometimes, so we create a validation function that accepts both the cookie value and the acceptable value. ```{r ui4-validation} our_cookie_validator <- function(cookie_value, acceptable) { cookie_value == acceptable } ``` We wrap `ui4` with the `req_has_cookie()` action, into a `shiny_scene`. ```{r scene4} scene4 <- set_scene( ui4, req_has_cookie( cookie_name = "our_cookie", validation_fn = our_cookie_validator, acceptable = "good value" # We can pass variables through to our validator. ) ) ``` The `shiny_scene` for `ui3` is similar, but we skip the validation. In other words, they must have the cookie set, but we don't care what value it has. ```{r scene3} scene3 <- set_scene( ui3, req_has_cookie( cookie_name = "our_cookie" ) ) ``` For `ui2`, we're looking for a parameter named "code". We don't care what the value is (if we did, we'd pass a vector of acceptable values). ```{r scene2} scene2 <- set_scene( ui2, req_has_query("code") ) ``` Finally, we set up a scene without any actions for our fall-through UI. ```{r scene1} scene1 <- set_scene( ui1 ) ``` ### Scene changes We wrap our scenes together with `change_scene()`. We list the scenes in priority order. ```{r change} ui <- change_scene( scene4, scene3, scene2, scene1 ) ``` We can use this `ui` just like any other `{shiny}` UI. ```{r shiny, eval = FALSE} # Any UI that the user sees will use this # shared server backend. server <- function(input, output, session) { # If they press the button in ui2, save a cookie and reload. shiny::observeEvent( input$cookie_simple, { cookies::set_cookie("our_cookie", "bad value") session$reload() } ) # If they press the button in ui3, save a "valid" cookie and reload. shiny::observeEvent( input$cookie_valid, { cookies::set_cookie("our_cookie", "good value") session$reload() } ) # If they press the reset button in ui4, delete the cookie and reload. shiny::observeEvent( input$reset, { cookies::remove_cookie("our_cookie") session$reload() } ) } shiny::shinyApp( ui = ui, server = server ) ```