Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create section on complex storage / bookmarking #12

Open
mjfrigaard opened this issue Nov 4, 2023 · 1 comment
Open

Create section on complex storage / bookmarking #12

mjfrigaard opened this issue Nov 4, 2023 · 1 comment
Assignees
Labels
enhancement New feature or request question Further information is requested

Comments

@mjfrigaard
Copy link
Owner

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

@tsolloway
Copy link

tsolloway commented Nov 5, 2023

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.

@mjfrigaard mjfrigaard self-assigned this Nov 6, 2023
@mjfrigaard mjfrigaard added enhancement New feature or request question Further information is requested labels Nov 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants