12 Shiny’s input system

Shiny inputs are key elements of Shiny apps since they are a way for the end-user to interact with the app. You may know sliderInput(), numericInput(), checkboxInput() but sometimes you may need fancier elements like knobInput() from shinyWidgets, as depicted on Figure 12.1 or even more sophisticated inputs like the shinyMobile smartSelect() (Figure 12.1, right panel). Have you ever wondered what are the mechanisms behind inputs? Have you ever dreamt to develop your own?

The goal of this section is to understand how Shiny inputs work and how to create new ones.

Custom shiny inputs. left: knobInput from shinyWidgets; right: smart select from shinyMobileCustom shiny inputs. left: knobInput from shinyWidgets; right: smart select from shinyMobile

FIGURE 12.1: Custom shiny inputs. left: knobInput from shinyWidgets; right: smart select from shinyMobile

12.1 Input bindings

When we run our app, most of the time it works just fine! The question is, what is the magic behind? Upon initialization, Shiny runs several JavaScript functions. Some are accessible to the programmer, thought the Shiny JS object, already mentioned in the previous chapter, section 10.6.3. To illustrate what they do, let’s run the app below:

library(shiny)
ui <- fluidPage(
  sliderInput(
    "obs", 
    "Number of observations:",
    min = 0, 
    max = 1000, 
    value = 500
  ),
  plotOutput("distPlot")
)

server <- function(input, output, session) {
  output$distPlot <- renderPlot({
    hist(rnorm(input$obs))
  })
}
shinyApp(ui, server)

We then open the HTML inspector and run Shiny.unbindAll(document), document being the scope, that is where to search. Try to change the slider input. You will notice that nothing happens. Now let’s type Shiny.bindAll(document) and update the slider value. Moving the slider successfully update the plot. Magic isn’t it? This simply shows that when inputs are not bound, nothing happens so binding inputs is necessary!

We consider another example with multiple inputs:

ui <- fluidPage(
  actionButton(
    "unbind", 
    "Unbind inputs", 
    onclick = "Shiny.unbindAll();"
  ),
  actionButton(
    "bind", 
    "Bind inputs", 
    onclick = "Shiny.bindAll();"
  ),
  lapply(1:3, function(i) {
    textInput(paste0("text_", i), paste("Text", i))
  }),
  lapply(1:3, function(i) {
    uiOutput(paste0("val_", i))
  })
)

server <- function(input, output, session) {
  lapply(1:3, function(i) {
    output[[paste0("val_", i)]] <- renderPrint({
      input[[paste0("text_", i)]]
    })
  })
}

shinyApp(ui, server)

Let’s see below what is an input binding and how it works.

12.1.1 Input structure

in HTML, an input element is given by the <input> tag as well as several attributes.

<input id = inputId type = "text" 
class = "input-text" value = value>
  • id guarantees the input uniqueness and a way for shiny to recover it in the input$<id> element.
  • type like checkbox, button, texttype may also be a good target for the input binding find method, as explained below.
  • class may be required to find the element in the DOM. It is more convenient for an input binding to target a class (and all associated elements) rather than an id which corresponds to one element by definition. It is also used by CSS to apply styles.
  • value holds the input value.

12.1.2 Binding Shiny inputs

An input binding allows Shiny to identify each instance of a given input and what you may do with this input. For instance, a slider input must update whenever the range is dragged or when the left and right arrows of the keyboard are pressed. It relies on a class defined in the input_binding.js file.

Let’s describe each method chronologically. For better convenience, the book side package contains step by step demonstrations which may be found here. Each example is called by the customTextInputExample(), which takes the input binding step as only parameter. For instance customTextInputExample(1) will invoke the first step, while customTextInputExample(4) will include all steps from 1 to 4.

12.1.2.1 Find the input

The first step, is critical which is to locate the input in the DOM. On the R side, we define an input, with a specific attribute that will serve as a receptor for the binding. For most of inputs, this may be handled by the type attribute. In other cases, this may be the class, like for the actionButton(). On the JS side, we need a method able to identify this receptor. Moreover, two different types of inputs (for instance radioButton() and selectInput()) cannot have the same receptor for conflict reasons, whereas two instances of the same input type can. If your app contains 10 sliders, they all share the same input binding, and this is where the thing is powerful since they are all bound in one step. The receptor identifier is provided by the find method of the InputBinding class. This method must be applied on a scope, that is the document. find accepts any valid jQuery selector:

find: function(scope) {
  return $(scope).find('.input-text');
}

Don’t forget the return statement. Omitting it would simply make the binding step failing, and all other downstream steps!

Figure 12.2 summarizes this important step.

How to find inputs?

FIGURE 12.2: How to find inputs?

Below, we are going to create a new binding for the textInput(), with only two methods mentioned in the previous section, that is find and getValue. As shiny already provides bindings for the textInput(), we don’t want them to recognize our new input. Therefore, we add a new input-text class and make our own input binding pointing to that specific class:

customTextInput <- function (
  inputId, 
  label, 
  value = "", 
  width = NULL, 
  placeholder = NULL
) {
  
  # this external wrapper ensure to control the input width
  div(
    class = "form-group shiny-input-container", 
    style = if (!is.null(width)) {
      paste0("width: ", validateCssUnit(width), ";")
    },
    # input label
    shinyInputLabel(inputId, label), 
    
    # input element + JS dependencies
    tagList(
      customTextInputDeps(),
      tags$input(
        id = inputId,
        type = "text",
        class = "form-control input-text",
        value = value,
        placeholder = placeholder
      )
    )
  )
}

