|
| 1 | +library(shiny) |
| 2 | +library(bslib) |
| 3 | + |
| 4 | +DO_ALERT <- FALSE |
| 5 | + |
| 6 | +action_choices <- c( |
| 7 | + "Singleton script" = "singleton", |
| 8 | + "Scripts with singleton" = "scripts", |
| 9 | + "HTML Widget" = "htmlwidgets", |
| 10 | + "Input/Output (content)" = "input_output_content", |
| 11 | + "Input/Output (nav)" = "input_output_nav", |
| 12 | + "Shiny sub-app" = "subapp", |
| 13 | + "Web Component" = "init_component" |
| 14 | +) |
| 15 | + |
| 16 | +ui <- page_navbar( |
| 17 | + title = "Reprex for #4179", |
| 18 | + id = "main", |
| 19 | + lang = "en", |
| 20 | + navbar_options = navbar_options(collapsible = FALSE), |
| 21 | + footer = absolutePanel( |
| 22 | + card( |
| 23 | + selectInput("insert_type", "Insert nav type", choices = action_choices), |
| 24 | + actionButton("do_insert", "Insert Nav"), |
| 25 | + HTML( |
| 26 | + '<p>Scripts: <span id="script-count">0</span> evaluated (<span id="script-count-expected">0</span> expected).' |
| 27 | + ), |
| 28 | + tags$script( |
| 29 | + HTML( |
| 30 | + "Shiny.addCustomMessageHandler('script-count-expected', function(value) { |
| 31 | + const exp = document.getElementById('script-count-expected') |
| 32 | + exp.textContent = +exp.textContent + value; |
| 33 | + })" |
| 34 | + ) |
| 35 | + ) |
| 36 | + ), |
| 37 | + bottom = "1rem", |
| 38 | + right = "1rem", |
| 39 | + draggable = TRUE |
| 40 | + ) |
| 41 | +) |
| 42 | + |
| 43 | +# https://github.com/rstudio/shiny/pull/1794#issuecomment-318722200 |
| 44 | +# We need these test cases for anywhere we insert dynamic UI: |
| 45 | + |
| 46 | +# 1. `<script>` blocks should run |
| 47 | +# 2. `<script>` blocks should only run once |
| 48 | +# 3. `head()`/`singleton()` should be respected |
| 49 | +# 4. HTML widgets should work |
| 50 | +# a. Even when the dependencies are not part of the initial page load |
| 51 | +# 5. Shiny inputs/outputs should work |
| 52 | +# 6. Subapps should work (include a `shinyApp` object right in the UI) |
| 53 | + |
| 54 | +action_link <- shiny::actionLink("refresh", "Refresh") |
| 55 | + |
| 56 | +script_hello_world <- local({ |
| 57 | + i <- 0 |
| 58 | + |
| 59 | + function() { |
| 60 | + i <<- i + 1 |
| 61 | + |
| 62 | + shiny::HTML( |
| 63 | + "<script>(function() { |
| 64 | + const el = document.getElementById('script-count') |
| 65 | + el.textContent = +el.textContent + 1 |
| 66 | + })()</script>" |
| 67 | + ) |
| 68 | + } |
| 69 | +}) |
| 70 | + |
| 71 | +script_singleton <- shiny::singleton(script_hello_world()) |
| 72 | + |
| 73 | +init_component <- function(init = NULL) { |
| 74 | + tag( |
| 75 | + "init-component", |
| 76 | + list( |
| 77 | + init = init, |
| 78 | + htmltools::htmlDependency( |
| 79 | + "init-component", |
| 80 | + "0.0.1", |
| 81 | + src = ".", |
| 82 | + script = "wc-init.js", |
| 83 | + all_files = FALSE |
| 84 | + ) |
| 85 | + ) |
| 86 | + ) |
| 87 | +} |
| 88 | + |
| 89 | +singleton_has_run <- FALSE |
| 90 | + |
| 91 | +nav_insert_singleton <- function(session) { |
| 92 | + if (!singleton_has_run) { |
| 93 | + session$sendCustomMessage('script-count-expected', 1L) |
| 94 | + singleton_has_run <<- TRUE |
| 95 | + } |
| 96 | + |
| 97 | + nav_insert( |
| 98 | + id = "main", |
| 99 | + select = TRUE, |
| 100 | + nav_panel( |
| 101 | + "One", |
| 102 | + p("Script should only run the first time this nav is inserted."), |
| 103 | + # 1. script blocks should run |
| 104 | + script_singleton, |
| 105 | + # 3. head() should be respected |
| 106 | + tags$head(tags$meta(content = "shiny-test-head")) |
| 107 | + ), |
| 108 | + ) |
| 109 | +} |
| 110 | + |
| 111 | +nav_insert_scripts <- function(session) { |
| 112 | + session$sendCustomMessage('script-count-expected', 2L) |
| 113 | + |
| 114 | + nav_insert( |
| 115 | + id = "main", |
| 116 | + select = TRUE, |
| 117 | + nav_panel( |
| 118 | + value = "Two", |
| 119 | + tagList( |
| 120 | + "Two", |
| 121 | + script_hello_world(), |
| 122 | + ), |
| 123 | + p( |
| 124 | + "Two scripts should run every time this nav is inserted." |
| 125 | + ), |
| 126 | + # 2. script blocks should only run once |
| 127 | + script_hello_world() |
| 128 | + ), |
| 129 | + ) |
| 130 | +} |
| 131 | + |
| 132 | +nav_insert_htmlwidget <- local({ |
| 133 | + widget_count <- 0 |
| 134 | + function() { |
| 135 | + widget_count <<- widget_count + 1 |
| 136 | + # 4. htmlwidgets work even if not part of initial page load |
| 137 | + nav_insert( |
| 138 | + id = "main", |
| 139 | + select = TRUE, |
| 140 | + nav_panel( |
| 141 | + "Map", |
| 142 | + leaflet::addTiles( |
| 143 | + leaflet::leaflet( |
| 144 | + elementId = sprintf("leaflet-%d", widget_count) |
| 145 | + ) |
| 146 | + ) |
| 147 | + ), |
| 148 | + ) |
| 149 | + } |
| 150 | +}) |
| 151 | + |
| 152 | +nav_insert_input_output_content <- function(input, output) { |
| 153 | + # 5. Input/outputs should work (in content) |
| 154 | + nav_insert( |
| 155 | + id = "main", |
| 156 | + select = TRUE, |
| 157 | + nav_panel( |
| 158 | + "Inputs/outputs", |
| 159 | + layout_columns( |
| 160 | + actionButton("btn", "Click me"), |
| 161 | + sliderInput("slider", "Slide me", min = 0, max = 10, value = 2), |
| 162 | + ), |
| 163 | + verbatimTextOutput("debug") |
| 164 | + ) |
| 165 | + ) |
| 166 | + |
| 167 | + output$debug <- renderPrint({ |
| 168 | + list( |
| 169 | + btn = input$btn, |
| 170 | + slider = input$slider, |
| 171 | + nav_link = input$nav_link |
| 172 | + ) |
| 173 | + }) |
| 174 | +} |
| 175 | + |
| 176 | +nav_insert_input_output_nav <- function(input, output) { |
| 177 | + # 5. Inputs/outputs work (in navbar) |
| 178 | + nav_insert( |
| 179 | + id = "main", |
| 180 | + nav_item( |
| 181 | + actionLink("nav_link", "Click me too", class = "nav-link") |
| 182 | + ) |
| 183 | + ) |
| 184 | + |
| 185 | + nav_insert( |
| 186 | + id = "main", |
| 187 | + nav_item(textOutput("nav_output")) |
| 188 | + ) |
| 189 | + |
| 190 | + output$nav_output <- renderText({ |
| 191 | + sprintf("Clicked %d times", input$nav_link) |
| 192 | + }) |
| 193 | +} |
| 194 | + |
| 195 | +nav_insert_subapp <- function() { |
| 196 | + # 6. Shiny subapps |
| 197 | + nav_insert( |
| 198 | + id = "main", |
| 199 | + select = TRUE, |
| 200 | + nav_panel( |
| 201 | + "Shiny app", |
| 202 | + p("There should be another shiny app in here."), |
| 203 | + shinyApp( |
| 204 | + ui = page_fluid( |
| 205 | + theme = bs_theme(preset = "darkly"), |
| 206 | + titlePanel("Hello from in here!"), |
| 207 | + p("This is a sub-app. Notice we're re-using the btn id."), |
| 208 | + actionButton("btn", "Click me"), |
| 209 | + verbatimTextOutput("debug") |
| 210 | + ), |
| 211 | + server = function(input, output, session) { |
| 212 | + output$debug <- renderPrint(list(btn = input$btn)) |
| 213 | + } |
| 214 | + ) |
| 215 | + ) |
| 216 | + ) |
| 217 | +} |
| 218 | + |
| 219 | +nav_insert_init_component <- function() { |
| 220 | + # `init_component()` renders differently if it goes through the cycle html -> |
| 221 | + # rendered -> html -> rendered, because the HTML of the element *after* being |
| 222 | + # attached to the DOM is different than it's initial HTML. In short, this |
| 223 | + # tests that web components are handled in a way that the connected callback |
| 224 | + # is only ever called once. |
| 225 | + |
| 226 | + nav_insert( |
| 227 | + id = "main", |
| 228 | + select = TRUE, |
| 229 | + nav_panel( |
| 230 | + value = "Web Component", |
| 231 | + tagList( |
| 232 | + "Web", |
| 233 | + init_component("Component") |
| 234 | + ), |
| 235 | + p(init_component()), |
| 236 | + p(init_component("custom init text")) |
| 237 | + ) |
| 238 | + ) |
| 239 | +} |
| 240 | + |
| 241 | +server <- function(input, output, session) { |
| 242 | + choices <- reactiveVal(action_choices) |
| 243 | + |
| 244 | + observe({ |
| 245 | + updateSelectInput( |
| 246 | + session, |
| 247 | + "insert_type", |
| 248 | + choices = choices(), |
| 249 | + selected = input$insert_type |
| 250 | + ) |
| 251 | + }) |
| 252 | + |
| 253 | + observeEvent(input$do_insert, { |
| 254 | + one_time_choice <- FALSE |
| 255 | + |
| 256 | + switch( |
| 257 | + input$insert_type, |
| 258 | + "singleton" = nav_insert_singleton(session), |
| 259 | + "scripts" = nav_insert_scripts(session), |
| 260 | + "htmlwidgets" = nav_insert_htmlwidget(), |
| 261 | + "input_output_content" = { |
| 262 | + one_time_choice <- TRUE |
| 263 | + nav_insert_input_output_content(input, output) |
| 264 | + }, |
| 265 | + "input_output_nav" = { |
| 266 | + one_time_choice <- TRUE |
| 267 | + nav_insert_input_output_nav(input, output) |
| 268 | + }, |
| 269 | + "subapp" = nav_insert_subapp(), |
| 270 | + "init_component" = nav_insert_init_component() |
| 271 | + ) |
| 272 | + |
| 273 | + if (one_time_choice) { |
| 274 | + choices(choices()[choices() != input$insert_type]) |
| 275 | + } |
| 276 | + }) |
| 277 | +} |
| 278 | + |
| 279 | +shinyApp(ui, server) |
0 commit comments