4 Dependency utilities

When creating a new template, you have to import custom HTML dependencies that are not available in shiny. Fortunately, this is not a problem with htmltools (Cheng, Sievert, et al. 2021)!

4.1 The dirty approach

Let’s consider the following example. Since Bootstrap is one of the most popular HTML/CSS/JS framework to develop websites and web apps, we want to include a Bootstrap 4 card in a shiny app. This case study is taken from a RStudio Community question. The naive approach would be to include the HTML code directly in the app code. This approach is dirty since it is not easily re-usable by others.

library(shiny)
# we create the card function before
my_card <- function(...) {
  withTags(
    div(
      class = "card border-success mb-3",
      div(class = "card-header bg-transparent border-success"),
      div(
        class = "card-body text-success",
        h3(class = "card-title", "title"),
        p(class = "card-text", ...)
      ),
      div(
        class = "card-footer bg-transparent border-success",
        "footer"
      )
    )
  )
}

# we build our app
shinyApp(
  ui = fluidPage(
    fluidRow(
      column(
        width = 6,
        align = "center",
        br(),
        my_card("Card Content")
      )
    )
  ),
  server = function(input, output) {}
)
Attempt to display a Bootstrap 4 card without dependencies

FIGURE 4.1: Attempt to display a Bootstrap 4 card without dependencies

As depicted by Figure 4.1, nothing is displayed which was expected since shiny (Chang et al. 2021) does not contain Bootstrap 4 dependencies. Don’t panic! We load the necessary css to display this card (if required, we could include the javascript as well). We could use either includeCSS(), tags$head(tags$link(rel = "stylesheet", type = "text/css", href = "custom.css")), as described in the shiny documentation here. Web development best practice recommend to point to external file rather than including CSS in the head or as inline CSS (see chapter 6). In the below example, we use a CDN (content delivery network) but that could be a local file in the www/ folder:

bs4_cdn <- "https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/"
bs4_css <- paste0(bs4_cdn, "css/bootstrap.min.css")
shinyApp(
  ui = fluidPage(
    # load the css code
    tags$head(
      tags$link(
        rel = "stylesheet",
        type = "text/css",
        href = bs4_css)
    ),
    fluidRow(
      column(
        width = 6,
        align = "center",
        br(),
        my_card("Card Content")
      )
    )
  ),
  server = function(input, output) {}
)

The card may seem ugly but at least it is displayed as shown on Figure 4.2.

Bootstrap 4 card within a simple app

FIGURE 4.2: Bootstrap 4 card within a simple app

4.2 The clean approach

The best approach consists in leveraging the htmlDependency() and attachDependencies() functions from htmltools. htmlDependency() takes the following main parameters:

  • A name.
  • A version (useful to remember on which version it is built upon).
  • A path to the dependency (can be a CDN or a local folder).
  • script and stylesheet to respectively pass css and scripts.
# handle dependency
card_css <- "css/bootstrap.min.css"
bs4_card_dep <- function() {
  htmlDependency(
    name = "bs4_card",
    version = "1.0",
    src = c(href = bs4_cdn),
    stylesheet = card_css
  )
}

If you are not pointing to a CDN and use local files, this is crucial to wrap the newly created dependency in a function since the path has to be determined at run time and not when the package builds. It means that if you are developing a package with dependencies, forgetting this step might prevent other users to get the dependency working on their own machine (the differences between Unix and Windows OS paths is clearly a good example).

We create the card tag and give it the Bootstrap 4 dependency through the attachDependencies() function.

In recent version of htmltools, we may simply use tagList(tag, deps) instead. Importantly, attachDependencies() has an append parameter FALSE by default. Ensure to set it to TRUE if you want to keep already attached dependencies!

# create the card
my_card <- function(...) {
  cardTag <- withTags(
    div(
      class = "card border-success mb-3",
      div(class = "card-header bg-transparent border-success"),
      div(
        class = "card-body text-success",
        h3(class = "card-title", "title"),
        p(class = "card-text", ...)
      ),
      div(
        class = "card-footer bg-transparent border-success",
        "footer"
      )
    )
  )

  # attach dependencies (old way)
  # htmltools::attachDependencies(cardTag, bs4_card_dep())

  # simpler way
  tagList(cardTag, bs4_card_dep())

}

