Why would I need this?

In the shiny framework, there are a lot of UI components that are easy to use and to fit with your data. If your data contains time values that may play a big role in the interactivity of your application, you would want a time input element that allows uses to filter data by a time or by a period of time. You may notice that a time input element does not exist in the shiny framework as a shiny control widget.

This is not a problem as it is possible to create your own using Shiny Input Bindings. In this tutorial, I will cover how to structure the HTML element and write the JavaScript input bindings.

It is important to note that not all browsers widely support input[type='time']. See Can I Use: date and time inputs page for more information. The purpose of this example is to demonstrate how to create custom input components and Shiny input bindings.

How does this shiny app work?

In this tutorial, I will focus on the following steps to get the component working.

  1. Building the time input component
  2. Writing the shiny input binding

See the GitHub repository for the complete code.

Building the time input component

To create the input element, we will use the HTML element input. There are many input types available, but we are only interested in the type time. We will stick with the basic attributes: id, name, class, value, min, and max attributes.

In shiny, the <input> element can be accessed using tags$input(). All inputs must have an accompanying label (<label>; tags$label) that is linked with the input using the for attribute. The value of for should be the id of the input element.

In addition to the label element, you may need to a caption to provide addition notes about the time input. This argument will be optional. If it is used, then content should be placed inside the label element.

Here is the time input component.

time_input <- function(inputId, label, value = "13:00", min = "07:00", max = "10:00", caption = NULL) {

    # <label />
    lab <- shiny::tags$label(
        class = "time__label",
        `for` = inputId,
        label
    )

    # if present, append caption to <label>
    if (!is.null(caption)) {
        lab$children <- shiny::tagList(
            lab$children,
            shiny::tags$span(
                class = "time__caption",
                caption
            ),
        )
    }

    # <input type="time" />
    input <- shiny::tags$input(
        id = inputId,
        name = inputId,
        class = "time__input",
        type = "time",
        min = min,
        max = max,
        value = value
    )

    # return
    shiny::tagList(lab, input)
}

The attributes min and max can be used to validate the input value. Some browsers may provide some errors natively, but this does not replace robust validation methods and client-side error messages. For more information, see Mozilla's time input reference guide.

Writing the Shiny Input Binding

Next, we will create shiny input binding for the time component. I will cover the methods required for this example. For a more detailed description of input bindings, see RStudio's Shiny input bindings guide.

New input bindings can be created using new Shiny.InputBinding(). Use JQuery's extend function to define the methods specific to the component. (It is not possible to write the binding using vanilla JS). Lastly, register input bindings using Shiny.inputBindings.register(...). Here is the basic structure.

// create new binding
var myInput = new Shiny.InputBinding();

// extend: define methods
$.extend(myInput, {
    ...
});

// register
Shiny.inputBindings.register(myInput);

There are several methods available for creating custom input bindings. In this example, we will use the following methods.

  • find: (required) this method is used to locate the time component within the web document
  • initialize: this method will run when the component is initialized (i.e., rendered). This is also useful for setting the initial value of the component (otherwise, the starting value will be NULL).
  • getValue: getValue returns the value of the time input component so it can be accessed in the Shiny server using input$some_id.
  • subscribe: this is used for attaching events to the time component (i.e., click, change, etc.). Depending on the event(s), you may want to use the callback() function. Doing so will run the getValue method.

Here is the input binding for the time component.

// init new binding
var timeInput = new Shiny.InputBinding();

// extend class
$.extend(timeInput, {

    // locate all instances of input[type='time']
    find: function(scope) {
        return $(scope).find(".time__input");
    },

    // return default value defined by the attribute `value`
    // this will also reset the input to it's default value
    // on page refresh
    initialize: function(el) {
        return el.value = $(el).attr("value");
    },

    // callback function: when called, return the current input value
    getValue: function(el) {
        return el.value;
    },

    // events: when input is changed, return the value
    subscribe: function(el, callback) {
        $(el).on("change", function(e) {

            // callback; i.e., run `getValue`
            callback();
        });
    }
});

// register
Shiny.inputBindings.register(timeInput)

What do I need to know before I implement this into my own project?

Creating custom input components is fairly straightforward. There are a couple of things that you should consider before implementing this method into your application.

To follow good semantic HTML and web accessibility practices, input components should be wrapped in a form element: tags$form(...). Include a title for that describes the form using the legend element: tags$legend(). For accessibility, make sure the legend is linked with the form element using aria-labelledby and reference the id of the legend.

tags$form(`aria-labelledby`="form-time-legend",

    # form legend
    tags$legend(id="form-time-legend","My title for the form"),

    # time input
    time_input(...),

    # other inputs if applicable
    ...

    # submit
    tags$button(type="submit", id="submit", class="action-button shiny-bound-input", "Enter")
)

It is important to point out that the input, by default, returns time in the 24-hour format. If you would like the component to return 12-hour format, than you can restructure the initialize and getValue methods to the following. Alternately, you can use R to convert the times from 24-hour to 12-hour format.

// method now calls `getValue`
intialize: function (el) {
    el.value = $(el).attr("value");
    this.getValue();
},

// method: returns time in 12-hour format
getValue: function (el) {
    var val = el.valueAsDate;
    var time = val.toLocaleString("en-us", { hour: "numeric", minute: "numeric" });
    return time;
},

How do I run the demo?

The code for the shiny application can be found on GitHub. Either clone the repository and run the app in locally or you can run the application by running the following code in the console.

shiny::runApp(
    repo = "shinyAppTutorials",
    username = "davidruvolo51",
    subdir = "time-input"
)