| Title: | Bootstrap Layout for 'shinyreact' Apps |
|---|---|
| Description: | R-side helpers for building Shiny apps in the shinyreact 'ui.tsx' pattern that look like 'bslib' apps. Provides page dependency discovery so a client bundle can request the same CSS and JavaScript a 'bslib' page would attach on its own, and a hosted-input helper for widgets rendered server-side and displayed by the client. The matching React components live in the 'bslibreact' npm package. |
| Authors: | Samuel Bharti [aut, cre, cph] |
| Maintainer: | Samuel Bharti <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.1.0 |
| Built: | 2026-11-15 14:53:21 UTC |
| Source: | https://github.com/samuelbharti/bslibreact |
Returns the htmltools::htmlDependency() for the vendored
bslibreact.js bundle (and its stylesheet, when one is built), which
sets window.bslibreact. Attach it only for a client with no build step
that reads the kit from that global, for example
page_react(page_deps(app_theme), bslibreact_dep(), theme = app_theme).
A client built with Vite bundles the kit itself and does not need it.
bslibreact_dep()bslibreact_dep()
An htmltools::htmlDependency().
The bundle reads window.shinyreact as it evaluates, so it must run after
shinyreact's own bundle. shinyreact::page_react() attaches that first,
and both scripts use defer, which keeps document order. The app's own
ui.js also runs before this bundle, so a client script reads
window.bslibreact inside a window load listener.
pkg-js has built anythingThis function never errors for a missing bundle file: an
htmltools::htmlDependency() does not check that its files exist when
it is constructed, only once something actually tries to serve them, so
this returns a structurally valid dependency even when
inst/lib/bslibreact/bslibreact.js has not been vendored in yet. This
matters during this repository's
own development, where pkg-js's build and pkg-r's own checks can run
before make update-dist has ever populated inst/lib/bslibreact/.
dep <- bslibreact_dep() dep$name dep$scriptdep <- bslibreact_dep() dep$name dep$script
Registers widget as a shiny::renderUI() output named
paste0(id, suffix), the way shinyreact::page_react() apps host a
bslib or shiny widget the client displays with <ShinyOutput id="<id><suffix>" /> (or the matching kit component, HostedInput,
from the bslibreact npm package) instead of rebuilding the widget's
markup in React. See docs/decisions/0003-hosted-widgets.md in the
repository for the reasoning behind hosting widgets this way instead of
driving them from React state.
host_input(output, id, widget, suffix = "_ui")host_input(output, id, widget, suffix = "_ui")
output |
The |
id |
A short, stable identifier for this hosted widget. This is
not itself a DOM id: it is combined with |
widget |
An expression that builds the widget, evaluated lazily
inside |
suffix |
Appended to |
The output name (paste0(id, suffix)), invisibly.
A hosted widget contributes two elements to the page, not one: the
output wrapper Shiny creates for output[[paste0(id, suffix)]] (a
<div id="foo_ui" class="shiny-html-output">, in the default case), and
the widget's own element, with the widget's own input id, nested inside
it (a <div id="foo" class="form-group shiny-input-container">, for a
shiny::sliderInput("foo", ...)). These are two different ids on
purpose: the wrapper is what the client's HostedInput component
addresses to swap in new markup, and the widget's own id is what Shiny's
input binding addresses to read input$foo. If the two ever match (a
widget whose own id is "foo_ui", or suffix = ""), the page holds two
elements under one id; document.getElementById(), Shiny's own
$("#foo") lookups, and every downstream consumer of that id become
ambiguous, silently addressing whichever one the browser happens to
return first. check_no_id_collision() (an internal helper) catches
this at render time with a message naming the id involved, instead of
leaving it to surface later as an input or output that mysteriously
never updates.
The output keeps rendering while it is hidden (suspendWhenHidden = FALSE), so a widget inside a closed panel or an inactive tab exists
before it is first shown, as it would in the server-rendered app.
## Not run: server <- function(input, output, session) { host_input(output, "bins", shiny::sliderInput("bins", "Bins", 1, 50, 30)) # The client displays this with <ShinyOutput id="bins_ui" />, and reads # the slider's own value the usual way, from input$bins. } ## End(Not run)## Not run: server <- function(input, output, session) { host_input(output, "bins", shiny::sliderInput("bins", "Bins", 1, 50, 30)) # The client displays this with <ShinyOutput id="bins_ui" />, and reads # the slider's own value the usual way, from input$bins. } ## End(Not run)
Renders a representative sample of bslib and shiny markup under theme
and returns the htmltools::htmlDependency() objects that markup
attaches, deduplicated, minus the "bootstrap" and "jquery"
dependencies the page function attaches itself (page_react() attaches
Bootstrap only when it is given a theme). Attach the result to a
shinyreact page, for example
shinyreact::page_react(page_deps(app_theme), theme = app_theme), so the
client gets exactly the CSS and JavaScript a matching bslib page
would have attached on its own: sliderInput()'s ionRangeSlider skin,
selectizeInput()'s selectize assets, bslib::accordion()'s and
bslib::tooltip()'s Bootstrap bundles, and so on.
page_deps(theme = NULL, ..., components = NULL)page_deps(theme = NULL, ..., components = NULL)
theme |
A |
... |
Additional tags to include in the dependency sample, unnamed
(checked with |
components |
Character vector of catalog sample names to render for
their dependencies, or |
"bootstrap" itself is dropped from the result because a shinyreact page
already attaches its own theme through the page function's own theme=
argument (or gets no theme at all); attaching it twice serves the
compiled Bootstrap CSS from two dependency entries pointed at two
separately compiled copies, which is wasted bytes at best and, if the
two copies were compiled from different themes, a page that looks
different depending on which <link> happens to load last.
A list of htmltools::htmlDependency() objects, one per
dependency name, keeping the highest version of each, and with no
dependency named "bootstrap" or "jquery".
theme, when supplied, is set with bslib::bs_global_set() for the
duration of this call and restored on exit. Several bslib widgets,
sliderInput()'s ionRangeSlider skin among them, resolve their CSS
through bslib::bs_dependency_defer(), which reads the globally active
theme at render time. Outside a running Shiny session, which is where
this function usually runs, that would be bslib's stock theme.
Set a global theme with bs_global_set(), not
bslib::bs_global_theme(): its first argument is a Bootstrap version
number, so passing it a theme object builds a different, stock theme
and drops custom fonts.
deps <- page_deps(bslib::bs_theme(version = 5)) length(deps) # A themeless app still gets the widget dependencies it needs (the # ionRangeSlider skin, selectize assets, and so on), just with no # bslib/Bootstrap theme compiled in. length(page_deps(theme = NULL)) > 0 ## Not run: # A real app, with a client built by Vite: app_theme <- bslib::bs_theme(version = 5) ui <- shinyreact::page_react( page_deps(app_theme), title = "Demo", theme = app_theme ) ## End(Not run)deps <- page_deps(bslib::bs_theme(version = 5)) length(deps) # A themeless app still gets the widget dependencies it needs (the # ionRangeSlider skin, selectize assets, and so on), just with no # bslib/Bootstrap theme compiled in. length(page_deps(theme = NULL)) > 0 ## Not run: # A real app, with a client built by Vite: app_theme <- bslib::bs_theme(version = 5) ui <- shinyreact::page_react( page_deps(app_theme), title = "Demo", theme = app_theme ) ## End(Not run)