Serverless shiny app embedded in a Quarto Website
Serverless Shiny
Inspired by Max Kuhn's presentation at R/Pharma today I tried out embedding shiny a app within a Quarto website and hosting it on GitHub pages. The app itself is a chemistry quiz built to help my son who had a school test coming up. By running it within a Quarto website it's available for him to use on the bus on the way to school for some quick revision.
Thanks so the app he scored 27/30!
Setup
To use shinylive in a Quarto document, you need to first install the shinylive Quarto extension:
1quarto add quarto-ext/shinylive
along with the shinylive R package
1remotes::install_github("posit-dev/r-shinylive")
_quarto.yml file
The _quarto.yml file is a project with some basic parameters to define the website. The only additional parameter of note is to include shinylive
as a filter.
1project:
2 type: website
3 output-dir: docs
4
5format:
6 html
7
8website:
9 title: "shinylive elements quiz"
10 navbar:
11 left:
12 - index.qmd
13
14filters:
15 - shinylive
index.qmd
The shiny app is in a single chunk of the index.qmd file with a code chunk type of shinylive-r
and the standalone
option set.
1---
2title: "Chem symbol test"
3---
4
5This is a simple chemical symbol test. You'll be tested on chemical elements and their names.
6
7```{r}
8#| include: false
9library(shinylive)
10```
11
12```{shinylive-r}
13#| viewerHeight: 300
14#| standalone: true
15
16chem <- data.frame(
17 symbol = c("Al", "Sb", "Ar", "As", "Ba", "Be", "Bi",
18 "B", "Br", "Cd", "Ca", "C", "Cs", "Cl", "Cr",
19 "Co", "Cu", "F", "Au", "He", "H", "I", "Fe",
20 "Kr", "Pb", "Li", "Mg", "Mn", "Hg", "Ne", "Ni",
21 "N", "O", "P", "Pt", "K", "Rn", "Se", "Si", "Ag",
22 "Na", "Sr", "S", "Te", "Sn", "Ti", "W",
23 "U", "V", "Xe", "Zn", "Zr"),
24 name = c("aluminum", "antimony", "argon", "arsenic", "barium", "beryllium", "bismuth",
25 "boron", "bromine","cadmium", "calcium", "carbon", "cesium", "chlorine", "chromium",
26 "cobalt", "copper", "fluorine", "gold", "helium", "hydrogen", "iodine", "iron",
27 "krypton", "lead", "lithium", "magnesium", "mangenese", "mercury", "neon", "nickel",
28 "nitrogen", "oxygen", "phosphorus", "platinum", "potassium", "radon", "selenium", "silicon", "silver",
29 "sodium", "strontium", "sulfur", "tellurium", "tin", "titanium", "tungsten",
30 "uranium", "vanadium", "xenon", "zinc", "zirconium")
31)
32
33ui <- fluidPage(
34 fluidRow(
35 column(5, offset = 1, checkboxGroupInput("chk_options", "Test me on", choices = names(chem), selected = "symbol", inline = TRUE))
36 ),
37 fluidRow(
38 column(5, offset = 1, uiOutput("ui_question")),
39 column(5,
40 fluidRow(uiOutput("ui_score")),
41 fluidRow(uiOutput("ui_streak"))
42 )
43 )
44
45)
46
47server <- function(input, output, session) {
48
49 last_num <- 0
50 rv <- reactiveValues(
51 question_count = 0,
52 score = 0,
53 question_type = NULL,
54 question = NULL,
55 answer = NULL,
56 streak = 0
57 )
58
59 question_index <- reactive({
60 rv$question_count
61 num <- last_num
62 while (num == last_num) {
63 num <- sample(nrow(chem), size = 1)
64 }
65 last_num <- num
66 })
67
68 output$ui_question <- renderUI({
69 if (length(input$chk_options) > 0) {
70 rv$question_type <- sample(input$chk_options, size = 1)
71 answer_type <- names(chem)[!names(chem) == rv$question_type]
72 rv$question <- chem[[rv$question_type]][question_index()]
73 rv$answer <- chem[[answer_type]][question_index()]
74 tagList(
75 h3(paste(answer_type, "for:", rv$question)),
76 textInput("txt_answer", label = NULL),
77 actionButton("but_answer", "Submit")
78 )
79 }
80 })
81
82 observeEvent(input$but_answer, {
83 if (rv$question_type == "symbol") {
84 answer <- tolower(input$txt_answer)
85 } else {
86 answer <- input$txt_answer
87 }
88 if (answer == rv$answer) {
89 rv$score <- rv$score + 1
90 rv$streak <- rv$streak + 1
91 showNotification(ui = "CORRECT!", type = "message")
92 } else {
93 rv$streak <- 0
94 showNotification(ui = paste0("WRONG. Correct answer is ", rv$answer), type = "error")
95 }
96 rv$question_count <- rv$question_count + 1
97 })
98
99 output$ui_score <- renderUI({
100 h3(paste0("Score: ", rv$score, "/", rv$question_count), style = "color: #388E3C")
101 })
102
103 output$ui_streak <- renderUI({
104 if (rv$streak > 29) {
105 col <- "#2E7D32"
106 } else if (rv$streak > 19) {
107 col <- "#00838F"
108 } else if (rv$streak > 9) {
109 col <- "#1E88E5"
110 } else if (rv$streak > 0) {
111 col <- "#8E24AA"
112 } else {
113 col <- "#E53935"
114 }
115 h3(paste0("Streak: ", rv$streak), style = paste0("color: ", col))
116 })
117
118}
119
120app <- shinyApp(ui = ui, server = server)
121
122```
The app itself contains the typical ui and server along with the shinyapp()
function to execute. It is basic and unoptimized (but it demonstrates the purpose). It takes very little effort to change a standalone app to an embedded one.
The quarto website with embedded app is available at https://harveyl888.github.io/shinylive-test/