The last part of the code contains a tagList() with two elements:

Below is an example of how we managed the dependency creation in the side package. If we had multiple inputs, we would add more script to the dependency by passing a vector to the script parameter.

customTextInputDeps <- function() {
  htmlDependency(
    name = "customTextBindings",
    version = "1.0.0",
    src = c(file = system.file(
      "input-system/input-bindings", 
      package = "OSUICode"
    )),
    script = "customTextInputBinding.js"
  )
}

Figure 12.3 shows the main elements of the textInput() widget. In the above code, shinyInputLabel is a Shiny internal function that creates the numeric input label, or in other word the text displayed next to it. The core input element is wrapped by tags$input.

Shiny's textInput elements

FIGURE 12.3: Shiny’s textInput elements

We invite the reader to run the full working demonstration with customTextInputExample(1). In short, this example consists in a simple text input and an output showing the current text input value:

customTextInputExample <- function(binding_step) {
  ui <- fluidPage(
    customTextInput(
      inputId = "caption",
      label = "Caption",
      value = "Data Summary",
      binding_step = binding_step
    ),
    textOutput("custom_text")
  )
  server <- function(input, output) {
    output$custom_text <- renderText(input$caption)
  }
  shinyApp(ui, server)
}

We open the developer tools to inspect the customTextInputBinding.js script, put a breakpoints in the find method and reload the page. Upon reload, the JavaScript debugger opens, as shown Figure 12.4. Type $(scope).find('.input-text') in the console and see what is displayed. This is the DOM element which you may highlight when you hover over the JavaScript output.

Building input bindings like this significantly ease the debugging process and you’ll get more chances to be successful!

Find is the first method triggered

FIGURE 12.4: Find is the first method triggered

Now, let’s see why it is better to target elements by type or class. We run the customTextInputExampleBis() example. This is a demonstration app containing two text inputs. Moreover, the binding has been modified so that it looks for element having a specific id:

find: function(scope) {
  return $(scope).find('#mytextInput');
}

If you repeat the above debugging steps, $(scope).find('.input-text') only targets the first text input, meaning that the second input will not be found and bound, as demonstrated in Figure 12.5.

Find by id is a rather bad idea

FIGURE 12.5: Find by id is a rather bad idea

As a side note, you’ll also get an error in the binding (Uncaught Not implemented), indicating that the getValue method is not implemented yet. Fear not! We are going to add it very soon.

12.1.2.2 Initialize inputs

Upon initialization, Shiny calls the initializeInputs function that takes all input bindings and call their initialize method before binding all inputs. Note that once an input has been initialized, it has a _shiny_initialized tag to avoid initializing it twice. The initialize method is not always defined but some elements require to be explicitly initialized or activated. For instance the Framework7 API, on top of which shinyMobile (Granjon, Perrier, and Rudolf 2021) is built, require to instantiate all elements. Below is an example for the toggle input:

// what is expected
let toggle = app.toggle.create({
  el: '.toggle',
  on: {
    change: function () {
      console.log('Toggle changed')
    }
  }
});

el: '.toggle' means that we are looking at the element(s) having the toggle class. app.toggle.create is internal to the Framework7 API. The corresponding shinyMobile input binding starts as follows:

var f7ToggleBinding = new Shiny.InputBinding();
  $.extend(f7ToggleBinding, {
    initialize: function(el) {
      app.toggle.create({el: el});
    },
    // other methods
});

Once initialized, we may use all specific methods provided by the API. Framework7 is clearly a gold mine, as its API provides many possible options for many inputs/widgets. We provide more examples in Chapters 24 and 26.

12.1.2.3 Get the value

The getValue method returns the input value. The way to obtain the value is different for almost all inputs. For instance, the textInput() is pretty simple since the value is located in the value attribute. el refers to the element holding the id attribute and recognized by the find method. Figure 12.6 shows the result of a console.log($(el));.

About el

FIGURE 12.6: About el

getValue: function(el) {
  console.log($(el));
  return $(el).val();
}

To get the value, we apply the jQuery method val on the $(el) element and return the result.

Don’t forget the return statement!

Similarly as in the find section, we run customTextInputExample(2) and open the developer tools to inspect the customTextInputBinding_2.js script. We put a breakpoints in the getValue method and reload the page. Upon reload, the JavaScript debugger opens starts in find. You may click on the next blue arrow to jump to the next breakpoints that is getValue, as shown Figure 12.7. Typing $(el).val() in the console shows the current text value.

getValue returns the current input value

FIGURE 12.7: getValue returns the current input value

Clicking on next again exit the debugger. Interestingly, you’ll notice that a text appears below the input, meaning that the input$caption element exists and is internally tracked by shiny. Notice that when you try to change the text content, the output value does not update as we would normally expect. We are actually omitting a couple of methods so preventing the binding from being fully functional. We will introduce them in the following sections!

12.1.2.4 Set and update

setValue is used to set the value of the current input. This method is necessary so that the input value may be updated. It has to be used in combination with receiveMessage, which is the JavaScript part of all the R updateInput functions, like updateTextInput(). We usually call the setValue method inside.

// el is the DOM element.
// value represents the new value.
setValue: function(el, value) {
  $(el).val(value);
}

Let’s create a function to update our custom text input. Call it updateCustomTextInput. It requires at least three parameters:

  • inputId tells which input to update.
  • value is the new value. This will be taken by the setValue JS method in the input binding.
  • session is the Shiny session object mentioned earlier in section 11.4.1. We will use the sendInputMessage to send values from R to JavaScript. The receiveMessage method will apply setValue with the data received from R. The current session is recovered with getDefaultReactiveDomain().
