--- title: "Driving the sidebar primitive" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Driving the sidebar primitive} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include=FALSE} knitr::opts_chunk$set(eval = FALSE, comment = "#>") ``` `blockr.ui` ships a Bootstrap-free slide-in panel that mirrors `shiny::showModal()` / `shiny::removeModal()` in spirit. Mount the panel once per page with `sidebar_ui()`, then drive its content from any `moduleServer()` body using `show_sidebar()` and `hide_sidebar()`. ## Minimal example ```{r} library(shiny) library(blockr.ui) ui <- fluidPage( actionButton("open", "Open sidebar"), sidebar_ui("main_sidebar") ) server <- function(input, output, session) { observeEvent(input$open, { show_sidebar( "main_sidebar", title = "Add a new block", ui = tagList( textInput("block_name", "Name"), selectInput("block_kind", "Type", c("data", "transform", "plot")), actionButton("confirm", "Add", class = "btn-primary") ) ) }) observeEvent(input$confirm, { # validate, mutate state, ... hide_sidebar("main_sidebar") }) } shinyApp(ui, server) ``` The inputs inside the body (`input$block_name`, `input$block_kind`, `input$confirm`) live in the calling session - exactly as they would inside a `modalDialog()`. No nested `moduleServer()` is required. ## Observing user-driven dismissals Each `sidebar_ui()` call also registers a Shiny input under the same id, exposing two logical fields: - `input[[id]]$open` - `TRUE` while the panel is open, `FALSE` once the user closes it (Esc, X button) or the server calls `hide_sidebar()`. - `input[[id]]$pinned` - `TRUE` while the user has the panel pinned. ```{r} observe({ state <- input$main_sidebar req(state) cat("open:", state$open, " pinned:", state$pinned, "\n") }) ``` ## Static body + client-side trigger For a panel whose body is known at UI-build time (a settings form, a help panel, an "About" sheet), you don't need a server round-trip to open it. Pre-render the body at `sidebar_ui()` time and put `data-blockr-sidebar-target = ""` on any element that should toggle it. ```{r} ui <- fluidPage( actionButton( "settings_btn", icon("gear"), `data-blockr-sidebar-target` = "settings_sidebar" ), sidebar_ui( "settings_sidebar", side = "left", title = "Board options", ui = tagList( checkboxInput("opt_a", "Option A", value = TRUE), sliderInput("opt_b", "Option B", min = 0, max = 100, value = 50) ) ) ) ``` Clicking the gear toggles the panel open / closed entirely on the client. The inputs inside (`input$opt_a`, `input$opt_b`) are observable from the server exactly as if they sat in the page directly. A later `show_sidebar("settings_sidebar")` (no `ui`) still opens the panel from the server when needed; `show_sidebar("settings_sidebar", ui = )` replaces the pre-rendered body if you want to swap it dynamically. ## One id per concern `show_sidebar()` replaces the panel's body and title in place, and pin state is bound to the panel element. Re-showing the same panel with fresh content from the same caller (the "chain after confirm" flow, which `keep_or_hide_sidebar()` builds on) is the supported pattern. Two unrelated concerns - say a "Settings" panel and an "Add block" form - should mount two `sidebar_ui()` panels with distinct ids. Sharing one id between unrelated callers means the second `show_sidebar()` will silently overwrite the first, including any pinned content the user expected to stay put. ## Auto-open on an empty board A common renderer pattern is to open the sidebar with a "first block" hint when the session starts on an empty board, while still respecting a later user dismissal. The recipe is built on top of `show_sidebar()` and `input[[id]]$open` - no special opt-in is needed on `sidebar_ui()`. ```{r} empty_board_renderer <- function(id, board) { moduleServer(id, function(input, output, session) { auto_opened <- reactiveVal(FALSE) observe({ if (auto_opened()) return() if (length(blockr.core::board_blocks(board)) > 0L) return() show_sidebar( session$ns("main_sidebar"), title = "Add your first block", ui = tagList( p("Pick a block to get started."), actionButton(session$ns("add_dataset"), "Dataset block"), actionButton(session$ns("add_import"), "Import data") ) ) auto_opened(TRUE) }) # Once the user dismisses the hint, input[[id]]$open flips to FALSE # and the auto_opened flag prevents this observer from re-opening. }) } ``` Push-mode panels reflow the page content via padding on ``, driven by `--blockr-sidebar-width-left` and `--blockr-sidebar-width-right` custom properties (both nonzero when both sides are open). The legacy `--blockr-sidebar-width` is also set to the open side's width when a single panel is open, so existing consumers of the form `margin-right: var(--blockr-sidebar-width, 0px)` keep working.