Chapter 9 Dynamic UI

So far, we’ve seen a clean separation between the user interface and the server function. The user interface is defined statically, when the app is launched. That means it can’t respond to anything that happens in the app. In this chapter, you’ll learn how to create dynamic user interfaces, using code run in the server function.

There are three key techniques for creating dynamic user interfaces:

  • Using the update functions to modify the parameters of input controls.

  • Using tabsetPanel() to conditionally show and hide different UI.

  • Using uiOutput() and renderUI() to generate entinre parts of the user interface from code.

These three tools give you considerable power to respond to the user’s actions by modifying the user interface. I’ll demonstrate some of the more useful ways in which you can apply them, but ultimately, you are only constrained by your creativity. That said, these tools can make your app substantially more difficult to reason about, so you should deploy them sparingly, and always use the simplest technique that solves your problem.

9.1 Updating inputs

We’ll begin with the simplest technique that allows you to tweak the parameters of an input after it has been created. Every input control, e.g. textInput(), is paired with a update function, e.g. updateTextInput(), that allows you to modify the control after it has been created. Each update function has the same arguments as the corresponding constructor, allowing you to change any of arguments after construction.

Take this very simple example. The app has three two inputs that control the range (the min and max) of the a slider. The key idea is to use observeEvent()21 to trigger updateNumericInput() whenever the min or max inputs change. updateNumericInput() looks a little different to other Shiny functions: it takes the current session as the first argument, and the name of the input (as a string) as the second. The remaining arguments match the arguments to the input constructor.

To help you get the hang of the update functions, I’ll next show a couple more simple examples. Then we’ll dive into a more complicated case study using hierarchical select boxes. I’ll finish off by discussing circular references, a major challenge with update functions, and the related problem of multiple sources of truth.

9.1.1 Simple uses

The simplest uses of the update functions are to provide small conveniences for the user. For example, maybe you want to make it easy to reset some parameters back to their starting place. The following snippet shows how you might combine an actionButton(), observeEvent() and updateNumericInput():

A similar application is to tweak the text of an action button so you know exactly what it’s going to do:

There are many ways to use update functions in this way; be on the look out for ways to give more information to the user when you are working on sophisticated applications. A particularly important application is making it easier to select from a long list of possible options, through step-by-step filtering. That’s often a problem for “hierarchical select boxes”.

9.1.2 Hierarchical select boxes

A more complicated, but particularly useful, application of the update functions is to allow interactive drill down across multiple categories. I’ll illustrate their usage with some imaginary data for a sales dashboard that comes from

For our purposes, I’m going to focus on a natural hierarchy in the data:

  • Each territory contains customers.
  • Each customer has multiple orders.
  • Each order contains rows.

I want to create a user interface where you can:

  • Select a territory to see all customers.
  • Select a customer to see all orders.
  • Select an order to see the underlying rows.

The essence of the UI is simple: I’ll create three select boxes and one output table. The choices for the customername and ordernumber select boxes will be dynamically generated, so I set choices = NULL.

In the server function, I work top-down:

  1. I create a reactive, territory(), that contains the rows from sales that match the selected territory.

  2. Whenever territory() changes, I update the list of choices in the input$customername select box.

  3. I create another reactive, customer(), that contains the rows from territory() that match the selected customer.

  4. Whenever customer() changes, I update the list of choices in the the input$ordernumber select box.

  5. I display the selected orders in output$data.

You can see a more fleshed out application of this principle in

9.1.3 Circular references

There’s an important issue we need to discuss if you want to use update functions to change the value22 of inputs. From Shiny’s perspectve, using an update function to modify value is no different to the user modifying the value by clicking or typing. That means an update function can trigger reactive updates in exactly the same way that a human can. This means that you are now stepping outside of the bounds of pure reactive programming, and you need to start worrying about circular references and infinite loops.

For example, take the following simple app. It contains a single input control and a observer that increments its value by one. Every time updateNumericInput() runs, it changes input$n, causing updateNumericInput() to run again, so the app gets stuck in an infinite loop constantly increasing the value of input$n.

You’re unlikely to create such an obvious problem in your own app, but it can crop up if you updating multiple controls that depend on one another, as in the next example.

9.1.5 Exercises

  1. Complete the user interface below with a server function that updates input$date so that you can only select dates in input$year.

  2. Complete the user interface below with a server function that updates input$county choices based on input$state. For an added challenge, also change the label from “County” to “Parrish” for Louisana and “Borrough” for “Alaska”.

  3. Complete the user interface below with a server function that updates input$country choices based on the input$continent. Use output$data to display all matching rows.

  4. Extend the previous app so that you can also choose to select no continent, and hence see all countries. You’ll need to add "" to the list of choices, and then handle that specially when filtering.

9.2 Showing and hiding

The next step up in complexity is to selectively show and hide parts of the UI. You’ll learn a number of sophisticated approaches later, once you’ve learned a little JS and CSS, but there’s a useful technique that you can use now: concealing optional UI in a tabset. This is a clever hack that allows you to show and hide UI as needed, without having to re-generate it from scratch (as you’ll learn in the next section).