updateCustomTextInput <- function(
  inputId, 
  value = NULL, 
  session = getDefaultReactiveDomain()
) {
  session$sendInputMessage(inputId, message = value)
}

We add setValue and receiveMessage to custom input binding.

Figure 12.8 illustrates the main mechanisms.

Events following a click on the update button. This figure demonstrates how R and JS communicate, through the websocket.

FIGURE 12.8: Events following a click on the update button. This figure demonstrates how R and JS communicate, through the websocket.

If we have to pass multiple elements to update, we would have to change the updateCustomTextInput function such as:

updateCustomTextInput <- function(
  inputId, 
  value = NULL, 
  placeholder = NULL, 
  session = getDefaultReactiveDomain()
) {
  message <- dropNulls(
    list(
      value = value,
      placeholder = placeholder
    )
  )
  session$sendInputMessage(inputId, message)
}

shiny:::dropNulls is an internal function ensuring that the list does not contain NULL elements. We send a list from R, which is then serialized to a JSON object. In the receiveMessage method, properties like value may be accessed using the . notation. It is good practice to add a data.hasOwnProperty check to avoid running code if the specified property does not exist:

// data are received from R. 
// It is a JS object. 
receiveMessage: function(el, data) {
  console.log(data);
  if (data.hasOwnProperty('value')) {
    this.setValue(el, data.value);
  }
  // other parameters to update...
}

this refers to the custom text input binding class (which is an object), so that this.setValue allows to call the setValue method …

Similarly as in the previous sections, we run updateCustomTextInputExample(3) and open the developer tools to inspect the customTextInputBinding_3.js script. We put a breakpoints in the receiveMessage and setValue methods and reload the page. Upon reload, the JavaScript debugger opens starts in find. You may click on the next blue arrow until you reach receiveMessage, as shown Figure 12.9. Inspecting the data object, it contains only one property namely the value. In practice, there may be more complex structure. As an exercise, you may change the data.value to whatever value you want.

Receive a message from R

FIGURE 12.9: Receive a message from R

Click on the next arrow makes us jump in the next call that is setValue, where we can print the value to check whether it is correct. Running $(el).val(value); in the debugger console instantaneously update the DOM element with the new text, as shown on Figure 12.10.

Set the new value

FIGURE 12.10: Set the new value

So far so good! We managed to update the text input value on the client. Yet, after clicking the button, the output value still does not change. We are going to fix this missing step in the next section.

12.1.2.5 Subscribe

subscribe listens to events defining Shiny to update the input value and make it available in the app. Some API like Bootstrap explicitly mention those events (like hide.bs.tab, shown.bs.tab, …). Going back to our custom text input, what event would make it change?

  • After a key is release on the keyboard. We may listen to keyup.
  • After copying and pasting any text in the input field or dictating text. The input event may be helpful.

We add those events to our binding using an event listener seen in Chapter 10.

$(el).on(
  'keyup.customTextBinding input.customTextBinding', 
  function(event) {
    callback(true);
});

Notice the event structure: EVENT_NAME.BINDING_NAME. It is best practice to follow this convention.

The callback parameter ensures that the new value is captured by Shiny. Chapter 13 provides more details, but this is quite technical.

We run the above example, open the HTML inspector, select the customTextInputBinding_4.js script and put a break point in the getValue as well as subscribe method. We enter a new text inside the input field, which triggers the debugger inside the subscribe call. Inspecting the event object, the type indicate the action, which is an input action and the target is the text input element itself, depicted on Figure 12.11.

Subscribe method after a manual update of the text input

FIGURE 12.11: Subscribe method after a manual update of the text input

We click on next and notice that we go back in the getValue method to get the new value. You may check typing $(el).val() in the debugger console, like on Figure 12.12. Clicking next again shows the updated output value.

Subscribe is followed by a new getValue

FIGURE 12.12: Subscribe is followed by a new getValue

Hooray! The output result is successfully changed when the input value is manually updated. However, it is not modified when we click on the update button. What did we miss? Looking back at the receiveMessage method, we changed the input value but how does Shiny knows that this step was successful? To check that no event is raised, we put a console.log(event); in the subscribe method. Any action like removing the text content or adding new text triggers event but clicking on the action button does not. Therefore, we must trigger an event and add it to the subscribe method. We may choose the change event, that triggers when an element is updated. Notice the parameter passed to callback. We discuss it in the next part!

$(el).on('change.customTextBinding', function(event) {
  callback(false);
});

Besides, in the receiveMessage we must trigger a change event to trigger the subscribe method:

receiveMessage: function(el, data) {
  if (data.hasOwnProperty('value')) {
    this.setValue(el, data.value);
    $(el).trigger('change');
  }
}

Let’s try again.

We put a new break point in the second event listener, that is the one for the change event. Clicking on the button only triggers the change event, as shown Figure 12.13.

We may add multiple event listeners inside the subscribe method

FIGURE 12.13: We may add multiple event listeners inside the subscribe method

… In some situations, we have to be careful with the this element. Indeed, called in an event listener, this refers to the element that triggered the event and not to the input binding object. For instance below is an example where we need to trigger the getValue method inside an event listener located in the subscribe method. If you call this.getValue(el), you’ll get an error. The trick consists in creating a variable namely self that take this as value, outside the event listener. In that case self refers to the binding itself and it make sens to call self.getValue(el):

