--- title: "Shiny integration" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Shiny integration} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} source("shots.R") ``` How the components meet Shiny: what they report, how the server reaches them, how they behave in modules, with bookmarks and alongside the rest of the Shiny ecosystem, and how to build components of your own. Each component's own page -- under Components -- shows what Element does with it. ## Reading and writing a component A component reports to `input$`, on load as well as on change: ```{r reading, eval = FALSE, shot = TRUE} ui <- el_page( el_select("city", choices = c("Beijing", "Shanghai"), value = "Shanghai"), verbatimTextOutput("picked") ) server <- function(input, output, session) { output$picked <- renderPrint(input$city) } shinyApp(ui, server) ``` Three separate channels reach back into it, and they do different things: | | Reaches | Example | |---|---|---| | `update_el_*()` | the component's props | `update_el_select(session, "city", selected = "Beijing")` | | `call_el()` | the component's methods | `call_el(session, "tbl", "clearSelection")` | | `update_vue()` | the Vue instance's fields directly | `update_vue(session, "tip", tipContent = "...")` | Like Shiny's `update*Input()`, each takes the current session by default, so `update_el_select(id = "city", selected = "Beijing")` is the same call. An update reaches a component whether or not it has a value to report -- an alert, a timeline, a component only ever drawn by `renderUI()` -- and one sent to an id that is not on the page, or to a field the component does not have, logs a `[shiny-vue]` warning in the browser console rather than vanishing. `update_el_*()` cannot call a method, because it assigns into the Vue instance's data and a method is a function. That is what `call_el()` is for. A method with a return value answers asynchronously, as `input$_`: ```{r methods, eval = FALSE, shot = TRUE, shot_js = "document.querySelector('#ask_container button').click()", shot_wait = 2} ui <- el_page( el_tree( "tree", show_checkbox = TRUE, node_key = "id", default_expand_all = TRUE, default_checked_keys = c("b1", "c"), data = list( list( id = "a", label = "Fruit", children = list( list(id = "b1", label = "Apple"), list(id = "b2", label = "Pear") ) ), list(id = "c", label = "Bread") ) ), el_button("ask", "Which are checked?"), verbatimTextOutput("answer") ) server <- function(input, output, session) { observeEvent(input$ask, call_el(session, "tree", "getCheckedKeys")) output$answer <- renderPrint(input$tree_get_checked_keys) } shinyApp(ui, server) ``` As with Shiny's own `update*Input()`, an update or a method call for a component that is not on the page yet -- one a `renderUI()` has still to draw -- is dropped, with a `[shiny-vue]` warning in the browser's console. Send it from an observer that runs once the component is there, as for any Shiny input. ### When an update arrives Shiny's own `update*Input()` do not send at once: the message waits for the flush, and goes with the outputs that the same observers recalculated. The updates and method calls here do the same -- every `update_el_*()`, `call_el()`, `update_vue()`, `call_vue()` -- so a component an observer re-renders and updates is updated once drawn again. A progress bar is the exception, as `withProgress()` is: `update_el_progress()` is sent at once. An update made before a long computation in the same observer therefore shows after it. `flush_vue()` sends what is queued at once: ```{r flush-vue, eval = FALSE} observeEvent(input$run, { flush_vue({ update_el_table(session, "tbl", loading = TRUE) }) result <- slow_query() # the table shows its mask meanwhile update_el_table(session, "tbl", data = result, loading = FALSE) }) ``` Often there is a better way. A computation in an `ExtendedTask` leaves the observer at once, and its updates go with that flush. A button that shows it is working needs no server at all: `el_button(task = TRUE)` turns to Element's loading state in the browser when clicked, and back once the server has handled the click -- or, bound with `bslib::bind_task_button()`, once the task is done -- as bslib's `input_task_button()`: ```{r task-button, eval = FALSE, shot = TRUE, shot_js = "document.querySelector('#save .el-button').click()", shot_wait = 0.5} ui <- el_page(el_button("save", "Save", type = "primary", task = TRUE)) server <- function(input, output, session) { save <- ExtendedTask$new(function() { promises::promise(function(resolve, reject) { later::later(function() resolve(TRUE), 3) }) }) bslib::bind_task_button(save, "save") observeEvent(input$save, save$invoke()) } shinyApp(ui, server) ``` ## Which events reach the server A component reports its value, the actions a user takes on purpose -- confirming, closing, submitting, going back -- and the requests the server answers, such as a remote search's text or a lazy node asking for its children. Element's other events -- focus and blur, hovering, a dropdown opening, a drag in progress -- stay in the browser until asked for: each would be a message to the server, and most apps never read them. Ask for one with `events`, by Element's name in snake_case; it arrives as `input$_`. `el_events()` lists what a component reports, and which it reports unasked; its help page lists the same under "Shiny inputs", and a name it does not have is an error that lists them: ```{r el-events-list, eval = FALSE} el_events("el_tree") #> el_tree() reports, as input$...: #> #> Unasked #> input$ the key of the node last clicked #> input$_checked the keys of the checked nodes #> input$_load with `lazy = TRUE`, a node asking for its children; ... #> #> When asked for, with `events =` #> input$_check_change `list(data, checked, indeterminate)` #> input$_node_drop `list(dragging, drop, type)`: ... #> ... el_tree("tree", data = tree_data, draggable = TRUE, events = "node_drop") # -> input$tree_node_drop ``` For an event Element does not list -- a key with a modifier, a DOM event on the element it draws -- or to send something else than an event carries, give the component a handler of your own with `on`: a `JS()` function called with `report` and the event's arguments. `report(name, value)` sets `input$_`, namespaced in a module as the id is, the same rule as every other input: ```{r on-handler, eval = FALSE, shot = TRUE, shot_js = "var i = document.querySelector('#q_container input'); i.value = 'leek'; i.dispatchEvent(new Event('input')); i.dispatchEvent(new KeyboardEvent('keyup', {key: 'Enter', bubbles: true}));", shot_wait = 2, shot_expect = "document.getElementById('searched').innerText.indexOf('leek') >= 0"} ui <- el_page( el_input( "q", placeholder = "Type, then Enter", on = list( "keyup.enter" = JS("function(report, e) { report('enter', e.target.value); }") ) ), verbatimTextOutput("searched") ) server <- function(input, output, session) { # input$q follows each keystroke; input$q_enter, only Enter output$searched <- renderText(paste("searched for:", input$q_enter)) } shinyApp(ui, server) ``` ## Events are event inputs Every forwarded Element event sets its input with event priority. The input keeps its last value like any other; what event priority adds is that sending the same value again still counts as a change. Clicking the same row twice fires an `observeEvent()` twice -- while an output that only reads the value sees nothing new the second time. Here is the same row clicked twice. The output reading the value shows where the last click landed; only the observer knows there were two: ```{r event-inputs, eval = FALSE, shot = TRUE, shot_js = "var td = document.querySelectorAll('#shot .el-table__body tr')[1].querySelector('td'); td.click(); setTimeout(function(){ td.click(); }, 600);", shot_wait = 2.5} ui <- el_page( el_table_output("tbl"), verbatimTextOutput("polled"), verbatimTextOutput("latched") ) server <- function(input, output, session) { output$tbl <- render_el_table( el_table(data = head(iris[, c(1, 5)], 3), events = "row_click") ) # Reading the value: the last row clicked, but not how many times output$polled <- renderText({ paste("last row clicked:", input$tbl_row_click$row_index) }) # Observing the event: runs once per click, repeats included clicks <- reactiveVal(0) observeEvent(input$tbl_row_click, clicks(clicks() + 1)) output$latched <- renderText(paste("clicks seen:", clicks())) } shinyApp(ui, server) ``` An event whose arguments cannot cross the wire -- a native `FocusEvent`, a DOM node, a whole Vue instance -- reports `TRUE` instead, so an observer can still tell that it happened. ## Shiny modules Components follow Shiny's own rule. In a module's UI, the id is wrapped in `ns()`, exactly as for `textInput()`; in its server, `input$` and every `update_el_*()` take the bare id, namespaced by the module's session. That holds for UI built by `renderUI()` inside the module too, and for the inputs a component adds to its id -- `input$rows_go` from a row action, `input$tabs_tab_remove`, `input$rows_selection_change`. An output is the same: `el_table_output(ns("rows"))` in the UI, `output$rows` in the server. ```{r module, eval = FALSE, shot = TRUE, shot_js = "document.querySelector('#orders-rows .el-table__body .el-button').click()", shot_wait = 2} orders_ui <- function(id) { ns <- NS(id) tagList( el_select(ns("status"), choices = c("paid", "pending"), selected = "paid"), el_table_output(ns("rows")), uiOutput(ns("more")), verbatimTextOutput(ns("seen")) ) } orders_server <- function(id) { moduleServer(id, function(input, output, session) { output$rows <- render_el_table(el_table( data = data.frame(order = c(101, 102)), columns = list( list(prop = "order", label = "Order"), list( label = "", cell = el$button( size = "small", "@click" = "rowAction('open', scope)", "Open" ) ) ) )) # Built in the server, still wrapped in ns() once output$more <- renderUI(el_switch( session$ns("urgent"), active_text = "Urgent" )) output$seen <- renderPrint(list( status = input$status, urgent = input$urgent, opened = input$rows_open$row_index )) }) } ui <- el_page(orders_ui("orders")) server <- function(input, output, session) orders_server("orders") shinyApp(ui, server) ``` Before 0.1.0 the UI functions namespaced their id themselves, from the session they were built in -- which inside `renderUI()` in a module turned `ns("urgent")` into `"orders-orders-urgent"`, an input that never reported. They no longer do; their `session` argument is deprecated, and a session passed to it still namespaces, with a warning. ## Shiny's own tools Each component is a Shiny input binding on a host element that carries its id, so the tools that work on Shiny's inputs work on these. **Bookmarking.** Under `enableBookmarking()`, every component's value comes back from a bookmark -- including where a container is: the open tab, the open panels of a collapse, an open dialog or drawer, the menu's current item, the pager's page. Nothing needs to be set up beyond what Shiny itself asks for. ```{r bookmark, eval = FALSE} ui <- function(request) { el_page( el_select("city", choices = c("Beijing", "Shanghai")), el_tabs( "views", tabs = list( list(name = "table", label = "Table", content = tags$p("...")), list(name = "chart", label = "Chart", content = tags$p("...")) ) ), bookmarkButton() ) } server <- function(input, output, session) {} shinyApp(ui, server, enableBookmarking = "url") ``` **shinyjs.** `shinyjs::hide()` and `show()` take the component and its label with it; `disable()` and `enable()` set the component's own `disabled`, so it is drawn disabled as Element draws it rather than having a native attribute set somewhere underneath. `reset()` puts the components under the element it is given back as the page first had them, beside Shiny's own inputs; to set any other value, use the component's `update_el_*()`. `hidden()` and `disabled()` wrap a component in the UI, `click()` clicks the button or link inside it, and `onclick()` and `onevent()` hear events from inside it. `addClass()` puts a class on the component's host, which draws no box of its own (`display: contents`): style the component with its own `class` argument instead. **bslib.** Components work in bslib's containers -- a sidebar, a card, an accordion, a nav panel not yet shown -- and bslib's `tooltip()` and `popover()` take one as their trigger. bslib's `input_dark_mode()` turns Element Plus's dark mode with Bootstrap's. Element's brand colours follow Bootstrap's CSS variables, so a theme changed while the app runs -- `session$setCurrentTheme()`, `bslib::bs_themer()` -- recolours Element's components too. **Busy indicators.** Under `useBusyIndicators()` a table output keeps Element's own loading mask while it recalculates, in place of Shiny's spinner; `el_table_output(loading = FALSE)` gives it Shiny's. **Asynchronous work.** Every render function waits for a promise, so the result of an `ExtendedTask` or a `promises` pipeline can be rendered as a table or as a component's data. **Inserting and removing.** `insertUI()` and `renderUI()` mount what they add; `removeUI()` on a component's id removes it and destroys its Vue instance, so it stops reporting. **Validation.** shinyvalidate draws its message the way Element draws a failed rule; see the forms article. **Testing.** shinytest2's `set_inputs()` and `get_values()` read and write a component as they do a `textInput()`. An input with no component of its own behind it -- a table's `_selection_rows`, an event such as `_row_click` -- has no input binding, so `set_inputs()` needs `allow_no_input_binding_ = TRUE` for it, and sets the server's value only: the page does not tick the rows. ## Nesting components Two kinds of component wrap other content, and they behave differently. **Containers are plain markup.** `el_row()`, `el_col()`, `el_container()`, `el_collapse()`, `el_tabs()`, `el_dialog()` and `el_drawer()` render Element's own CSS classes and drive their state through a Shiny input binding. They hold anything, and what is inside keeps working as it would on its own. ```{r container, eval = FALSE, shot = TRUE} el_collapse( "panels", value = "one", items = list( list( name = "one", title = "Settings", content = tagList( el_switch("dark", value = TRUE, active_text = "Dark mode"), el_rate("stars", value = 4) ) ), list(name = "two", title = "About", content = tags$p("Version 0.1.0")) ) ) ``` **Wrappers absorb what they are given.** `el_tooltip()`, `el_popover()`, `el_popconfirm()` and `el_infinite_scroll()` compile their content into their own Vue instance, so a component handed to one is folded in rather than nested: the two become a single instance carrying both sets of markup, data and methods. ```{r absorb, eval = FALSE, shot = TRUE, shot_js = "var b = document.querySelector('#shot .el-button'); b.click(); b.click(); ['mouseenter','mouseover'].forEach(function(t){ b.dispatchEvent(new MouseEvent(t, {bubbles: true})); });", shot_sel = ".el-tooltip__popper", shot_wait = 1.5} ui <- el_page( el_tooltip("hint", el_button("save", "Save", type = "primary"), content = "Writes to disk", placement = "right"), verbatimTextOutput("clicks") ) server <- function(input, output, session) { output$clicks <- renderText(paste("Saved", input$save, "times")) # still reports } shinyApp(ui, server) ``` An absorbed component has no host of its own, but it keeps its id: the wrapper lists the components it took in, and `update_el_*()` and `call_el()` for that id reach it through the wrapper's instance. In the page the id is on the component itself, as Element hands it on -- a button's `