14 Mastering Shiny’s events

We’ve already seen a couple of Shiny JS events since the beginning of this book. Advanced readers probably know the shiny:connected, meaning that the client and server are properly initialized and all internal methods/functions are available to the programmer. Below, we add more elements to the list, trying to give practical examples.

14.1 Get the last changed input

14.1.1 Motivations

We probably all had this question one day: How can I get the last changed input in a Shiny app? There are already some methods like this one:

runApp(
  shinyApp(
    ui = shinyUI(
      fluidPage(
        textInput('txt_a', 'Input Text A'),
        textInput('txt_b', 'Input Text B'),
        uiOutput('txt_c_out'),
        verbatimTextOutput("show_last")
      )
    ),
    server = function(input, output, session) {
      output$txt_c_out <- renderUI({
        textInput('txt_c', 'Input Text C')
      })
      
      values <- reactiveValues(
        lastUpdated = NULL
      )
      
      observe({
        lapply(names(input), function(x) {
          observe({
            input[[x]]
            values$lastUpdated <- x
          })
        })
      })
      
      output$show_last <- renderPrint({
        values$lastUpdated
      })
    }
  )
)

Shouldn’t this be easier? Could we do that from the client instead, thereby reducing the server load?

14.1.2 Invoke JS events

shiny:inputchanged is the event we are looking for. It is fired each time an input gets a new value. The related events has five properties:

  • name, the event name.
  • value, the new value.
  • inputType, the input type.
  • binding, the related input binding.
  • el the related input DOM element.

You may try below:

library(shiny)

ui <- fluidPage(
  tags$script(
  "$(document).on('shiny:inputchanged', function(event) {
    console.log(event);
  });"
  ), 
  textInput("test", "Test")
)

server <- function(input, output) {}

shinyApp(ui, server)

Changing the textInput() value fires the event. Contrary to what is mentioned in the online documentation, inputType does not always have a value. In this case, an alternative, is to access the related input binding and extract its name, as shown below:

$(document).on('shiny:inputchanged', function(event) {
  Shiny.setInputValue(
    'pleaseStayHome', 
    {
      name: event.name, 
      value: event.value, 
      type: event.binding.name.split('.')[1]
    }
  );
});

If you use this code in a custom shiny template, it is possible that input bindings doesn’t have name, which would thereby make event.binding.name.split('.')[1] crash, event.binding` being undefined.

For the textInput(), the event is also fired when moving the mouse cursor with the keyboard arrows, which is a sort of FALSE positive, since the value isn’t changed. However, as Shiny.setInputValue only sets a new value when the input value really changed (unless the priority is set to event), we avoids this edge case. As an exercice, you may try to add {priority: 'event'} to the above code.

$(document).on('shiny:inputchanged') is also cancellable, that is we may definitely prevent the input to change it’s value, calling event.preventDefault();.

ui <- fluidPage(
  tags$script(
  "$(document).on('shiny:inputchanged', function(event) {
    event.preventDefault();
  });"
  ), 
  textInput("test", "Test"),
  textOutput("val")
)

server <- function(input, output) {
  output$val <- renderText(input$test)
}

shinyApp(ui, server)

14.1.3 Example

shinyMobile natively implements this feature that may be accessed with input$lastInputChanged.

library(shinyMobile)
shinyApp(
  ui = f7Page(
    title = "My app",
    f7SingleLayout(
      navbar = f7Navbar(
        title = "Single Layout",
        hairline = FALSE,
        shadow = TRUE
      ),
      toolbar = f7Toolbar(
        position = "bottom",
        f7Link(label = "Link 1", href = "https://www.google.com"),
        f7Link(label = "Link 2", href = "https://www.google.com")
      ),
      # main content,
      f7Card(
        f7Text(inputId = "text", label = "Text"),
        f7Slider(
          inputId = "range1", 
          label = "Range", 
          min = 0, max = 2, 
          value = 1, 
          step = 0.1
        ),
        f7Stepper(
          inputId = "stepper1", 
          label = "Stepper", 
          min = 0, 
          max = 10, 
          value = 5
        ),
        verbatimTextOutput("lastChanged")
      )
    )
  ),
  server = function(input, output) {
    output$lastChanged <- renderPrint(input$lastInputChanged)
  }
)

This approach has the advantage not to overload the server part with complex logic.

14.1.4 About {shinylogs}

The shinylogs (Meyer and Perrier 2019) package developed by dreamRs provide this feature with much more advanced options.

library(shinylogs)

shinyApp(
  ui = fluidPage(
    numericInput("n", "n", 1),
    sliderInput("s", "s", min = 0, max = 10, value = 5),
    verbatimTextOutput("lastChanged")
  ),
  server = function(input, output, session) {
    # specific to shinylogs
    track_usage(storage_mode = store_null())
    output$lastChanged <- renderPrint({
      input$`.shinylogs_lastInput`
    })
  }
)

14.2 Custom overlay screens

If you ever designed corporate production apps, you probably faced this situation where clients wanted a loading screen, whenever a computation occurs or at startup. To date, one of the most comprehensive alternative is the waiter (Coene 2021) package by John Coene. It provide myriad of options to significantly enhance the perceived performance of your app. In the following, we’ll focus on the waiter_preloader() and waiter_on_busy() functions. How does this work?

14.2.1 Preloader

Under the hood, this feature relies on the shiny:idle event. At app startup, shiny:idle is triggered just after shiny:connected and shiny:sessioninitialized. Then, shiny:idle is also called each time a computation cycle is finished.

Whenever we call waiter_preloader(), an HTML overlay is added in the DOM. Moreover, this extra JS code ensures to hide the waiter when shiny is ready:

window.ran = false;
$(document).on('shiny:idle', function(event){
  if(!window.ran)
    hide_waiter(id = null);
  window.ran = true;
});

As a security, window.ran avoids to run this code twice. As an example, consider this app with a slider input and a plot output. We simulated a delay of three seconds to produce the plot:

library(shiny)
library(waiter)

ui <- fluidPage(
  use_waiter(), # dependencies
  # shows before anything else 
  waiter_preloader(spin_fading_circles()), 
  sliderInput("obs", "Number of observations:",
    min = 0, max = 1000, value = 500
  ),
  plotOutput("distPlot")
)

server <- function(input, output){
  output$distPlot <- renderPlot({
    Sys.sleep(3)
    hist(rnorm(input$obs))
  })
}
shinyApp(ui, server)

Notice how the waiter correctly handles the plot processing time.

14.2.2 Load on busy

Similarly, the waiter_on_busy() exploit the shiny:idle and shiny:busy events. We first create the loader as soon as shiny is busy:

$(document).on('shiny:busy', function(event) {
  show_waiter(
    id = null,
    html = ..., 
    color = ...
  );
});

and is hidden once shiny is done:

$(document).on('shiny:idle', function(event) {
  hide_waiter(null);
});