subscribe: function(el, callback) {
  self = this;
  $(el).on('click.button', function(e) {
    var currentVal = self.getValue(el);
    $(el).val(currentVal + 1);
    callback();
  });
}

Perfect? Not exactly.

12.1.2.6 Setting rate policies

It would be better to only change the input value once the keyboard is completely released for some time (and not each time a key is released). This is what we call debouncing, which allows a delay before telling Shiny to read the new value, and is achieved using the getRatePolicy method. Additionally, we must also pass true to the callback in the subscribe method, in order to apply our specific rate policy (debounce, throttle). This is useful for instance when we don’t want to flood the server with useless update requests. For example when using a slider, we only want to send the value as soon as the range stops moving and not all intermediate values. Those elements are defined here.

Run the below app and try to manually change the text input value by adding a couple of letters as fast as you can. What do you notice? We see the output value only updates when we release the keyboard.

You may adjust the delay according to your needs, but we caution to not set the delay too long as this becomes problematic too (unnecessary lags).

If you want to get an overview of all binding steps, you may try the following slide from the 2020 R in Pharma workshop.

12.1.2.7 Register an input binding

At the end of the input binding definition, we register it for Shiny.

let myBinding = new Shiny.inputBinding();
  $.extend(myBinding, {
  // methods go here
});

Shiny.inputBindings.register(
  myBinding, 
  'PACKAGE_NAME.BINDING_NAME'
);

Best practice is to name it following PACKAGE_NAME.BINDING_NAME, to avoid conflicts. Although the Shiny documentation mentions a Shiny.inputBindings.setPriority method to handle conflicting bindings, if you respect the above convention, this case almost never happens.

As a side note, if you think that the binding name is useless, have a look at the shinytest internal structure. Under the hood, it has a file which maps all input elements:

