How to develop an interactive, dynamic help system for your app with introJS
Introduction
In the previous tutorials, we focused on using JavaScript to create htmlwidgets based on C3. To do this, we used three pivotal functions:
-
the R function
sendCustomMessage
, which you can call via the session object, i.esession\(sendCustomMessage</code></li> <li>the JavaScript function <code>Shiny.onInputChange</code></li> <li>the JavaScript function <code>Shiny.addCustomMessageHandler</code>.</li> </ol> <p>In this tutorial, we will show you how to use the same functions to create a dynamic interactive help system for Shiny apps, based on the JavaScript library <a href="https://introjs.com/">introJS</a>.</p> <p>IntroJS lets you create a step-by-step guide for a website. It will draw a nice box around elements of your choice, combined with an annotation layer and a navigation system. Click the green button in the example below to see an introJS ‘hello-world’ example in action.</p> <iframe src="https://introjs.com/example/hello-world/index.html" width="100%" height="600px" frameborder="0" scrolling="no"> </iframe> </div> <div id="intro.js-basics-and-jsfiddle" class="section level2"> <h2>Intro.js basics and jsFiddle</h2> <p>Using introJS in a piece of HTML code is very simple. Let’s first set up a jsFiddle example that uses a <a href="https://getbootstrap.com/">bootstrap</a> grid of 3 rows, with 3 columns each. In addition, let’s add a button that you can click to activate introJS.</p> <iframe width="100%" height="500px" src="https://jsfiddle.net/ok1vzopq/6/embedded/result,html,js,css" allowfullscreen="allowfullscreen" frameborder="0"> </iframe> <div style="text-align:center"> press the green start button to activate introJS </div> <p><br><br> We can add introJS to the app in three steps, described in detail <a href="https://github.com/usablica/intro.js/">here</a>:</p> <ol style="list-style-type: decimal"> <li><p>Include <a href="https://raw.githubusercontent.com/usablica/intro.js/master/introjs.css">introjs.css</a> and <a href="https://raw.githubusercontent.com/usablica/intro.js/master/intro.js">intro.js</a> with your page.</p></li> <li><p>For each element in the tour, add a <code>data-intro</code>, a <code>data-position</code>, and an optional <code>data-step</code> attribute. <br><br> These respectively specify the display text, the position of the box displayed by introJS, and the step number of the tour.</p></li> <li><p>In order to start the tour, in JavaScript, call <code>introJs().start();</code>.</p></li> </ol> <p>In the HTML tab of our fiddle, we see that the first row is created via:</p> <div class="sourceCode"><pre class="sourceCode html"><code class="sourceCode html"><span class="kw"><div</span><span class="ot"> class=</span><span class="st">"row"</span><span class="kw">></span> <span class="kw"><div</span><span class="ot"> class=</span><span class="st">"col-sm-4 well"</span><span class="ot"> data-step=</span><span class="st">"1"</span><span class="ot"> data-intro=</span><span class="st">"text step 1"</span><span class="ot"> data-position=</span><span class="st">'bottom'</span><span class="kw">></span>element 1<span class="kw"></div></span> <span class="kw"><div</span><span class="ot"> class=</span><span class="st">"col-sm-4 well"</span><span class="ot"> data-step=</span><span class="st">"2"</span><span class="ot"> data-intro=</span><span class="st">"text step 2"</span><span class="ot"> data-position=</span><span class="st">'bottom'</span><span class="kw">></span>element 2<span class="kw"></div></span> <span class="kw"><div</span><span class="ot"> class=</span><span class="st">"col-sm-4 well"</span><span class="ot"> data-step=</span><span class="st">"3"</span><span class="ot"> data-intro=</span><span class="st">"text step 3"</span><span class="ot"> data-position=</span><span class="st">'bottom'</span><span class="kw">></span>element 3<span class="kw"></div></span> <span class="kw"></div></span></code></pre></div> <p>The classes <code>row</code>, <code>col-sm-4</code>, and <code>well</code> are all bootstrap classes, which define the grid layout. <a href="https://getbootstrap.com/">Bootstrap</a> is the most popular HTML, CSS, and JavaScript framework for developing responsive, mobile first projects on the web. For some inspiration, see <a href="https://www.w3schools.com/bootstrap/bootstrap_grid_examples.asp">these</a> examples. For more complete use cases, see <a href="https://getbootstrap.com/getting-started/#examples">these</a> templates.</p> <p>Of note, many layout functions in Shiny are directly based on bootstrap. For instance, in a Shiny context the R code snippet:</p> <pre><code>fluidRow( column(4,"element 1") )</code></pre> <p>produces an output that resembles row 1 in our example (but without the introJS attributes and well classes).</p> <p>Coming back to introJS, from the HTML code in the example above we see that it is quite easy to add the <code>data-step</code>, <code>data-intro</code> and <code>data-position</code> attributes.</p> <p>Next, in the JavaScript tab of our fiddle, we’ve attached an event handler to our button using <a href="https://jquery.com/">jQuery</a> (see also <a href="https://shiny.rstudio.com/tutorial/js-lesson3/">tutorial 3</a>). Here we use the event handler to start introJS on a button click, i.e. via the JavaScript code:</p> <div class="sourceCode"><pre class="sourceCode javascript"><code class="sourceCode javascript"><span class="at">\)("button").on("click", function(){ introJs().start(); })
In the CSS tab of our jsFiddle, we’ve set some styling options to center the button and the text in the example. In the CSS definitions we use flexbox to center the button. Even though flexbox is by no means a focus point of this tutorial and is a relative new addition to the CSS language, it greatly enhances your abilities to create page layouts via CSS. For a complete guide to flexbox, see this article by CSS-tricks!
Finally, you may wonder, where did we included the JavaScript and CSS file from step 1? You can find the links when pressing the button, located at the top right in our example fiddle. This opens up a new display with a panel on the left hand side that contains an External Resources tab, which includes all the external dependencies for our jsFiddle, i.e. the introjs CSS and JavaScript files, Bootstrap and jQuery.
Of note, in a standard Shiny app you don’t need to explicitly load either Bootstrap or jQuery as they are included automatically by Shiny. However, if you base your app on HTML Templates, then be careful you don’t mistakenly include multiple jQuery or Bootstrap instances. In case you need to, you can also suppress specific web dependencies via suppressDependencies. This function can be helpful if distinct widgets include different versions of the same library e.g. different versions of d3.js.
Using JSON to setup introJS
Even though the previous example creates a tour, it’s not very convenient to manually set all the attributes for each element. Luckily, introJS allows you to use a JSON array to specify all relevant options, in which each step can use a CSS selector to indicate where we want to draw a box, what should be in it and where the box should be displayed relative to the selected element, among other things. The fiddle below shows an implementation using this concept.
press the green start button to activate introJS
The output is identical to the fiddle in the previous section. However, you can see in the HTML tab that we greatly simplified the HTML markup and now only need an id field. Furthermore, in the JavaScript tab you can see that we specified an array that contains objects that describe the individual steps in the tour. The array for the first two steps looks like this:
var Steps = [
{
element: '#step1',
intro: "text step 1",
position: 'bottom'
},
{
element: '#step2',
intro: "text step 2",
position: 'bottom'
}
];
Finally, we created an instance of introJS, loaded the data via the setOptions
method and subsequently started the tour. The relevant code looks like this:
// initialize an introjs instance
var intro = introJs();
// pass in the Steps array created earlier
intro.setOptions({steps: Steps });
// start intro.js
intro.start();
Note that all of this code is located inside the callback function for our button click event handler. Hence, this code will only fire after we press the button!
Customization and additional methods
IntroJS is a versatile library, which offers many additional functions and options next to those discussed here. For a more comprehensive overview of additional attributes that allow customization, see here.
In addition, introJS offers a rich set of methods such as: introJs.nextStep()
, introJs.previousStep()
and introJs.exit()
.
These methods allow you to control the direction of the guided tour. For many of these methods, introJS let’s you call a JavaScript function, before, during or after completion, e.g. via: introJs.onchange
, introJs.oncomplete
and introJs.onexit
.
A complete list of methods is provided here.
Caveat: for some functionality in more recent versions of introJS (≥ 2), an inexpensive licence is required for commercial applications. All functionality in this tutorial work with earlier versions though.
Using introJS in a Shiny app
Now that we have a better grasp on what introJS offers, let’s see how we can use introJS in a Shiny context. The idea is quite simple.
From the previous tutorial, we know that we can call any JavaScript function from R by using session$sendCustomMessage
, which sends a message to an event handler, which we can create via Shiny.addCustomMessageHandler
. Furthermore, we can use Shiny.onInputChange
inside a JavaScript function to send a message back to the Shiny input
object, e.g. when we click on a box.
Now that we know which JavaScript functions to call and what our data should look like, we can setup Shiny to make these calls.
For our first Shiny based introJS app, we are not going to put the code in an R package. This makes the steps easier to follow. It also makes it easier for you to experiment with the code. For readers who just want to use introJS in a Shiny app, without knowing all the details, we made an R package that you can download and use. We explain how to use this package in the next section.
A live version of the end result of a first basic Shiny implementation can be found here and the source code can be found here.
Click the image to see a live shiny app!
We proceed by discussing the various steps needed to make the above demo app work and how you can start making your own JavaScript powered apps.
Create small toy examples first
When you’re building new Shiny functionality, it is often useful to first build a small prototype in HTML and JavaScript (without Shiny). This let’s you focus on:
- which CSS and JavaScript files to include
- how to structure your data
- how to call specific library functions
Essentially, this is the role that our jsFiddle examples fill in our tutorials. After you’re comfortable that your mini application works the way you want, the sole trick is to let Shiny include the files you need, and to use Shiny to build the HTML elements you need. After that, just let Shiny call whatever JavaScript function(s) you need to call and pass any data via jsonlite (Shiny will implicitly do this for you, see here).
In the case at hand, to go from the fiddle shown above, to a Shiny app, we have to implement the following steps:
- Make Shiny include the correct CSS and JavaScript files
- Let Shiny spit out the HTML you need
- In R, send a custom message to the client i.e. JavaScript, with the data we need introJS to have
- In JavaScript, setup a custom message handler that can call introJS and pass it the data it needs
Creating the UI code
Coming back to our jsFiddle, note that many layout functions rely on bootstrap classes. In fact, in Shiny, the fluidRow
and column
functions create divs with a row
and col-sm-x
class, respectively. Hence, these functions almost provide the HTML output of our first introJS fiddle example. We only need to add an extra div to each column to make it right.
First, we start by copying the CSS we defined in the fiddle into a separate file, i.e. app.css
, such that we can use the custom well
and flexcontainer
classes.
The ui.R code snippet below shows how to get the first row of elements, including the start button, as well as how to load all of the dependencies that we need. We also include a file called app.js
, which contains all of the client side code that we need to update the help contents and start the help (see below).
# Include IntroJS styling
includeCSS("introjs.min.css"),
# Include styling for the app
includeCSS("app.css"),
# Include IntroJS library
includeScript("intro.min.js"),
# Include JavaScript code to make shiny communicate with introJS
includeScript("app.js")
# setup grid
# row 1
fluidRow(
column(4, div(id="step1", class="well", "element1")),
column(4, div(id="step2", class="well", "element2")),
column(4, div(id="step3", class="well", "element3"))
),
...
# centered button
div(class="flexcontainer",
# action button
actionButton(inputId="startHelp", label="start", class="btn-success")
)
Send help contents from the server to the client
Next, let’s see what we should put in app.js
.
The first thing we need to do is to initiate introJS. We can do this exactly like in our fiddle i.e. via var intro = introJs();
. Next, instead of setting the help contents in JavaScript, we want to be able to set the help contents dynamically from Shiny.
As mentioned above, we can use the JavaScript method Shiny.addCustomMessageHandler
to create a custom message handler that we can invoke from R. In our case, the handler must be able to receive tour data in the same format as described in the fiddle above and to pass this data to the introJS object intro
.
Putting these steps together results in the following JavaScript code:
// initialize an introjs instance
var intro = introJs();
// handler 1
Shiny.addCustomMessageHandler("setHelpContent",
// callback function.
// note: data is passed by shiny and contains the tour data
function(data){
// load data
intro.setOptions({steps: data});
}
);
Our handler is called setHelpContent
, which is the name we’ll use when sending information from R via session\(sendCustomMessage</code> (see below).</p> <p>To conveniently configure our help system, we can put the help contents into a comma separated configuration file, e.g.<code>help.csv</code>.</p> <p>In our case, <code>help.csv</code> looks like this:</p> <div id="htmlwidget-8392" style="width:100%;height:auto;" class="datatables html-widget"></div> <script type="application/json" data-for="htmlwidget-8392">{"x":{"filter":"none","data":[[1,2,3,4],["This is a generic welcome message, press the buttons below the navigate trough the elements of this page","This help page shows content for a specific element. Specific elements are targeted in the help.csv file by using a css selector in the third column.","Using the selector and step columns in the help.csv file we can create a tour over all elements of the page","You can also use html to <b>mark up</b> the <i>text</i> thats displayed in the help."],[null,"#Gauge1","#PieLabel","#lineBarChart"],["auto","auto","auto","auto"]],"container":"<table class=\"display\">\n <thead>\n <tr>\n <th>step\u003c/th>\n <th>intro\u003c/th>\n <th>element\u003c/th>\n <th>position\u003c/th>\n \u003c/tr>\n \u003c/thead>\n\u003c/table>","options":{"dom":"t","columnDefs":[{"className":"dt-right","targets":0}],"order":[],"autoWidth":false,"orderClasses":false}},"evals":[],"jsHooks":[]}</script> <p><br><br> From left to right, the columns respectively indicate: the step number, the display text for that step, the CSS selector and the position in which we want the text box to appear. The value for the latter option can be either <code>top</code>, <code>left</code>, <code>right</code>, <code>bottom</code>, <code>bottom-left-aligned</code>, <code>bottom-middle-aligned</code>, <code>bottom-right-aligned</code> or <code>auto</code>. If left empty, the default option equals <code>bottom</code>.</p> <p>Once we have our dataframe ready to go (here called <code>steps</code>), we can send it to introJS via:</p> <div class="sourceCode"><pre class="sourceCode r"><code class="sourceCode r"><span class="co"># set help content</span> session\)sendCustomMessage(type = ‘setHelpContent’, message = list(steps = toJSON(steps) ))
Essentially, you can use one of three basic strategies to send information that introJS needs from R to JavaScript:
- Send the information from R in a convenient R structure. Then further modify the object in JavaScript to create the object JavaScript needs.
- Reshape the information in R so that when Shiny passes it as jsonlite (see tutorial 2 & 3), it ends up as the precise object that JavaScript needs.
- A combination of strategies 2 and 3.
Note that we didn’t send steps
from R directly. Instead, we sent toJSON(steps)
, which means we went with strategy 2. This is only to ensure that JavaScript gets the object it needs. In practice, this step often needs some experimentation to see how you can best send an object from R to JavaScript. Similar logic holds for sending objects from JavaScript to R.
Start the help from the server
All that remains is to create a mechanism to start the help from the server. As you might have guessed, this mechanism is again constructed via a custom message handler, in which we invoke introJS.start()
:
// handler 2
Shiny.addCustomMessageHandler("startHelp", function(message) {
// start intro.js
// note: we don't need information from shiny, just start introJS
intro.start();
}
);
Note that we could also directly add an onClick attribute to our button like we did in the examples above. An advantage of the approach taken here is that we have more control over our help because we can now invoke it from the server i.e. Shiny.
To invoke the help from the server, we add an observeEvent
block in R. This block listens to our button and calls sendCustomMessage
to invoke our startHelp
handler as defined above:
# listen to the action button
observeEvent(input$startHelp,{
# on click, send custom message to start help
session$sendCustomMessage(type = 'startHelp', message = list(""))
})
introJS demo package
Below we present a Shiny demo app that contains a compete implementation of the various aspects discussed above. For convenience, we made the code available as an R package.
We won’t cover the complete code base in detail as this involves various aspects related to package building e.g. using addResourcePath to include dependencies, which is outside the scope of this tutorial. The complete code for this example app can be downloaded here, while the accompanying R package can be downloaded here.
In short, the R library lets you:
- Load a definition of the help content via a dataframe, which is subsequently transformed into a JSON array (like in the jsFiddle above)
- Automatically add a help button to the page to activate introJS
- Automatically add all introJS related JavaScript and CSS dependencies to the app
- Automatically create an introJS instance
- Automatically pass the help settings from step 1 to the introJS instance
To see the package in action, press the screenshot below!