The sky is not the limit – embedding raw HTML and JavaScript to create dynamic UI elements in Shiny applications

October 17, 2016 | Yuri

One of the key advantages of Shiny, the interactive visualisation web application framework for R, is that it allows users to deploy interactive applications straight from within R, without having to write HTML, CSS and/or JavaScript. Shiny does a lot of heavy lifting under the hood, as it comes with a set of pre-configured functions that generate HTML body and widgets with already defined I/O reactive bindings. This saves Shiny users from having to program those aspects themselves and instead allows them to focus on doing analysis and achieving desired outputs, by simply defining interactive widgets, and linking them with computation functions. Back in 2015 we collaborated with RStudio to integrate Shiny into AnalytiXagility in order to bring rapid prototyping capability to our workspaces, allowing users to quickly and easily visualise their data and accelerate the development and deployment of analytically-driven applications. When I started building mini-apps in AnalytiXagility, I found it very easy to rapidly prototype an application. I could simply configure widgets as inputs, plug them into the custom functions and manipulate the data on demand. From there, all I have to do is to define an output (e.g. a filtered table or a plot), and I get an interactive application without having to think about how to make Inputs and Outputs aware of each other – Shiny handles that splendidly. mini-apps That said, I often found myself wanting to include elements which can be dynamically added and removed (if required). This blog post will therefore be the first in a series of extending mini-apps in various ways to make them more dynamic and customisable. Since Shiny is primarily designed to be run in the browser with HTML, CSS and JavaScript in mind, we will discuss various ways of including these to enhance our applications by creating custom HTML widgets and linking all up into an enhanced dynamic application. In today’s example, I’ll be using raw HTML, embedding it in R to create a custom UI element, and working with JavaScript to make the UI elements dynamic by allowing users to add and remove them if wanted.

The basics

If you’re not familiar with Shiny I strongly recommend skimming through RStudio’s tutorial on Shiny and our blog post on getting started with AnalytiXagility mini-apps. First, let’s create ui.r and server.r files to initiate an empty Shiny mini-app.

ui.r
library(shiny)

# Define UI space for our application
shinyUI(fluidPage(
  
  # UI code goes here
))
server.r
# Define server logic
shinyServer(function(input, output) {
  
   # Server code goes here
})

Creating HTML components

Shiny comes with functions called tags which incorporate various HTML tags and can be called directly in the UI and included as part of the mini-app’s HTML. If we print the outputs from tags you’ll see that they contain HTML code. We can combine parts of the UI by using tags or use HTML() function to include raw HTML. For more on this check tutorial on HTML tags.

# Defining tags ui
tags_UI <- tags$div(# HTML div
  tags$p('This is 1st Paragraph'),
  tags$p("This is 2nd paragraph, check the link to the tutorial!")
)

print(tags_UI)
## <div>
##   <p>This is 1st Paragraph</p>
##   <p>This is 2nd paragraph, check the link to the tutorial!</p>
## </div>
# Defining raw html ui
raw_UI <- HTML(
  '<div>
    <p>This is 1st Paragraph</p>
    <p>This is 2nd paragraph, check the link to the tutorial!</p>
  </div>'
)

print(raw_UI)
## <div>
##    <p>This is 1st Paragraph</p>
##    <p>This is 2nd paragraph, check the link to the tutorial!</p>
## </div>

