shinyEvents: build shiny apps with event handlers

15 Jan 2015

RStudio’s shiny is a great framework to generate web applications with R. In a classical shiny app, interactivity is not generated via event handlers but by reactive programming. For details, see the shiny documentation and tutorials under http://shiny.rstudio.com/.

While shiny’s reactive programming model is great for smaller apps, I personally found it less useful for bigger applications that create a lot of interactive dynamic UI.

For example, when writing the initial shiny interface for my package RTutor https://github.com/skranz/RTutor, I felt that some observers or render functions were triggered too frequently, and I was not sure where to best put the ‘server code’ of newly dynamically created objects. Of course, it is definitely possible to write large applications with reactivity, but given my limited understanding of the reactivity model, it just was hard for me…

Anyway, I generated the package shinyEvents to emulate the classical event-handling paradigm for shiny applications and find it personally quite useful…

The shinyEvents package allows to write shiny applications that use classical event handlers, e.g. for button clicks, value changes, etc. One does not write an explicit server function, but just adds event handlers to an app object. Widgets will be updated with explicit calls to updateXXX or setXXXX functions, like e.g. setText(id, "New text"). Widget values and event handlers can be set in a similar fashion for an app that has not yet started as for an already running app.

Installation

To install the package see the description on the package’s Github page:

https://github.com/skranz/shinyEvents

Examples

A simple static app

Here is a simple example app.

library(shinyEvents)

# Create a new eventsApp
app = eventsApp()

# ui
app$ui = fluidPage(
  actionButton("plotBtn", "plot"),
  selectInput("mySelect", "Select:",
      c("Cylinders" = "cyl",
        "Transmission" = "am",
        "Gears" = "gear")
  ),
  textOutput("myText"),
  plotOutput("myPlot")
)

# Handler for the plot button
buttonHandler("plotBtn", function(session, id, value, app,...) {
  setText("myText", paste0("You pressed the button ",id," ",
          value," times. "))
  setPlot("myPlot", plot(runif(10), runif(10)))    
})

# Handler for change of an input value
changeHandler("mySelect", function(id, value,...) {
  setText("myText",paste0("You chose the list item ", value,". ", 
                          "A random number: ", sample(1:1000,1)))
})

# Set an initial text
setText("myText","This is the start text...")

# Directly launch the events app in the viewer pane
runEventsApp(app,launch.browser=rstudio::viewer)

Note that a call to eventsApp() stores the generated app object (an environment) globally. The calls to buttonHandler, changeHandler and setText reference by default to this globally stored app object. Once the app starts, a copy of the app object will be generated for each user session that is generated by shiny.

Such a simple app app could be much easier written with the standard reactivity model of shiny. Yet, shinyEvents can become more useful when you have an app that creates a lot of dynamic UI.

A simple app with dynamic UI

Here is a simple app that creates dynamic UI.

# Create a new eventsApp
app = eventsApp()

# main ui
app$ui = fluidPage(
  actionButton("uiBtn", "make dynamic ui"),
  textOutput("myText"),
  uiOutput('myUI')
)

# Dynamically create UI with button and add handler for it
buttonHandler("uiBtn", function(session, value,...) {  
  # Set a new dynamic UI
  dynUI= fluidRow(
    actionButton("dynBtn", paste0("Dynamic button ",value))
  )
  setUI("myUI", dynUI)
  
  # Add handler for the new dynBtn in the new UI.
  # Existing handlers for dynBtn are by default replaced
  buttonHandler("dynBtn", ui.count = value, function(value,ui.count,...) {
    setText("myText", paste0(
      "UI was created ", ui.count, " times.\n",
      "Dynamic button pressed ", value, " times."))
  })
})
# Directly launch the events app in the viewer pane
runEventsApp(app,launch.browser=rstudio::viewer)

The button handler for the static button creates and sets a new UI with another button and also generates a handler for the new button.

