mjfrigaard / shiny-app-pkgs

Shiny app-packages (book)
https://mjfrigaard.github.io/shiny-app-pkgs/
MIT License
15 stars 0 forks source link

Create section on complex storage / bookmarking #12

Open mjfrigaard opened 10 months ago

mjfrigaard commented 10 months ago

"Did you have a section on complex storage / bookmarking? Would love to see others strategies with that."

tsolloway commented 10 months ago

So maybe two separate issues... on the board it sounded like we wanted to talk about shiny components, which we'd put into a package. I just note this point, because there's often emphasis to write analytic pkgs, but shiny function pkgs are just as useful.

Pkgs

Simple functions

Let's say we want to initialize an input disabled, rather than disable it with something like shinyjs::disable after it's been initialized (even if you trigger it on start up, there's that awkward flicker a user can see). Rather than repeat that code in different apps / places, we can put it in a specific package, like my_shiny_pkg.

reprex:

library(shiny)
library(magrittr)
library(shinyjs)

disable_input <- function(tag) {
  children <- lapply(tag$children, function(child) {
    if (!is.null(child) && child$name == "input") {
      htmltools::tagAppendAttributes(child, disabled = NA)
    } else {
      child
    }
  })
  tag <- htmltools::tagSetChildren(tag, list = children)
  tag
}

ui <- fluidPage(
  shinyjs::useShinyjs(),
  shiny::textInput("foo", "words") %>% disable_input(),
  shiny::actionButton("go", "enable words")
)

server <- function(input, output) {
  shiny::observeEvent(input$go, shinyjs::enable("foo"))
}

shinyApp(ui = ui, server = server)

But if we stored that reusable function in a pkg, it would look like

library(shiny)
library(magrittr)
library(shinyjs)
library(my_shiny_pkg)

ui <- fluidPage(
  shinyjs::useShinyjs(),
  shiny::textInput("foo", "words"), %>% my_shiny_pkg::disable_input(),
  shiny::actionButton("go", "enable words")
)

server <- function(input, output) {
  shiny::observeEvent(input$go, shinyjs::enable("foo"))
}

shinyApp(ui = ui, server = server)

Or let's say I want the input to have blur event so things trigger after the user exits the an input object...

reprex

library(shiny)

add_blur <- function(id, dynamic = TRUE){
  js <- paste0("$(document).on('blur', '#", id,"', function(event) {
  Shiny.onInputChange('", id, "_blur', new Date().getTime());
});")

  rtn <- tags$script(js)

  if(dynamic){
    rtn <- tags$head(rtn)
  }

  return(rtn)
}

ui <- function(id){
  shiny::tagList(
    add_blur("foo", F),
    shiny::textInput("foo", "words", "add punctuation"),
  )
}

server <- function(input, output, session) {
  shiny::observeEvent(
    input$foo_blur, 
    {
      new_val <- gsub("[[:punct:]]+", "_", input$foo)
      shiny::updateTextInput(session, "foo", value = new_val)
      }
    )
}

shinyApp(ui = ui, server = server)

but if we're using the method a lot in an app, a pkg function call is preferable:

library(shiny)

ui <- function(id){
  shiny::tagList(
    my_shiny_pgk::add_blur("foo", F),
    shiny::textInput("foo", "words", "add punctuation"),
  )
}

server <- function(input, output, session) {
  shiny::observeEvent(
    input$foo_blur, 
    {
      new_val <- gsub("[[:punct:]]+", "_", input$foo)
      shiny::updateTextInput(session, "foo", value = new_val)
      }
    )
}

shinyApp(ui = ui, server = server)

Module functions There's less of a reprex to illustrate here, as it's just a simple notion of reusable modules across apps / dashboards. An example may be an analysis template module, where you place the UIs for input options, analysis options, report table and visual, and user notes, along with the server to process the other nested modules. A less company specific example may be a descriptives module that you may often use often within and between apps to empower users to see variable descriptives, while it's not the primary focus of the app.

Packaging your shiny code really has to do with how much consistency are you seeking between apps, along with how many functions / modules you can reuse even in inconsistent apps.

Bookmarking Let's take an app where the data source is dynamic. Let's say it's upload-able or can be linked dynamically. Then the user can choose a handful of options and the results are generated. That user may want to return to the app in the state they previously left it, or share that state with other users. Here, we'll need to bookmark the app, likely with some nested modules. Writing the bookmarking is often less fun, as you'll have to capture it and edit your observes and reactives to accommodate the restore. Here, we send the bookmarked folder to S3 using shiny::onBookmarked, and put that folder back into the app instance using shiny::onRestore. Naturally, we'd need to create some user friendly (and secure) to access their specific state.

Not confident that this is the best method, just one way we got it to work. And obviously, one doesn't have to use S3, but some storage outside the app instance.

Also used to be an regular Shiny coder, then work tasks shifted my focus over the last year or so. So please do push back on any thoughts that feel dated / I need to learn more.