Currently23 you need a dash of CSS to make it work. In the following UI specification, tags$style("#switcher { display:none; }") hides the tab switcher. If you adapt this for your own code make sure to repace switcher with id of your the tabsetPanel().

There are two main ideas here:

  • Use tabset panel with hidden tabs.
  • Use updateTabsetPanel() to switch tabs from the server.

This is a simple idea, but when combined with a little creativity, it gives you a considerable amount of power. The following two sections illustrate give a couple of small examples of how you might use it in practice.

9.2.1 Conditional UI

Imagine that you want to create a shiny app that allows the user to simulate from the normal, uniform, and exponential distributions. Each distribution has different parameters, so we’ll need some way to show different controls for different distributions. The tabset approach provides an elegant way to do so.

First, I’ll create the UI for the tabset, and then show you how it embeds into a bigger app. The basic idea is pretty simple: we have a tabset where each panel provides the user interface for one distribution.

I’ll then embed that inside a bigger UI which allows the user to pick the number of sample, and shows a histogram of the results.

Note that I’ve carefully matched the input$dist choices to the names of the tab panels. That makes it easy to write the observeEvent() code that automatically switches controls when the distribution changes.

Note that I also pull out the sampling code into it’s own reactive. That’s not strictly necessary here, but I think it makes the app a little easier to understand.

9.2.3 Exercises

9.3 Dynamic UI

Sometimes modifying the values of an existing control is not enough, and you need control the number of type of controls. There’s a special pair of output and render functions that allow you to do UI on the server: uiOutput() and renderUI()

Here’s a simple example to illustrate the basic idea. It dynamically creates a different type of input control depending on an input:

If you run this code yourself, you’ll notice that it takes a fraction of a second to appear after the app loads - that’s because it has to be rendered by the server function. Relying too much on renderUI() can create a laggy UI. Want to keep as much as of the fixed structure in UI as possible. Better peformance. Simpler reasoning.

Note that you are now creating IDs in two places so that when you add to ui, you have to be careful not to call the control dynamic. Later, in Chapter 10 we’ll see how modules can help avoid this problem.

There’s one other problem with this approach: when you change controls, you lose the value that the user selected. It’s possible to fix by setting the value to the current value. We haven’t talked about isolate() yet; allows you to get a reactive value without creating a reactive dependency.

Maintaining existing user input is one of the big challenges of working with renderUI(). This is one reason that selecting showing and hiding UI (as above) is a better appraoch it works for you - because you’re not destroying and recreating the controls, you don’t need to do anything to prserve the values.

Dynamic UI is most useful when you are generating an arbitrary number or type of controls. That typically means that you’ll be generating UI with code, and function programming is a good fit for this sort of task. If you’re not familiar with the basic map() and reduce() of functional programming, you might want to take a brief detour to read before continuing.

9.3.1 Multiple controls

A more realistic use of dynamicUI is when you need to create an arbitrary number of controls. For example, imagine you’d like the user to be able to supply their own palette of colours.

The UI side is pretty simple: we have a numeric input that controls that will control the number of text inputs; and then a UI output where the text boxes will go.

Then in the server function I first a make a reactive that will provide the names for each control,

  • I create the dynamic UI by using map() to create a textInput() once for each name.

  • To generate the output, I need to use use a new trick. So far we’ve always accessed the components of input with $. But we can also use [[, which is what we need here because we have the name of the component stored in a character vector. I use map_chr() to collect them all into a character vector.

If ran this app, you’d discover a really annyoing behaviour: whenever you change the number of colours, all the data you’ve entered disappears. We can fix this problem by using the same technique as before: setting value to the current value (using isolate() so we don’t create an reactive dependency that we don’t want).

I’ll also tweak the apperance to look a little nicer, including displaying the selected colours in a plot.

9.3.2 Dynamic filtering

To finish off the chapter, I’m going to create an app that generates a filtering interface for any data frame.

To begin I’ll create a function that generates either a slider for numeric inputs, or a multi-select for factor inputs:

And then I’ll write a function that takes the value of that control and returns a logical vector saying whether or not to include it. I chose to return a logical vector here because it’ll make it easy to combine the results from multiple columns.

I’ll illustrate how these bits fit together on the iris dataset:

I got sick of copying and pasting so the above app only works with three columns. I can generalise to all the columns by using some functional programming:

From there, it’s a simple generalisation to allow the user to provide the dataset.

  1. Note that I’ve used observeEvent() here, although observe() would also have worked and would yield shorter code. I generally prefer observeEvent() because its argument cleanly separate the event you’re listening for from the action you want to take in response.

  2. This is generally only a concern when you are changing the value, but be some other parameters can change the value indirectly. For example, if you modify the choices for selectInput(), or min and max for sliderInput(), the current value will be modified if it’s no longer in the allowed set of values.

  3. Hopefully, it will be built into tabsetPanel() in the future; follow for details.