#' @export
ui <- function(headers) {
box::use(shiny)
tags$div(
shiny$actionButton("show", "Click"),
shiny$uiOutput("message")
)
}
#' @export
server <- function(input, output, session) {
box::use(shiny = shiny[tags])
shiny$observeEvent(input$show, {
shiny$showNotification("You pushed that.")
output$message <- shiny$renderUI({
tags$h1("You have clicked me!")
})
})
}
shiny::shinyApp(ui, server)10 Shiny
10.1 Thinking in Shiny
I love Shiny, but it isn’t easy to understand deeply. Shiny is a framework for constructing web applications in both R and Python. Its core purpose is to facilitate the connection between a user and executable R (and now Python) code through a web browser.
To use Shiny effectively, we must understand its fundamental components without oversimplifying them. When launching a Shiny application, you must define two functions: one for the user interface (UI) and another for server logic - this pattern continues when we use modules, which I will discuss later. The UI function specifies the HTML elements to render immediately when the application starts. Unlike traditional websites that rely on client-side JavaScript, Shiny applications can utilize anything available to the browser and R code. Since R is primarily a server-side language, it requires a separate server to run the code because it cannot execute directly in the user’s browser. The beauty of Shiny is how it has Meteor-inspired reactivity for the R language. It’s beautiful.
The provided code snippet demonstrates the essential elements of Shiny for communicating with a web browser.
Before we click the button, the application looks like this -
<body>
<div>
<button id="show" type="button" class="btn btn-default action-button shiny-bound-input">Click</button>
<div id="message" class="shiny-html-output shiny-bound-output" aria-live="polite"></div>
</div>
</body>And after -
<div>
<button id="show" type="button" class="btn btn-default action-button shiny-bound-input">Click</button>
<div id="message" class="shiny-html-output shiny-bound-output" aria-live="polite">
<h1>You have clicked me!</h1>
</div>
</div>In this code, we define a Shiny application with a button in the user interface that establishes a communication channel with the remote R server. When a user interacts with this button, the server responds accordingly within the observeEvent block and sends the internal content back to the browser if required.
The first thing we focus on is the HTML id and class. From now on, an id will be exclusively for action and class exclusively for CSS styling - for you.Classes are used to manage render connections on the back end of Shiny. Yes, you can create CSS for id but an id should be and for all intent and purpose and your rule is an id is always unique. If you would like to see how classes can be used for communication to the backend server, take a look a actionButton’s action-button class. Take the raw HTML of actionButton and see what happens when you remove action-button from it.
Applications as Modules
Let’s take the prior example and convert it into a module. A module is a way of encapsulating logic in Shiny so that we can break our application down into sub applications. There is no rule stating your entire application cannot be a module. These sub-applications are exclusively partitioned by their associated ids. For UI components, we have to manage these partitions. Assuming we have set up our modules correctly, the session object on the server side manages knowledge of where the module is given it’s nesting.
#' @export
ui_button <- function(id = "button") {
box::use(shiny = shiny[tags])
ns <- shiny$NS(id)
tags$div(
shiny$actionButton(ns("show"), "Click"),
shiny$uiOutput(ns("message"))
)
}
#' @export
server_button <- function(id = "button") {
box::use(shiny = shiny[tags])
shiny$moduleServer(
id,
function(input, output, session) {
ns <- session$ns
shiny$observeEvent(input$show, {
shiny$showNotification("You pushed that.")
output$message <- shiny$renderUI({
tags$h1("You have clicked me!")
})
})
}
)
}
#' @export
ui <- function(headers) {
box::use(shiny)
ui_button("ui")
}
#' @export
server <- function(input, output, session) {
server_button("ui")
}
shiny::shinyApp(ui, server)With a module before the click
<body>
<div>
<button id="ui-show"
type="button"
class="btn btn-default action-button shiny-bound-input">Click</button>
<div id="ui-message"
class="shiny-html-output shiny-bound-output"
aria-live="polite"></div>
</div>
</body>With a module after the click
<body>
<div>
<button id="ui-show"
type="button"
class="btn btn-default action-button shiny-bound-input">Click</button>
<div id="ui-message"
class="shiny-html-output shiny-bound-output"
aria-live="polite">
<h1>You have clicked me!</h1>
</div>
</div>
</body>So what is different? And here’s where we really focus - we never wrote ui-show explicitly. Even within the module, output$show was not written like output$ui-show. The UI components need ns around them, but the server just knows where it is.
Default ndexr project
In the example above, we 1) Created a button with id='show' which is sent to the server as input$show to use in your backend logic. This is an example of an id being used to send information to the server. 2) Used input$show to tell the server to create some HTML to send back to the browser and place in the uiOutput with the same id.
It’s weird though right - to have such a specific connection between the server and ui with somewhat different syntax? In the UI we have 'message' but in the server we have output$message. These will need to remain synced up in order to the pattern to work.
#' @export
ui_table <- function(id = "table") {
box::use(shiny = shiny[tags], purrr)
ns <- shiny$NS(id)
tags$table(
class = "table",
tags$thead(
tags$tr(
purrr$map(
colnames(datasets::mtcars),
function(x) tags$th(scope = "col", x)
)
)
),
shiny$uiOutput(ns("ui"), container = function(...) {
tags$tbody(...)
})
)
}
#' @export
server_table <- function(id = "table") {
box::use(shiny = shiny[tags], . / table, uuid, purrr)
shiny$moduleServer(
id,
function(input, output, session) {
ns <- session$ns
output$ui <- shiny$renderUI({
purrr$map(
split(datasets::mtcars, 1:nrow(datasets::mtcars)),
function(x) {
id <- uuid$UUIDgenerate()
table$server_row(id, x)
table$ui_row(ns(id))
}
)
})
}
)
}
#' @export
ui_row <- function(id = "row") {
box::use(shiny = shiny[tags])
ns <- shiny$NS(id)
shiny$uiOutput(ns("ui"), container = function(...) {
tags$tr(...)
})
}
#' @export
server_row <- function(id = "row", data) {
box::use(shiny = shiny[tags], purrr)
shiny$moduleServer(
id,
function(input, output, session) {
ns <- session$ns
output$ui <- shiny$renderUI({
shiny$tagList(
tags$th(
scope = "row",
rownames(data)
),
purrr$map(data, function(item) tags$td(item))
)
})
}
)
}10.2 Default Project
The default shiny project for ndexr servers is located in Gitlab here. You can also download the source code as a zip file here.
The root directory ./ contains the docker-compose.yaml file. Docker compose is a Docker plugin that makes deploying and networking multiple Docker containers together easier. A Dockerfile is associated with a specific service, and a service may run on one or more ports behind a load balancer, like an NGINX upstream block.
The Makefile is used to quickly start and stop the system, weaving together the Docker commands needed. This is a file for you to add additional commands you may need. This file will be automatically read into ndexr in time so you can execute the commands without accessing your server directly.
fdrennan@pop-os:~/public$ tree -L 3 .
.
├── docker-compose.yaml
├── Makefile
├── public.Rproj
├── README.html
├── README.qmd
└── services
└── console
├── app.r
├── console.Rproj
├── Dockerfile
├── environment.yml
├── node_modules
├── onload
├── package.json
├── package-lock.json
├── renv
├── renv.lock
├── requirements.txt
├── scripts
├── src
└── yarn.lock
{snippet}
snippet smod
#' @export
ui_${1} <- function(id='${1}') {
box::use(shiny=shiny[tags])
ns <- shiny\$NS(id)
shiny\$uiOutput(ns('ui'))
}
#' @export
server_${1} <- function(id='${1}') {
box::use(shiny=shiny[tags])
shiny\$moduleServer(
id,
function(input, output, session) {
ns <- session\$ns
output\$ui <- shiny\$renderUI({
shiny\$div('${1}')
})
})
}