One observer for all buttons in Shiny using JavaScript / jQuery

One of the profound limitations of Shiny is the way it supports buttons. Buttons are based on a function called actionButton(). When a button is clicked, Shiny reports the click on input$button_id. Every button has to have a unique id, which means that each button also has to have its own observer to watch for a click. But what if you want to do something like this:

Here we have a list of projects. The buttons let you view or join one of the projects. When you are writing the code to create observers, you don’t know how many projects there will be – only that there could eventually be hundreds of them – which means your code would need hundreds times 2 observers (times 2 because there are two buttons).

As an alternative to actionButton(), however, it’s pretty simple to have a JavaScript / jQuery function that watches for <button> clicks and uses Shiny.onInputChange() to let R and Shiny know the id of the button that was clicked.

<button id="view_1">...

Here’s some sample jQuery code for setting this up on the JavaScript side. Include this in your script file. (There’s more on basic JavaScript/Shiny interactions, including how to set up the script file, in How to get Shiny on the server and JavaScript in the browser talking to each other.

$(document).on('click', 'button', function(e) {
   e.stopPropagation()
   if(typeof BUTTON_CLICK_COUNT == "undefined") {
      BUTTON_CLICK_COUNT = 1; 
    } else {
      BUTTON_CLICK_COUNT ++;
    }
    Shiny.onInputChange("js.button_clicked", 
      e.target.id + "_" + BUTTON_CLICK_COUNT);
});

The code uses a JavaScript programming convention in which global variable names are in all caps. BUTTON_CLICK_COUNT is a global so that it doesn’t start over at 1 every time a button gets clicked.

The first line says to run the embedded function whenever there’s a click on a <button> element in the document. The next line, which is the first line of the embedded function, tells JavaScript not to bubble the event upward to the <button>’s parent elements. Without this line, the embedded function is called a second time when the event hits the document level.

Next let’s look at Shiny.onInputChange() in the final line. It sends the button’s id to an observer inside Shiny called js.button_clicked. It appends the BUTTON_CLICK_COUNT onto the button’s id. So, if the button id is view_1 (in this example, view means a View Project button, 1 indicates the the row the button is in), what the js.button_clicked observer would receive is something like view_1_1.

The BUTTON_CLICK_COUNT is just a trick to make sure Shiny thinks the input has changed . Without that, sometimes a button will go dead because although Shiny.onInputChange() sees the click, it doesn’t think anything has changed, so it doesn’t send the id back to the server. All of the other code in the embedded function does nothing but come up with that number. Once back in Shiny on the server,  the click count is meaningless.

In your Shiny app, the observer for button clicks might begin like this:

observeEvent(input$js.button_clicked, {
   uid = str_split(input$js.button_clicked, "_")
   button = uid[[1]][1]
   n = uid[[1]][2]
      # for debugging...
   print(paste0(button, " clicked on row ", n))

switch(button,
   "view" = {...},
   ...
)

For each type of button you have, you include code in this observer that reacts to the type of button (in this example, View Project or Join Project) and the exact row the button was in (in n). And that’s how you can have any number of buttons and just one observer in Shiny server.

This method can be extended to include other HTML elements. In the next post, Adding anchors to our Shiny button observer, the JavaScript / jQuery event handler also deals with menus that have href-less anchors.

I figured out this trick after reading JavaScript & jQuery while traveling over spring break. This is the most readable programming book I’ve ever encountered.

Leave a Reply

Your email address will not be published. Required fields are marked *