We finally run our app:

# run shiny app
ui <- fluidPage(
  title = "Hello Shiny!",
  fluidRow(
    column(
      width = 6,
      align = "center",
      br(),
      my_card("Card Content")
    )
  )
)

shinyApp(ui, server = function(input, output) { })

With this approach, you can develop a package of custom dependencies that people could use when they need to add custom elements in shiny.

4.3 Importing HTML dependencies from other packages

The shinydashboard (Chang and Borges Ribeiro 2018) package helps to design dashboards with shiny. In the following, we would like to integrate the box component in a classic Shiny App (without the dashboard layout). However, if you try to include the box tag, you will notice that nothing is displayed since shiny does not have shinydashboard dependencies. htmltools contains a function, namely findDependencies() that looks for all dependencies attached to a tag. Before going further, let’s define the basic dashboard skeleton:

library(shinydashboard)
shinyApp(
  ui = dashboardPage(
    dashboardHeader(),
    dashboardSidebar(),
    dashboardBody(),
    title = "Dashboard example"
  ),
  server = function(input, output) { }
)

There are numerous details associated with shinydashboard that we will not go into. If you are interested in learning more, please check out the package website. The key point here is the main wrapper function dashboardPage(). The fluidPage() is another wrapper function that most are already familiar with. We apply findDependencies() on dashboardPage():

library(htmltools)
deps <- findDependencies(
  dashboardPage(
    header = dashboardHeader(),
    sidebar = dashboardSidebar(),
    body = dashboardBody()
  )
)
deps[[1]]
## List of 10
##  $ name      : chr "font-awesome"
##  $ version   : chr "5.13.0"
##  $ src       :List of 1
##   ..$ file: chr "www/shared/fontawesome"
##  $ meta      : NULL
##  $ script    : NULL
##  $ stylesheet: chr [1:2] "css/all.min.css" "css/v4-shims.min.css"
##  $ head      : NULL
##  $ attachment: NULL
##  $ package   : chr "shiny"
##  $ all_files : logi TRUE
##  - attr(*, "class")= chr "html_dependency"

For space reasons, we only printed the first dependency output but deps is a list containing four dependencies:

  • Font Awesome handles icons. Interestingly, this dependency is provided by dashboardHeader, especially the shiny::icon("bars") that collapses the left sidebar.
  • Bootstrap is the main HTML/CSS/JS template. Importantly, please note the version 3.3.7, whereas the current is 4.5.2.
  • AdminLTE is the dependency containing HTML/CSS/JS related to the admin template. It is closely linked to Bootstrap 3.
  • shinydashboard, the CSS and javascript necessary for our dashboard to work properly. In practice, integrating custom HTML templates to shiny does not usually work out of the box for many reasons and some modifications are necessary. For instance, here is a list of changes to optimize adminLTE for shiny. This has major consequences on the template maintenance such that upgrading to another AdminLTE version would require to modify all these elements by hand. You may understand why template maintainers are quite often reluctant to upgrade their dependencies as it might brake the whole package, quite easily.

Below, we attach the dependencies to the box() with tagList(), as shown above. Notice that our custom box() does not contain all parameters as in the official shinydashboard version, which is actually ok at this time. For a better contrast with the body, we add a custom color to the background, as depicted by Figure 4.3:

my_box <- function(title, status) {
  tagList(box(title = title, status = status), deps)
}
ui <- fluidPage(
  tags$style("body { background-color: gainsboro; }"),
  titlePanel("Shiny with a box"),
  my_box(title = "My box", status = "danger"),
)
server <- function(input, output) {}
shinyApp(ui, server)
AdminLTE2 box inside classic shiny app

FIGURE 4.3: AdminLTE2 box inside classic shiny app

You now have limitless possibilities! Interestingly, this same approach is the basis of shinyWidgets for the useBs4Dash() function and other related tools.

4.4 Suppress dependencies

In rare cases, you may need to remove an existing conflicting dependency. The suppressDependencies() function allows users to perform this task. For instance, shiny.semantic built on top of semantic ui is not compatible with Bootstrap, the latter being dropped from the list, as illustrated by Figure 4.4.

Deletion of Bootstrap inside semanticPage

FIGURE 4.4: Deletion of Bootstrap inside semanticPage