We can see that the returned output is the same for both methods. This allows us to easily define elements of the UI via tags or raw HTML (another useful technique for combining multiple UI elements is by using Shiny modules; check out this blog on using Shiny modules for more information. Now, let’s define a widget for us to work with using the raw HTML method. In this example, I will create a meeting attendance form, containing three entry fields: “Name”,“Department” and “Email”. I’ve chosen this example because normally the number of attendees differs from meeting to meeting, therefore we want our users to dynamically add and remove entry forms for each attendee. We also add a remove button of CSS class ‘remover’ to each dynamic form, as we want to be able to add and remove attendees as we go. Personally I like to keep defined widgets in a separate R script widgets.r and then source it on the initiation of the mini-app.

widgets.r
# Creating raw HTML widget form containing ("Name","Department","Email") fields and Remove button of CSS class 'remover'

attendeeForm <- HTML('
  <p>

    <label for="name">Name: </label>
    <input style="width: 150px;" id="name" type="text" placeholder="Enter name", class="name">
                     
    <!--Department field-->
    <label for="department">Department: </label>
    <input style="width: 150px;" id="department" type="text" placeholder="Enter department", class="department">
                     
    <!--Email field-->
    <label for="email">Email: </label>
    <input style="width: 150px;" id="email" type="text" placeholder="example@email.com", class="email">
                     
    <!--Remove button-->
    <input type = "button" class="remover" value = &#10008>
  </p>
')

HTML(attendeeForm)

form We have now defined the custom attendee form, but at the moment this only renders the widget and isn’t interactive. This means that if we insert values into the widget the Shiny server won’t recognise it as an input – we will come back to this later.

Adding JavaScript functionality

Now, let’s add the ability to dynamically add and remove our attendee forms. Again, there are several ways of including JavaScript functions in our mini-apps; by embedding them in raw HTML in tags$script, or creating a .js file in www directory and then sourcing it in the mini-app using includeScript(). In this example we will embed JavaScript function directly in the ui.r. First, let’s add a button which will trigger adding the attendee form element. Here, we also create a div element to which we will be adding attendee forms.

ui.r
library(shiny)
library(stringi)

source("widgets.r") # Source our widget

shinyUI(fluidPage(
    # First lets create a button which will trigger adding a new attendee form.
    actionButton('add_Button', 'Add attendee!'),
   
     # Now, lets create a DIV element to which we will be adding attendee forms.
    div(id="my_div")
))

Now, lets read our attendeeForm widget defined in widgets.r. Since we defined our form string in R, we might want to use the stringi package to remove special UNICODE trailing spaces which could cause the JavaScript to fail.

ui.r
# Using stringi package to remove UNICODE trailing spaces
attendeeForm <- stri_replace_all_charclass(attendeeForm, "\\p{WHITE_SPACE}", " ")

Now, let’s define a JavaScript function to dynamically add attendeeForm to our UI. We pass defined attendeeForm inside the function using sprintf.

tags$head(HTML(sprintf(
  "<script>
    function addAttendee() {
      var attendee = \'%s\'
      $('#my_div').append(attendee)
    }
  </script>", attendeeForm)))

Now, lets add another function which will trigger the addAttendee() function when we click ‘add_Button’. We must wrap the function to the event handler shiny:connected to initiate the state when the application is ready.

# On click event to triger addAttendee function
tags$head(HTML("
  <script>
    $(document).on('shiny:connected', function(event) { 
      $('#add_Button').on('click', function() {addAttendee()})
    })
  </script>
"))

And finally, let us add a removeAttendee() function. This function is triggered when any button of CSS ‘remover’ class is clicked. It then removes the parent element of the button – which is a paragraph tag bounding our attendeeForm object.

# Trigger removeAttendee function when button of class 'remover' is clicked.
tags$head(HTML("
  <script>
    function removeAttendee(el) {
      $(el).parents(\"p\").remove()  
    };

    $(document).on('click','.remover',function() {
      removeAttendee(this)
    });   
  </script>
"))

Now, when we insert these functions into our ui.r file, we can dynamically add a pre-defined attendeeForm as many times as we want, as well as easily remove it by pressing on ‘X’ sign next to it.

Use JavaScript to extract data from dynamic widgets

One last touch is to make our forms interact with the Shiny server and pass entered fields to the attendee list. In this example, we don’t want to directly identify entry forms and differentiate them by utilising some form of unique id, since they are all identical. Because of that, we can simply use JavaScript to scoop data from all forms and Shiny.onInputChange() to send data to the server function. First, let’s add a button to confirm attendees.

actionButton('confirm_list', 'Confirm attendees'),

tags$head(HTML("
  <script>
    $(document).on('shiny:connected', function(event) { 

      $('#confirm_list').on('click', function() {getAttendees()})
                 
    });               
  </script>
"))

And define JavaScript function getAttendees() which I will place in the script.js file located in the www directory.

ui.r
tags$head(tags$script(src = 'script.js')),
www/script.js
function getAttendees() {
  var name = [];
  var department = [];
  var email = [];
  var attendees = [];
     
  $(".name").each(function() {
    name.push($(this).val())
  })

  $(".department").each(function() {
    department.push($(this).val())
  })
    
  $(".email").each(function() {
    email.push($(this).val())
  }) 
    
  Shiny.onInputChange("attendees", {name, department, email})
}

Now, when I click the confirm_list button, this will trigger the getAttendees() function which will scoop all values of entry fields and combine them to lists. Then, these lists are passed to the R server using Shiny.onInputChange call. This will return outputs of the function and recognise them as input$attendees in server.r. Next, I will create a table output in the ui.r and link it up with a data frame defined from input$attendees.

ui.r
tableOutput('attendee_table')
server.r
output$attendee_table <- renderTable({
  df <- data.frame(Name = unlist(input$attendees$name),
                   Department = unlist(input$attendees$department),
                   Email = unlist(input$attendees$email))
  df  
})

What’s next?

output Normally, we would pre-define a number of UI elements displayed to the user in a Shiny mini-app. This example allows us to add an arbitrary number of identical UI elements, and also remove them if needed. This is useful in the examples when we want users to be able to dynamically create and destroy elements in the UI and not limit them only to what is pre-configured. Such methods might be useful when our elements are similar or identical, such as in form-filling situations and data collection. Today, we went through a simple example of using dynamic UI functionality. In future posts I’ll go further to demonstrate how to create specific UI elements and bind them with Shiny I/O, add JavaScript-driven controls, and create draggable and droppable elements. I hope that this post showed that the sky is not the limit when it comes to Shiny, and how our AnalytiXagility mini-apps can be made even more dynamic to suit a range of requirements.


 

yuri

Yuri joined Aridhia in early 2016 with an MSc in Data Engineering from the University of Dundee. A key member of the Enablement team and an enthusiastic and creative data scientist, Yuri is a champion for high quality data-driven decision-making across the company and beyond. Outside of work Yuri puts his energy into volunteering and lifelong learning, including sitting as a member of a local TEDx organising committee.

Comments (2)

  1. Keith Bailey Reply

    November 16, 2017 at 9:35 pm

    FYI, your code has a slight error in it. Your widget.R where you build the html input boxes should be defined as class=”name” etc otherwise your script.js won’t actually find the elements to be able to add them to each array.

    1. Harry Peaker Reply

      November 17, 2017 at 1:23 pm

      Thanks for the feedback Keith, this is indeed an error in the code.
      I have edited the widget.R script so that the class is now defined as you describe.

Leave a Reply

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