--- title: "Forms and inputs" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Forms and inputs} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = FALSE ) ``` ```{r} library(shiny) library(bootstrict) ``` Form inputs are where `bootstrict` stays closest to Shiny. **Most inputs delegate to the matching `shiny::*Input()`** and only re-dress the markup as Bootstrap 5 — so `input$id` and Shiny's own `updateXxx()` work exactly as you already know. Two native inputs have no Shiny equivalent and get their own `update_bs_*()` helpers. ## The two families | Family | Constructors | Reactive value | Update from server | | --- | --- | --- | --- | | **Delegating** (wrap Shiny) | text, textarea, numeric, password, select, checkbox, switch, radio, checkbox-group, file | `input$id` | Shiny's `updateXxx()` | | **Native** (own binding) | `bs_range_input()`, `bs_color_input()`, `bs_date_input()`, `bs_date_range_input()`, `bs_radio_button_input()`, `bs_checkbox_button_input()` | `input$id` | `update_bs_range()`, `update_bs_color()`, `update_bs_date_input()`, `update_bs_toggle_buttons()` | Every input takes a leading `id`, an optional `label`, and — for delegating inputs — a `help` string that renders as `.form-text` below the control, plus a `width`. Where Bootstrap offers sizing, `size = "sm"` or `"lg"` is available. ## Text-like inputs ```{r} bs_text_input("name", "Your name", placeholder = "Jane Doe") bs_textarea_input("bio", "Bio", rows = 4) bs_numeric_input("age", "Age", value = 30, min = 0, max = 120) bs_password_input("pw", "Password", help = "Must be 8–20 characters.") ``` Signatures share a common shape; `bs_text_input()` is representative: ```{r} bs_text_input( id, label = NULL, value = "", ..., placeholder = NULL, size = NULL, # "sm" or "lg" help = NULL, # .form-text below the control width = NULL ) ``` Because these delegate to Shiny, update them with Shiny's own updaters: ```{r} updateTextInput(session, "name", value = "Colin") ``` ## Select `bs_select_input()` renders a **plain Bootstrap `` (**not** Shiny's `sliderInput()` — no ticks, animation or ion.rangeSlider features): ```{r} bs_range_input("vol", "Volume", value = 50, min = 0, max = 100, step = 1) ``` If `value = NULL`, the browser starts it at the midpoint `(min + max) / 2`. Update it from the server with `update_bs_range(id, value)`: ```{r} update_bs_range("vol", 75) ``` `bs_color_input()` is a native colour picker; `value` must be a 6-digit hex string: ```{r} bs_color_input("col", "Pick a colour", value = "#0d6efd") update_bs_color("col", "#198754") ``` ## Dates and files ```{r} bs_date_input("day", "Pick a day", value = "2026-06-26") bs_date_range_input("range", "Period", start = "2026-01-01", end = "2026-12-31") bs_file_input("upload", "Upload a file", accept = ".csv") ``` These delegate to `shiny::dateInput()`, `dateRangeInput()` and `fileInput()` respectively, so update them with the matching Shiny updater. ## Composing inputs Three layout helpers restyle or group inputs without touching their bindings. ### Input groups `bs_input_group()` places add-ons and controls on a single line. When you pass a full `bs_*_input()`, only its bare control is kept (its label and help text are dropped, since the group is one line): ```{r} bs_input_group( bs_input_group_text("@"), bs_text_input("user", placeholder = "Username") ) bs_input_group( bs_input_group_text("$"), bs_numeric_input("amount", value = 0), bs_input_group_text(".00"), size = "lg" ) ``` A handful of inputs whose binding lives on their container — `bs_date_input()`, `bs_date_range_input()`, `bs_file_input()`, `bs_radio_input()` and `bs_checkbox_group_input()` — cannot be unwrapped into a group and will raise a clear error if you try. ### Floating labels `bs_floating_label()` reshapes an input into the Bootstrap 5 floating-label style. It **moves** the existing control rather than rebuilding it, so the binding is preserved. If you do not pass a `label`, the control's existing label text is reused: ```{r} bs_floating_label(bs_text_input("email", "Email address")) bs_floating_label(bs_select_input("fruit", "Fruit", c("Apple", "Pear"))) ``` The same container-bound inputs listed above cannot be floated. ### Forms, labels, help and validation `bs_form()` is a plain `
` wrapper for grouping controls; `novalidate = TRUE` disables the browser's native validation UI: ```{r} bs_form( bs_text_input("email", "Email"), bs_button("submit", "Submit", color = "primary") ) ``` The lower-level pieces are available when you hand-build a control: - `bs_form_label("email", "Email address")` — a `.form-label` tied to a control by its `for` attribute. - `bs_form_text("Must be 8–20 characters.")` — muted helper text (the same thing the `help=` argument produces). - `bs_valid_feedback("Looks good!")` and `bs_invalid_feedback("Please choose a username.")` — inline validation messages that Bootstrap shows when the control is marked valid/invalid. ## Dates `bs_date_input()` is a native `` with the `.form-control` class, which is what the Bootstrap 5.3 forms page shows: the **browser** supplies the calendar. It deliberately does not delegate to `shiny::dateInput()`, which loads `bootstrap-datepicker` — a third-party stylesheet whose calendar markup (`.datepicker`, `.datepicker-days`, …) appears nowhere in the Bootstrap documentation and which a designer's SASS sheet cannot reach. The cost is real: `format`, `language`, `weekstart` and `datesdisabled` are gone, because the browser owns the presentation and Bootstrap offers no way to ask it for something else. ```{r} bs_date_input("day", "Day", value = "2026-06-26", min = "2026-01-01") bs_date_range_input("period", "Period", start = "2026-01-01") ``` `input$day` is a `Date`, and `NA` while the field is empty. A range is a length-2 `Date`, so it keeps both positions when only one end is filled in. Drive them with `update_bs_date_input()` and `update_bs_date_range_input()` — `shiny::updateDateInput()` will not reach them. Pass `NA` to clear a field, and `NULL` (the default) to leave it alone. ```{r} update_bs_date_input("day", value = Sys.Date()) update_bs_date_range_input("period", start = "2026-02-01", end = NA) ``` Bootstrap has no date-range component, so `bs_date_range_input()` is the assembly its docs prescribe: two fields in an `.input-group` with an `.input-group-text` between them. Each field carries its own `aria-label`, since one label cannot name both. ## Toggle buttons Bootstrap's segmented control is a row of buttons backed by hidden `.btn-check` inputs. Its markup is not the `.form-check` one with a different skin -- the input is a *sibling* of its label -- so these are native controls with their own constructors, driven by `update_bs_toggle_buttons()` rather than `shiny::updateRadioButtons()`. ```{r} bs_radio_button_input("size", "Size", c(Small = "s", Large = "l")) bs_checkbox_button_input("opts", "Options", c("a", "b")) ``` `input$size` is a single string; `input$opts` is a character vector, empty when nothing is picked. `outline = FALSE`, `color`, `size` and `vertical` cover the Bootstrap variants. ## Validation Bootstrap only displays a feedback message when it is a *following sibling* of the control carrying `.is-valid` / `.is-invalid`. A message placed after a `bs_*_input()` is a sibling of shiny's input container, not of the control inside it, so it would never appear. `bs_feedback()` puts it in the right place: ```{r} bs_feedback( bs_text_input("user", "Username"), invalid = "Please choose a username." ) ``` Declare the messages in the UI, then switch the state from the server with `set_bs_validation()`: ```{r} observeEvent(input$submit, { if (input$user %in% taken) { set_bs_validation("user", "invalid", "That name is taken.") } else { set_bs_validation("user", "valid") } }) ``` `set_bs_validation(id, "none")` clears the state. Pass `state =` to `bs_feedback()` when a control should render already marked. The bare `bs_valid_feedback()` / `bs_invalid_feedback()` divs remain useful in a hand-built ``, where you control the sibling order yourself. ## Putting it together A small settings form, driven from the server: ```{r} ui <- bs_page( bs_container( class = "py-4", bs_form( bs_floating_label(bs_text_input("email", "Email address")), bs_range_input("vol", "Volume", value = 50), bs_switch_input("notify", "Email me updates", value = TRUE), bs_button("save", "Save settings", color = "primary") ) ) ) server <- function(input, output, session) { observeEvent(input$save, { str(list(email = input$email, vol = input$vol, notify = input$notify)) update_bs_range("vol", 50) # native input → bootstrict helper updateTextInput(session, "email", value = "") # delegating input → Shiny }) } shinyApp(ui, server) ```