--- title: "Migrating from native Shiny controls" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Migrating from native Shiny controls} %\VignetteEncoding{UTF-8} %\VignetteEngine{knitr::rmarkdown} editor_options: markdown: wrap: 72 --- ```{r setup, include=FALSE} knitr::opts_chunk$set(eval = FALSE) library(shiny) library(glasstabs) ``` This guide converts an existing app without changing its downstream code. The filter logic stays identical; only the input construction and the update calls change. The one trap that breaks migrations is the empty-selection default — it is covered first, because it decides the rest. ## The trap: empty does not mean the same thing Native Shiny and glasstabs disagree about the starting state: | Control | `selected = NULL` means | | ------- | ----------------------- | | `selectInput(multiple = TRUE)` | Nothing selected | | `glassMultiSelect()` | Every enabled choice selected | A migration that copies `selected = NULL` across changes the app's initial filter from "nothing" to "everything." Decide which behavior the app had, and write it explicitly — do not let the default decide for you. The behavior-preserving migration below uses `selected = character(0)` to keep the native empty start. ## Before and after: a multi-select filter Before, with native Shiny: ```{r run-native-before} library(shiny) regions <- c(North = "north", South = "south", East = "east") data <- data.frame( region = c("north", "south", "east", "north"), cases = c(12, 8, 5, 3) ) ui <- fluidPage( selectInput( "region", "Region", choices = regions, multiple = TRUE, selected = NULL ), tableOutput("rows") ) server <- function(input, output, session) { output$rows <- renderTable({ data[data$region %in% input$region, , drop = FALSE] }) } shinyApp(ui, server) ``` After, with glasstabs. The downstream filter is untouched: ```{r run-glasstabs-after} library(shiny) library(glasstabs) regions <- c(North = "north", South = "south", East = "east") data <- data.frame( region = c("north", "south", "east", "north"), cases = c(12, 8, 5, 3) ) ui <- fluidPage( useGlassTabs(), glassMultiSelect( "region", choices = regions, label = "Region", selected = character(0), theme = "auto" ), tableOutput("rows") ) server <- function(input, output, session) { output$rows <- renderTable({ data[data$region %in% input$region, , drop = FALSE] }) } shinyApp(ui, server) ``` The example above is complete: it defines its dataset, both app halves, and the app object, so it runs end to end. Two changes beyond the widget swap: `useGlassTabs()` once at the UI root (the one rule), and `selected = character(0)` preserving the native empty start. If the app actually wants the glasstabs default (all enabled selected), drop the `selected` argument and keep the filter — but then say so in the filter's comments, because the next reader will otherwise assume the native default. For a single-select the swap is `selectInput()` (without `multiple = TRUE`) to `glassSelect()`, with the same `useGlassTabs()` addition. The cleared state of a single-select returns `NULL` in both worlds, so no default trap applies there. ## Update function mapping | Native | glasstabs | Notes | | ------ | --------- | ----- | | `updateSelectInput(session, id, choices, selected)` | `updateGlassMultiSelect(session, id, choices, selected, ...)` or `updateGlassSelect(session, id, choices, selected, ...)` | Choose by widget, not by the old call | | `updateSelectizeInput(session, id, choices, selected)` | Same as above | Server-side selectize becomes `server = TRUE` + the `*Server()` helper, not an update call | | `label` argument on the native updaters | No equivalent | The glasstabs updaters take no `label`; a label change means recreating the widget with `renderUI()` | | `selected = NULL` (keep) | `selected = NULL` (keep) | Same meaning on both sides for updates | | `selected = character(0)` (clear) | `selected = character(0)` (clear) | Same meaning on both sides for updates | The update semantics that have no native counterpart are the transaction options: `preserve_selection`, `drop_invalid`, and `notify` on the multi-select updater (see the linked-filters article). A migration that passes only `choices` and `selected` behaves like the native updater; the extra options are opt-in. ## What native selectize can do that glasstabs cannot Be candid about the gaps before migrating a selectize-heavy app: - **User-created choices** (`options = list(create = TRUE)`): no equivalent. glasstabs choices come from the app, not the user. - **Selectize plugins** (`options = list(plugins = ...)`): no equivalent — no drag-and-drop sorting, no remove-button plugin (tag pills have their own remove affordance, but it is not the plugin). - **Server-side selectize as a drop-in**: the mechanism differs. Native selectize queries the server per keystroke through its own protocol; glasstabs server mode uses `server = TRUE` plus a `*Server()` helper with bounded results. The user-visible behavior is close, but the code is not interchangeable. ## When a native control is still the better choice - The app needs user-created choices or a selectize plugin above. - The app must render a plain `