Below, we remove the AdminLTE2 dependency from a shinydashboard page and nothing is displayed (as expected):

shinyApp(
  ui = dashboardPage(
    dashboardHeader(),
    dashboardSidebar(),
    dashboardBody(suppressDependencies("AdminLTE")),
    title = "Dashboard example"
  ),
  server = function(input, output) { }
)

4.5 Resolve dependencies

Imagine a situation in which we would like to use the very last version of Font Awesome icons, that is currently 5.15.1 according to jsdelivr. We recall that shiny already provides version 5.13.0 through the icon() function. Including another version would probably cause conflicts and we would like to avoid that case. htmltools has a resolveDependencies() tool that removes any redundant dependencies, keeping the dependency with the higher version if names are identical:

jsdelivr_cdn <- "https://cdn.jsdelivr.net/npm/@fortawesome/"
ft_aws <- paste0(jsdelivr_cdn, "fontawesome-free@5.15.1/")
new_icon_dep <- htmlDependency(
  name = "font-awesome",
  version = "5.15.1",
  src = c(href = ft_aws),
  stylesheet = "css/all.min.css"
)

icon_deps <- list(
  new_icon_dep,
  findDependencies(shiny::icon("th"))[[1]]
)

resolveDependencies(icon_deps)
## [[1]]
## List of 10
##  $ name      : chr "font-awesome"
##  $ version   : chr "5.15.1"
##  $ src       :List of 1
##   ..$ href: chr "https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.15.1/"
##  $ meta      : NULL
##  $ script    : NULL
##  $ stylesheet: chr "css/all.min.css"
##  $ head      : NULL
##  $ attachment: NULL
##  $ package   : NULL
##  $ all_files : logi TRUE
##  - attr(*, "class")= chr "html_dependency"

Combining findDependencies(), suppressDependencies() and resolveDependencies() gives you great power to successfully manage your dependencies!

4.6 Insert Custom script in the head

With shinydashboardPlus, users can fine tune their dashboard behavior with a simple option parameter passed to dashboardPage(). The sidebarExpandOnHover capability, that consists in expanding the sidebar when hovering on it, is part of those options, yet not exposed by shinydashboard. Under the hood, all those options are gathered in a list, then converted into JSON to eventually generate a JavaScript configuration file. Until now, we only saw two ways to include scripts or stylesheets. How do we include any arbitrary script (defined on the fly by the user when the app starts) in a dependency?

htmlDependency() has a head parameter allowing to pass any lines of HTML to insert into the document head. We can easily imagine giving it a string containing a script. Below, we first construct the options list. Then, we create the dependency: notice since src is mandatory, we have to give it a value but we will not use script nor stylesheet arguments.

options <- list(
  sidebarExpandOnHover = TRUE,
  boxWidgetSelectors = list(
    remove = '[data-widget="remove"]'
  )
)

config_script <- function(options) {
  htmlDependency(
    "options",
    as.character(utils::packageVersion("shinydashboardPlus")),
    src = c(file = system.file(
      "shinydashboardPlus-2.0.0",
      package = "shinydashboardPlus")
    ),
    head = if (!is.null(options)) {
      paste0(
        "<script>var AdminLTEOptions = ",
        jsonlite::toJSON(
          options,
          auto_unbox = TRUE,
          pretty = TRUE
        ),
        ";</script>"
      )
    }
  )
}

# show the script
print(HTML(config_script(options)$head))
## <script>var AdminLTEOptions = {
##   "sidebarExpandOnHover": true,
##   "boxWidgetSelectors": {
##     "remove": "[data-widget=\"remove\"]"
##   }
## };</script>

We invite the reader to run the example below involving shinydashboardPlus, open the HTML inspector and look at the head.

 shinyApp(
   ui = dashboardPage(
     options = options,
     header = dashboardHeader(),
     sidebar = dashboardSidebar(),
     body = dashboardBody(),
     controlbar = dashboardControlbar(),
     title = "DashboardPage"
   ),
   server = function(input, output) { }
 )

According the the AdminLTE documentation, global options must be passed before loading the app.min.js script. Creating this “dummy” dependency allowed us to do so, as shown on Figure 4.5.

Insert arbitrary script in the head

FIGURE 4.5: Insert arbitrary script in the head