widget_names <- c(
    "shiny.actionButtonInput"  = "actionButton",
    "shiny.checkboxInput"      = "checkboxInput",
    "shiny.checkboxGroupInput" = "checkboxGroupInput",

Guess what? Those names are the one given during the input binding registration!

12.1.2.8 Other binding methods

There are a couple of methods not described above that are contained in the InputBinding class prototype. They were not described before since most of the time, we don’t need to change them and can rely on the defaults:

  • getId returns the object id (Figure 12.14). If you don’t provide your own method, the binding falls back to the default one provided in the InputBinding class. This method is called after find. The next Chapter 13 provides more details.
  • getType required to handle custom data format. It is called after getId. A entire section 12.4 is dedicated.
  • getState ?
The binding getId method

FIGURE 12.14: The binding getId method

12.1.3 Edit an input binding

In some cases, we would like to access the input binding and change it’s default behavior, even though not always recommended, since it will affect all related inputs. As bindings are contained in a registry, namely Shiny.inputBindings, one may seamlessly access and modify them. This is a five steps process:

  1. Wait for the shiny:connected event, so that the Shiny JS object exists.
  2. Unbind all inputs with Shiny.unbindAll().
  3. Access the binding registry, Shiny.inputBindings.
  4. Extend the binding and edit its content with $.extend(... {...})
  5. Apply the new changes with Shiny.bindAll().
$(function() {
  $(document).on('shiny:connected', function(event) {
    Shiny.unbindAll();
    $.extend(Shiny
      .inputBindings
      .bindingNames['shiny.actionButtonInput']
      .binding, {
        // do whathever you want to edit existing methods
      });
    Shiny.bindAll();
  });
});

12.1.4 Update a binding from the client

The interest of receiveMessage and setValue is to be able to update the input from the server side, that is R, through the session$sendInputMessage. Yet, this task might be done directly on the client, thereby lowering the load on the server. We consider the following example: a shiny app contains two actions buttons, clicking on the first one increases the value of the second by 10. This won’t be possible with the classic approach since a button click only increases its value by 1. How do we proceed?

  1. We first set an event listener on the first button.
  2. We target the second button and get the input binding with $obj.data('shiny-input-binding').
  3. We recover the current value.
  4. We call the setValue method, adding 10 to the current value.
  5. Importantly, to let Shiny update the value on the R side, we must trigger an event that will be detected in the subscribe method. The action button only has one event listener but other may be added. Don’t forget that triggering a click event would also increment the button value by 1! In the following we have to customize the subscribe method to work around:
$(function() {
  // each time we click on #test (a button)
  $('#button1').on('click', function() {
    let $obj = $('#button2');
    let inputBinding = $obj.data('shiny-input-binding');
    let val = $obj.data('val') || 0;
    inputBinding.setValue($obj, val + 10);
    $obj.trigger('event');
  });
});

If you click on the second button, the value increments only by 1 and the plot will be only visible after 10 clicks, while only 1 click is necessary on the first button. The reset button resets the second action button value to 0. It implements the feature discussed in the previous part, where we extend the button binding to add a reset method and edit the subscribe method to add a change event listener, simply telling shiny to get the new value. Contrary to click, change does not increment the button value.

$.extend(
  Shiny
    .inputBindings
    .bindingNames['shiny.actionButtonInput']
    .binding, {
  reset: function(el) {
   $(el).data('val', 0);
  },
  subscribe: function(el, callback) {
    $(el).on('click.actionButtonInputBinding', function(e) {
      let $el = $(this);
      let val = $el.data('val') || 0;
      $el.data('val', val + 1);

      callback();
    });
            
    // this does not trigger any click and won't change 
    // the button value            
    $(el).on('change.actionButtonInputBinding', function(e) {
      callback();
    });
  }
});

The whole JS code is found below:

$(function() {
  $(document).on('shiny:connected', function(event) {
    Shiny.unbindAll();
    $.extend(Shiny
      .inputBindings
      .bindingNames['shiny.actionButtonInput']
      .binding, {
        reset: function(el) {
         $(el).data('val', 0);
        },
        subscribe: function(el, callback) {
          $(el).on(
            'click.actionButtonInputBinding', function(e) {
              let $el = $(this);
              let val = $el.data('val') || 0;
              $el.data('val', val + 1);

              callback();
          });
                
          $(el).on(
            'change.actionButtonInputBinding', function(e) {
              callback();
          });
              
        }
      });
    Shiny.bindAll();
  });
      
  $('#button1').on('click', function() {
    let $obj = $('#button2');
    let inputBinding = $obj.data('shiny-input-binding');
    let val = $obj.data('val') || 0;
    inputBinding.setValue($obj, val + 10);
    $obj.trigger('change'); 
  });
  
  $('#reset').on('click', function() {
    let $obj = $('#button2');
    let inputBinding = $obj.data('shiny-input-binding');
    inputBinding.reset($obj);
    $obj.trigger('change');
  });
});

Below is the working app:

whose output is shown Figure 12.15.
Edit and trigger an input binding from the client

FIGURE 12.15: Edit and trigger an input binding from the client

This trick has been extensively used in the virtual physiology simulator to trigger animations.

Another example of accessing a binding method from the client is found in the shinydashboard package.

12.2 Secondary inputs

The Shiny input binding system is too convenient to be only used for primary input elements like textInput(), numericInput(). It is a super powerful tool to unleash apps’s interactivity. In the following, we show how to add an input to an element that was not primarily designed to be a user input, also non officially denoted as secondary inputs.

By convention, we’ll not use inputId but id for secondary inputs. This convention is used in all the new versions of RinteRface packages like bs4Dash.

12.2.1 {shinydashboard} boxes on steroids

You may know the shinydashboard box function. Boxes are containers with a title, body, footer, as well as optional elements. Those box may also be collapsed. It would be nice to capture the state of the box in an input, so as to trigger other actions as soon as this input changes. Since an input value is unique, we must add an id parameter to the box function. You may inspect the code here.

Since we may collapse and uncollapse the box, we create the updateBox2 function, which will toggle it:

updateBox2 <- function(
  id, 
  session = getDefaultReactiveDomain()
) {
  session$sendInputMessage(id, message = NULL)
}

When collapsed, a box gets the collapsed-box class, which is useful to keep in mind for the input binding design. As mentioned above, it is also necessary to know when to tell Shiny to update the value with the subscribe method. Most of the time, the change event might be sufficient, but as shinydashboard is built on top of AdminLTE2, it has an API to control the box behavior. We identify two events corresponding to the collapsible action:

  • expanded.boxwidget (Triggered after the box is expanded)
  • collapsed.boxwidget (Triggered after the box is collapsed)

Unfortunately, after further investigations, those events are not possible to use since the AdminLTE library does not trigger them in the main JS code (see the collapse method line 577-612). There are other solutions, as shown below with the click event. To toggle the box, we use the toggleBox method.

let boxBinding = new Shiny.InputBinding();
$.extend(boxBinding, {
  find: function(scope) {
    return $(scope).find('.box');
  },
  getValue: function(el) {
    let isCollapsed = $(el).hasClass('collapsed-box')
    return {collapsed: isCollapsed}; // this will be a list in R
  },
  setValue: function(el, value) {
    $(el).toggleBox();
  }, 
  receiveMessage: function(el, data) {
    this.setValue(el, data);
    $(el).trigger('change');
  },
  subscribe: function(el, callback) {
    $(el).on(
      'click', 
      '[data-widget="collapse"]', 
      function(event) {
        setTimeout(function() {
          callback();
        }, 50);
    }); 
    
    $(el).on('change', function(event) {
      setTimeout(function() {
        callback();
      }, 50);
    });
  },
  unsubscribe: function(el) {
    $(el).off('.boxBinding');
  }
});

Shiny.inputBindings.register(boxBinding, 'box-input');

$(function() {
  // overwrite box animation speed. 
  // Putting 500 ms add unnecessary delay for Shiny.
  $.AdminLTE.boxWidget.animationSpeed = 10;
});

Some comments about the binding:

  • getValue returns an object which will give a list in R. This is in case we add other elements like the remove action available in AdminLTE. We therefore access each input element with input$<box_id>$<property_name>.
  • setValue calls the plug and play toggleBox method.
  • receiveMessage must trigger a change event so that Shiny knows when the value needs to be updated
  • subscribe listens to the click event on the [data-widget="collapse"] element and delays the callback call by a value which is slightly higher than the default AdminLTE2 animation to collapse the box (500ms). If you omit this part, the input will not have time to properly update! Even though animations are nice, it might appears rather sub-optimal to wait 500 ms for a box to collapse. AdminLTE options allow to change this through the $.AdminLTE.boxWidget object. We specify the animationSpeed property to 10 milliseconds and update the input binding script to reduce the delay in the subscribe method (50 ms seems reasonable).
  • We don’t need an extra listener for the updateBox2 function since it also triggers a click on the collapse button, thereby forwarding to the corresponding listener.

Let’s try our new toy in a simple app. The output is depicted on Figure 12.16.

{shinydashboard} box with custom input binding listening to the box collapse state

FIGURE 12.16: {shinydashboard} box with custom input binding listening to the box collapse state

ui <- fluidPage(
  # import shinydashboard deps without the need of 
  # the dashboard template
  useShinydashboard(),

  tags$style("body { background-color: ghostwhite};"),

  br(),
  box2(
   title = textOutput("box_state"),
   "Box body",
   id = "mybox",
   collapsible = TRUE,
   plotOutput("plot")
  ),
  actionButton(
    "toggle_box", 
    "Toggle Box", 
    class = "bg-success"
  )
 )

 server <- function(input, output, session) {
  output$plot <- renderPlot({
    req(!input$mybox$collapsed)
    plot(rnorm(200))
  })

  output$box_state <- renderText({
    state <- if (input$mybox$collapsed) {
      "collapsed"
    } else {
      "uncollapsed"
    }
    paste("My box is", state)
  })

  observeEvent(input$toggle_box, {
    updateBox2("mybox")
  })

 }

 shinyApp(ui, server)

The bs4Dash box function follows the same principle, with extra features showed here. We leave the reader to explore the code as an exercise.

12.2.2 Going further

We may imagine leveraging the input binding system to update any box property and get rid of the classic renderUI() approach. Indeed, until now, there would be only one way to update a box from the server (Figure 12.17):

ui <- fluidPage(
  # import shinydashboard deps without the need of the 
  # dashboard template
  useShinydashboard(),

  tags$style("body { background-color: ghostwhite};"),

  br(),
  uiOutput("custom_box"),
  selectInput(
    "background", 
    "Background", 
    choices = shinydashboard:::validColors
  )
 )

 server <- function(input, output, session) {
  output$custom_box <- renderUI({
    box2(
      title = "Box",
      "Box body",
      background = input$background
    )
  })
 }

 shinyApp(ui, server)
{shinydashboard} box updated with shiny::renderUI

FIGURE 12.17: {shinydashboard} box updated with shiny::renderUI

The whole piece of UI is re-rendered each time, while only the box class should be modified. This does not have much impact here but for a very complex app festooned with inputs/outputs, the overall user experience may be altered.

Let’s provide some optimization and get rid of the renderUI(). We proceed in two steps. The first part consists in customizing the box() function to gather as many parameter as possible in a list of options. For instance, we choose to extract background, width and title. width and background are expected to be numeric and character, respectively, while title might be any HTML tag, justifying the use of as.character() (we can’t use toJSON() on a shiny tag):

box2 <- function(..., id = NULL, title = NULL, footer = NULL,
                 background = NULL, width = 6, height = NULL,
                 collapsible = FALSE, collapsed = FALSE) {
  
  props <- dropNulls(
    list(
      title = as.character(title),
      background = background,
      width = width
    )
  )
  
  # I removed some of the code to highlight that part
}

This properties list has to be treated on the JS side. Before, we must make it accessible within the box2 HTML tag. We choose the following approach, where we convert our properties to a JSON with toJSON() and embed them in a script tag. Note the data-for attribute with the unique id parameter. This will guarantee the uniqueness of our configuration script:

box2 <- function(..., id = NULL, title = NULL, footer = NULL,
                 background = NULL, width = 6, height = NULL,
                 collapsible = FALSE, collapsed = FALSE) {
  
  # code not shown
  
  boxTag <- shiny::tags$div(
    class = if (!is.null(width)) paste0("col-sm-", width), 
    shiny::tags$div(
      id = id,
      class = boxClass, 
      headerTag, 
      shiny::tags$div(
        class = "box-body", 
        style = style,
        ...,
        sidebar[c(1, 3)],
      ), 
      if (!is.null(footer)) shiny::tags$div(
        class = if (isTRUE(footerPadding)) {
          "box-footer"
        } else {
          "box-footer no-padding"
        }, footer)
    ),
    
    # this will make our props accessible from JS
    shiny::tags$script(
      type = "application/json",
      `data-for` = id,
      jsonlite::toJSON(
        x = props,
        auto_unbox = TRUE,
        json_verbatim = TRUE
      )
    )
  )
  
  boxTag
  
}

Then, we have to update the updateBox2 such that it handles both toggle and update possibilities. options contains all updatable properties like background, title and width. We don’t describe the toggle case since it is quite similar to the previous implementations. When the action is update, we enter the if statement and options must be processed. If the option element is a shiny tag or a list of shiny tags (tagList()), we convert it to character. The returned message is a vector containing the action as well as the option list:

updateBox2 <- function(
  id, 
  action = c("toggle", "update"), 
  options = NULL,
  session = getDefaultReactiveDomain()
) {
  # for update, we take a list of options
  if (action == "update") {
    # handle case where options are shiny tag 
    # or a list of tags ...
    options <- lapply(options, function(o) {
      if (inherits(o, "shiny.tag") || 
          inherits(o, "shiny.tag.list")) {
        o <- as.character(o)
      }
      o
    })
    message <- dropNulls(c(action = action, options = options))
    session$sendInputMessage(id, message)
  } else {
    session$sendInputMessage(id, message = action)
  }
}

On the JS side, we modify the setValue method to import our newly defined properties. The boxTag has two children, the box and the configuration script. $(el) refers to the box, therefore we have to look one level up to be able to use the find method (find always goes deeper in the DOM), namely $(el).parent(). From there, we only have to target the script tag $(el).parent().find("script[data-for='" + el.id + "']"). Once captured in a variable, we parse the corresponding element to convert it to an objects that we can manipulate: for instance config.width returns the initial width:

setValue: function(el, value) {
  let config = $(el)
    .parent()
    .find("script[data-for='" + el.id + "']");
  config = JSON.parse(config.html());
}

value.options.width will contain the new width value provided in the updateBox2 message output. Good practice is to check whether value.options.width exists with value.options.hasOwnProperty("width"). If yes we ensure whether its value and config.width are different. We always choose === which compares the type and the value (== only compares the value such that "1" == 1 is true):

setValue: function(el, value) {
  let config = $(el)
    .parent()
    .find("script[data-for='" + el.id + "']");
  config = JSON.parse(config.html());
  
  if (value.action === "update") {
    if (value.options.hasOwnProperty("width")) {
      if (value.options.width !== config.width) {
        this._updateWidth(
          el, 
          config.width, 
          value.options.width
        )
        config.width = value.options.width;
      }
    }
    // other items to update
  }
}

_updateWidth is a internal method defined in the input binding. It has three parameters, el, o and n (o and n being the old and new values, respectively):

_updateWidth: function(el, o, n) {
  // removes old class
  $(el).parent().toggleClass("col-sm-" + o);
  $(el).parent().addClass("col-sm-" + n); 
  // trigger resize so that output resize
  $(el).trigger('resize');
}

We must trigger a resize event so that output correctly scale. The internal method is identified by an underscore since it is not an inherited Shiny.InputBinding method. We finally update the config value by the newly set value and repeat the process for any other property. Don’t forget to update the config script attached to the card tag at the end of the update condition, otherwise the input value won’t be modified:

// replace the old JSON config by the 
// new one to update the input value 
$(el)
  .parent()
  .find("script[data-for='" + el.id + "']")
  .replaceWith(
    '<script type="application/json" data-for="' + 
    el.id + 
    '">' + 
    JSON.stringify(config) + 
    '</script>'
  );

The whole JS code may be found below:

setValue: function(el, value) {
  var config = $(el)
    .parent()
    .find("script[data-for='" + el.id + "']");
  config = JSON.parse(config.html());
  
  // JS logic
  if (value.action === "update") {
    if (value.options.hasOwnProperty("width")) {
      if (value.options.width !== config.width) {
        this._updateWidth(
          el, 
          config.width, 
          value.options.width
        )
        config.width = value.options.width;
      }
    }
    // other items to update
    
    // replace the old JSON config by the new one 
    // to update the input value 
    $(el)
      .parent()
      .find("script[data-for='" + el.id + "']")
      .replaceWith(
        '<script type="application/json" data-for="' + 
        el.id + 
        '">' + 
        JSON.stringify(config) + 
        '</script>'
      );
    
  } else {
    // other tasks
  }
  
}

If it represents a significant amount of work, it is also the guarantee to lower the load on the server side, thereby offering a faster end-user experience. A working prototype has been implemented in shinydashboardPlus and bs4Dash:

shinyAppDir(system.file(
  "vignettes-demos/box-api", 
  package = "shinydashboardPlus"
))

12.3 Utilities to quickly define new inputs

12.3.1 Introduction

If you ever wondered where the Shiny.onInputChange or Shiny.setInputValue comes from (see article), they are actually defined in the initShiny function.

exports.setInputValue = function(name, value, opts) {
  opts = addDefaultInputOpts(opts);
  inputs.setInput(name, value, opts);
};

We recommend using Shiny.setInputValue over Shiny.onInputChange, the first one being slightly misleading. Briefly, this function avoids the creation an input binding and is faster to code but there is a price to pay: losing the ability to easily update the new input through R. Indeed, without input binding, there is no R side updateInput function! By default, Shiny.setInputValue is able to cache the last set value from that input, so that if is is identical, no value is being assigned. If this behavior does not meet your needs and you need to set the input even when the value did not change, be aware that you may specifify a priority option like:

Shiny.setInputValue("myinput", value, {priority: "event"});

12.3.2 Examples

Shiny.setInputValues becomes powerful when combined to the numerous Shiny JavaScript events listed here. This is what we use in the shinyMobile package to store the current device information in a shiny input. Briefly, Framework7 (on top of which is built shinyMobile) has a method Framework7.device, which gives many details related to the user device.

$(document).on('shiny:connected', function(event) {
  Shiny.setInputValue('deviceInfo', Framework7.device);
});

This allows to conditionally display elements and deeply customize the interface. In the example below, the card will not show on mobile devices.

library(shinyMobile)
shinyApp(
  ui = f7Page(
    title = "My app",
    f7SingleLayout(
      navbar = f7Navbar(
        title = "shinyMobile info",
        hairline = FALSE,
        shadow = TRUE
      ),
      # main content
      uiOutput("card"),
      verbatimTextOutput("info"),
    )
  ),
  server = function(input, output, session) {
    
    output$info <- renderPrint(input$shinyInfo)

    # generate a card only for desktop
    output$card <- renderUI({
      if (!input$deviceInfo$desktop) {
        f7Card(
          "This is a simple card with plain text,
          but cards can also contain their own header,
          footer, list view, image, or any other element."
        )
      } else {
        f7Toast(
          session, 
          "You are on desktop! The card will not display", 
          position = "center"
        )
      }
    })
  }
)

12.4 Custom data format

In some cases, the automatic Shiny R to JS data management may not meet our needs. We introduce input handlers, a tool to fine tune the deserialization of data from JS.

12.4.1 The dirty way

For instance, assume we create a date in JS with new Date() and store it in a shiny input with Shiny.setInputValue. On the R side, we will not obtain a date but a character, which is not convenient. This is where input handlers are useful since they allow to manipulate data generated on the JS side before injecting them in R. Such handlers are created with registerInputHandler that takes two parameters:

  • type allows to connect the handler to Shiny.setInputValue. Note that the id is followed by the handler type, for instance Shiny.setInputValue('test:handler', ...) is connected to shiny::registerInputHandler('handler', ...). As recommended by the Shiny documentation, if the input handler is part of a package, it is best practice to name it like packageName.widgetName.
  • a function to transform data, having data as main parameter.

Below we exceptionally include JS code directly in the shiny app snippet, which is not best practice but convenient for the demonstration. Only the second input will give the correct result:

registerInputHandler("OSUICode.textDate", function(data, ...) {
  if (is.null(data)) {
    NULL
  } else {
    res <- try(as.Date(unlist(data)), silent = TRUE)
    if ("try-error" %in% class(res)) {
      warning("Failed to parse dates!")
      # as.Date(NA)
      data
    } else {
      res
    }
  }
}, force = TRUE)

ui <- fluidPage(
  tags$script(
    "$(function(){
      $(document).on('shiny:connected', function() {
        var currentTime = new Date();
        Shiny.setInputValue('time1', currentTime);
        Shiny.setInputValue(
          'time2:OSUICode.textDate', 
          currentTime
        );
      });
    });
    "
  ),
  verbatimTextOutput("res1"),
  verbatimTextOutput("res2")
)