Note:

  • The syntax for creating handlers (and setting values) stays the same for dynamic objects created by an already running app as for static objects that are created before the app has started.

  • The dynamically created button Handler buttonHandler("dynBtn", ui.count = value, function(value,ui.count...) { passes a manual parameter ui.count to the handler function.

A small chat app

The code below generates a small chat application as a shiny events app in which multiple users can interact. Open multiple browser windows to see how chatting among multiple clients works.

  library(shinyEvents)
  library(shinyAce)

  app = eventsApp()
  
  # app$glob can contain "global" variables that are visible
  # for all sessions.
  # app$glob$txt will be the content of the chat window
  app$glob$txt = "Conversation so far"
  
  app$ui = fluidPage(
    textInput("userName","User Name",""),
    
    # Chat window
    aceEditor("convAce",value = app$glob$txt, height="200px",
              showLineNumbers = FALSE, debounce=100),    
    
    # Enter new text
    aceEditor("enterAce",value = "Your text",height="30px",
              showLineNumbers = FALSE,debounce = 100,
              hotkeys = list(addTextKey="Ctrl-Enter")),
    
    actionButton("addBtn", "add")
  )

  addChatText = function(session,app,...) {
    restore.point("addChatText")
    user = getInputValue("userName")
    str = getInputValue("enterAce")
    app$glob$txt = paste0(app$glob$txt,"\n",user, ": ",paste0(str,collapse="\n"))
    updateAceEditor(session,"convAce", value = app$glob$txt)
    updateAceEditor(session,"enterAce", value = " ")
  }
  
  # Add chat text when button or Ctrl-Enter is pressed 
  buttonHandler(id="addBtn",addChatText)
  aceHotkeyHandler("addTextKey",addChatText)
  
  # refresh chat window each second
  timerHandler("refreshChatWindow",1000, function(session,app,...) {
    txt = getInputValue("convAce")
    if (!identical(txt, app$glob$txt)) {
      cat("Refresh chat window...")
      updateAceEditor(session, "convAce", value = app$glob$txt)
    }
  })
  

  # Initialize each new session with a random user name
  appInitHandler(function(session,app,...) {
    updateTextInput(session,"userName",
                    value=paste0("guest", sample.int(10000,1)) )
    updateAceEditor(session,editorId = "convAce",value = app$glob$txt)
  })


  runEventsApp(app, launch.browser=TRUE)
  # To test chat function, open several browser tabs

We use some new handlers in this example:

  • aceHotkeyHandler(...) can handle hotkeys in an aceEditor input

  • timerHandler(...) specifies a function that will be called in fixed time intervals

  • appInitHandler(...) specifies a function to customize a newly initiated session of the app

The app object has a field glob that can be used to store variables that will be shared among sessions. (Of course, you could alternatively just use a global variable directly in R.)

Deploying as a shiny app

The example run the generated app locally. Of course you can also deploy an event app via shiny server. Just generate in the usual fashion an app folder with files ui.R, server.R, and global.R.

I would recommend to ui.R and server.R to be the following one-liners:

# ui.R
shinyUI(app$ui)

and

# server.R
shinyServer(app$server)

The generation of the app can then be put into global.R. If our first example, we would put into global.R:

# global.R for simply shiny events app

library(shinyEvents)

# Create a new eventsApp
app = eventsApp()

# ui
app$ui = fluidPage(
  actionButton("plotBtn", "plot"),
  selectInput("mySelect", "Select:",
      c("Cylinders" = "cyl",
        "Transmission" = "am",
        "Gears" = "gear")
  ),
  textOutput("myText"),
  plotOutput("myPlot")
)

# Handler for the plot button
buttonHandler("plotBtn", function(session, id, value, app,...) {
  setText("myText", paste0("You pressed the button ",id," ",
                           value," times. "))
  setPlot("myPlot", plot(runif(10), runif(10)))    
})

# Handler for change of an input value
changeHandler("mySelect", function(id, value,...) {
  setText("myText",paste0("You chose the list item ", value,". ", 
                          "A random number: ", sample(1:1000,1)))
})

# Set an initial text
setText("myText","This is the start text...")

Published on 15 Jan 2015