server <- function(input, output, session) {
  output$res1 <- renderPrint({
    list(class(input$time1), input$time1)
  })
  output$res2 <- renderPrint({
    list(class(input$time2), input$time2)
  })
}

shinyApp(ui, server)

12.4.2 The clean way: leverage getType

The cleanest way is to leverage the getType method from the InputBinding class. Let’s refine our text input so that it handles dates. On the R side, in the customTextInput() function, we check the current value’s type:

type <- if (inherits(value, "Date")) {
  "date"
} else {
  NULL
}

We add a custom data attribute to the input tag, which won’t be displayed if the value is not a date:

tags$input(
  id = inputId,
  type = "text",
  class = "form-control input-text",
  value = value,
  placeholder = placeholder,
  `data-data-type` = type
)

We then define our custom handler. This code is run when the package is loaded and usually located in a zzz.R script:

.onLoad <- function(...) {
  registerInputHandler(
    "OSUICode.textDate", function(data, ...) {
      if (is.null(data)) {
        NULL
      } else {
        res <- try(as.Date(unlist(data)), silent = TRUE)
        if ("try-error" %in% class(res)) {
          warning("Failed to parse dates!")
          # as.Date(NA)
          data
        } else {
          res
        }
      }
  }, force = TRUE)
}

shiny already handles dates and we could use the built-in input handler. The current handler was only designed to explain the underlying processes!

On the JavaScript side, we refer to the OSUICode.textDate defined input handler. We recover the data-type value passed from R and call the handler if the type is a date. We return false otherwise, which is the default behavior:

getType: function getType(el) {
  var dataType = $(el).data("data-type");
  if (dataType === "date") return "OSUICode.textDate";
  // if (dataType === "date") return "shiny.date"
  else return false;
}

To use the shiny built-in handler we could return "shiny.date" instead. We then run:

which sets the value as text by default. After opening the HTML inspector and setting a break point in the getType method (Figure 12.18), we check that the data type is not defined. Therefore the input handler will not apply.

Example where getType does not call the input handler

FIGURE 12.18: Example where getType does not call the input handler

For the second example, we give a date value to the function:

As illustrated Figure 12.19, the date is properly processed. Moreover, if you type any other valid date in the text field like 2020-11-12, it will be recognized as a date, while entering a text will return a character element. This is a way to obtain a slightly more clever text input widget.

Passing a date to a text input correctly processes it

FIGURE 12.19: Passing a date to a text input correctly processes it

Importantly, since the data-type is set at app startup by checking the class of the value, it will never change later. For instance, if you start the app with the text input value to be a simple text, setting it to a date through the app does not convert it into a date since $(el).data("data-type") always return undefined! Therefore, if you want to be able to use both text and dates, be sure to wisely set the initial value.

To finish, we could seamlessly make our text input even more clever, by handling numbers. Even though shiny has a shiny.number input handler, it simply makes sure that whenever the input is missing a value, NA is returned instead of "". (Figure 12.20).

shiny.number input handler ensures that an empty numericInput returns NA instead of ""

FIGURE 12.20: shiny.number input handler ensures that an empty numericInput returns NA instead of ""

What we want is an handler that recognizes the string "1" and convert it to a number. In R, converting a string to a number gives NA:

as.numeric("test")
## Warning: NAs introduced by coercion
## [1] NA

Therefore, if we obtain NA, we return original data so that the input gives the correct type. Right after our previous handler, we can write:

registerInputHandler(
  "OSUICode.textNumber", function(data, ...) {
    if (is.null(data)) {
      NULL
    } else {
      res <- as.numeric(unlist(data))
      if (is.na(res)) {
        data
      } else {
        res
      }
    }
  }, force = TRUE)

We also update the JavaScript getType method as follows:

getType: function getType(el) {
  var dataType = $(el).data("data-type");
  if (dataType === "date") return "OSUICode.textDate";
  else if (dataType === "number") return "OSUICode.textNumber";
  else return false;
}

On the R side, don’t forget to add an extra else if statement to the customTextInput() function:

type <- if (inherits(value, "Date")) {
  "date"
} else if (inherits(value, "numeric")) {
  "number"
} else {
  NULL
}

If we run:

we obtain the desired behavior shown Figure 12.21.

Passing a number to a text input correctly processes it

FIGURE 12.21: Passing a number to a text input correctly processes it