| Title: | Component-Based 'Shiny' UI with Fine-Grained Reactivity |
|---|---|
| Description: | A component-based reactive UI framework for 'Shiny' that binds reactive values directly to DOM attributes, enabling fine-grained updates without re-rendering or 'update*Input' callbacks. |
| Authors: | Kyle Husmann [aut, cre] |
| Maintainer: | Kyle Husmann <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.3.0 |
| Built: | 2026-10-10 15:48:04 UTC |
| Source: | https://github.com/khusmann/irid |
Match()
Define a case for Match()
Case(predicate, body)Case(predicate, body)
predicate |
One of: a function |
body |
A function |
A case definition (a list).
Match()
Sugar for Case(\() TRUE, body) — matches when no earlier Case does.
Place this as the last argument to Match.
Default(body)Default(body)
body |
A function returning the tag tree to render when no other
case matches. Same arity rules as |
A case definition (a list).
Shorthand for Output(DT::renderDT, DT::DTOutput, ...). Requires the
DT package.
DTOutput(fn, ...)DTOutput(fn, ...)
fn |
A function that produces a DataTable. |
... |
Additional arguments passed to |
A irid output node.
Iterates over a reactive list and calls fn for each item. The
callback receives a per-item callable (a mini-store for record items,
a scalar accessor for atomic items) and an optional position
accessor. The reconciliation strategy is selected by by:
Each(items, fn, by = NULL)Each(items, fn, by = NULL)
items |
A reactive expression that returns a list. |
fn |
A function of |
by |
|
Positional (by = NULL, the default) — slot i is slot i.
The list can grow or shrink at the end; in-place value changes
update each slot's accessor without DOM recreation.
Keyed (by = \(x) x$id) — items are tracked across reorders,
adds, and removes by their key. Kept items propagate new values
through their mini-store (only changed leaves fire); reordered
items have their DOM nodes moved (no recreation).
Records are projected as a per-item mini-store: item() reads the
whole record, item(record) writes it back, item$field() reads a
leaf, and item$field(v) is a synthetic setter that writes through
the parent. Scalars are passed as a per-item callable: item() reads,
item(value) writes back to the parent's slot.
Records may be heterogeneous in shape — different leaf trees per
entry. Each slot's mini-store is sized to its own item, and a
Match() inside the body dispatches on the discriminator:
Each(state$blocks, by = \(b) b$id, \(block) {
Match(block,
Case(\(b) b$type == "heading", \(b) Heading(b)),
Case(\(b) b$type == "paragraph", \(b) Paragraph(b)),
Case(\(b) b$type == "todo", \(b) Todo(b))
)
})
When a record's shape changes (different key set, or a sub-record's shape shifts), that one entry is torn down and rebuilt with the new mini-store. Shape-stable updates use the fine-grained in-place path.
Mixing records and scalars in the same list is rejected at flush
time as a likely data-modeling slip — wrap scalars in
list(value = ...) to mix them.
A irid control-flow node.
Builds a Shiny app from a function that returns a irid tag tree. The function is called once per session so that each client gets its own reactive state.
iridApp(fn, ...)iridApp(fn, ...)
fn |
A zero-argument function that returns a irid tag tree (e.g. a
|
... |
Additional arguments passed to |
A Shiny app object.
Launches one of the example apps shipped with the package, mirroring
shiny::runExample(). The examples are the same apps published as editable
editors on the package website.
iridExample( example = NA, port = getOption("shiny.port"), launch.browser = getOption("shiny.launch.browser", interactive()), host = getOption("shiny.host", "127.0.0.1"), display.mode = c("showcase", "normal", "auto") )iridExample( example = NA, port = getOption("shiny.port"), launch.browser = getOption("shiny.launch.browser", interactive()), host = getOption("shiny.host", "127.0.0.1"), display.mode = c("showcase", "normal", "auto") )
example |
Name of the example to run (e.g. |
port |
The TCP port the application should listen on. Defaults to
|
launch.browser |
If |
host |
The IPv4 address the application should listen on. Defaults to
|
display.mode |
The display mode. Defaults to |
Called with no argument (or an unrecognised name), it lists the available examples instead of launching one.
Some examples depend on packages beyond irid's hard dependencies (e.g.
bslib, plotly). The runner checks for these up front and, if any are
missing, stops with a message naming what to install. A couple of examples
(codemirror, plotly) also load JavaScript from a CDN at runtime, so they
need an internet connection even when run locally.
By default the app runs in Shiny's showcase display mode, so the annotated
source appears beside the running app (as with shiny::runExample()). Pass
display.mode = "normal" to run the app on its own.
The remaining arguments mirror shiny::runExample().
If example is supplied, launches the app and blocks until it is
stopped. Otherwise returns the available example names invisibly as a
character vector (after listing them).
# List the available examples iridExample() if (interactive()) { iridExample("todo") }# List the available examples iridExample() if (interactive()) { iridExample("todo") }
Creates a shiny::uiOutput() with the irid JavaScript dependency
attached. Use this in a standard Shiny UI to mark where renderIrid()
should inject its content.
iridOutput(id)iridOutput(id)
id |
The output ID, matching the corresponding |
An HTML tag with the irid dependency.
IridWidget() is the third irid process-tags citizen (alongside
control-flow nodes and Output). It emits a container element plus an
init record that mount turns into a widget-init op.
The client's irid.defineWidget("<name>", factory) registration is
looked up by name and called once per mount. The factory may return its
{update, destroy} handle directly or a Promise of it — make it async
and await whatever its construction needs first (a script-tag library
global, an ESM import, a WASM init). irid buffers updates during the
wait and disposes cleanly on a teardown mid-construction. See the JS-side
API in ARCHITECTURE.md.
IridWidget( name, props = list(), events = list(), deps = NULL, container = NULL )IridWidget( name, props = list(), events = list(), deps = NULL, container = NULL )
name |
Widget registry name, matching a JS-side
|
props |
Named list of props. Callable values are two-way-capable
bindings; non-callable values are init-only constants. |
events |
Named list of notifications (client → server), keyed by wire
event name. Each value is a handler, a |
deps |
Optional |
container |
Optional |
Props are two-way-capable by default, exactly like DOM value /
checked. A callable prop (reactiveVal, store leaf, reactiveProxy,
...) reads inbound to the widget (server → client, routed to the
factory's update(key, value) hook) and accepts write-back: when the
widget JS calls setProp(key, value), irid writes the value through the
bound reactive (gated by writability — a read-only reactive's write is
dropped and the canonical value is snapped back). Whether a prop is
actually two-way depends on whether the widget JS pushes through
setProp; the snap-back machinery is latent until it does. A non-callable
prop rides in the init message as a constant and is never re-sent.
Wrap a prop in wire() only to tune its round-trip timing
(content = wire(content, wire_debounce(200))) — never to enable or
disable two-way. To react to a prop's change, observe the bound reactive
or pass a reactiveProxy; a bound prop is not also handled.
events carries genuine notifications the widget emits that correspond to
no prop (e.g. cursor-changed). Keys are the wire event names the widget
JS passes to sendEvent() (lowercase kebab-case by web CustomEvent
convention). Each value is a handler or a wire() (to tune timing);
NULL entries are dropped so optional handlers forward declaratively.
dom_opts is illegal on a widget event.
A irid widget construct with class irid_widget.
Evaluates cases in order against a bound value and renders the body of
the first case whose predicate is TRUE. Records are projected as a
mini-store for the active case's body; scalars are passed as the bare
callable. On active-case change, the previous case is torn down (its
reactives, DOM, and mini-store) and the new case is mounted fresh.
Match(callable, ...)Match(callable, ...)
callable |
The bound value — any 0-arg callable. Records (named lists) are projected as a mini-store; scalars are passed as the bare callable. |
... |
Use a choice function as the leading callable to fold unrelated reactive state into a tagged variant on the fly:
Match(\() if (loading()) list(tag = "loading") else list(tag = "data", x = data()), Case(\(r) r$tag == "loading", \() Spinner()), Case(\(r) r$tag == "data", \(r) Items(r$x)) )
Conceptually, When() is a fixed-shape binary specialization of Match.
A irid control-flow node.
A generic wrapper that pairs a Shiny render function with its
corresponding output function. For common cases, use the convenience
wrappers PlotOutput(), TableOutput(), or DTOutput().
Output(render_fn, output_fn, fn, ...)Output(render_fn, output_fn, fn, ...)
render_fn |
A Shiny render function (e.g. |
output_fn |
A Shiny output function (e.g. |
fn |
A function passed to |
... |
Additional arguments passed to |
A irid output node.
A first-class output primitive for interactive plotly charts, on par with
PlotOutput and TableOutput. Unlike the {plotly} htmlwidget — which
destroys and recreates the chart on every reactive update — PlotlyOutput
uses Plotly.react() for incremental updates, so a data change preserves
the user's zoom, pan, and selection (via plotly's uirevision).
PlotlyOutput( spec, ..., onClick = NULL, onHover = NULL, onUnhover = NULL, onDoubleclick = NULL, onDeselect = NULL, onSelecting = NULL, onBrushing = NULL, onLegendClick = NULL, onLegendDoubleclick = NULL, onClickAnnotation = NULL, onSunburstClick = NULL, onRelayout = NULL, container = NULL )PlotlyOutput( spec, ..., onClick = NULL, onHover = NULL, onUnhover = NULL, onDoubleclick = NULL, onDeselect = NULL, onSelecting = NULL, onBrushing = NULL, onLegendClick = NULL, onLegendDoubleclick = NULL, onClickAnnotation = NULL, onSunburstClick = NULL, onRelayout = NULL, container = NULL )
spec |
A zero-argument function returning a plotly object (from
|
... |
Named state arguments, each a callable ( A
|
onClick, onHover, onUnhover, onDoubleclick
|
Discrete pointer callbacks. Each receives the (slimmed) plotly event payload. |
onDeselect, onSelecting, onBrushing
|
Selection-lifecycle notifications.
|
onLegendClick, onLegendDoubleclick, onClickAnnotation, onSunburstClick
|
Discrete interaction callbacks. |
onRelayout |
Escape hatch — receives the raw |
container |
Optional |
PlotlyOutput is a thin wrapper over IridWidget. User-controllable UI
state (axis ranges, drag mode, selection, trace visibility) is exposed as
named reactive arguments, each a two-way prop: bind a reactiveVal,
store leaf, or reactiveProxy and the user's interaction writes back to it.
A reactiveProxy that rejects a write snaps the plot back. Discrete events
(clicks, hovers, legend interactions) are plain on* callbacks.
An irid_widget construct.
Shorthand for Output(renderPlot, plotOutput, ...).
PlotOutput(fn, ...)PlotOutput(fn, ...)
fn |
A function that produces a plot. |
... |
Additional arguments passed to |
A irid output node.
Builds a callable proxy from a 0-arg get reader and an optional
1-arg set writer. The proxy is itself a callable: proxy() invokes
get(), proxy(value) invokes set(value). Auto-bind treats the
proxy like any other callable, so it composes with value and
checked props without any special handling.
reactiveProxy(get, set = NULL)reactiveProxy(get, set = NULL)
get |
A 0-arg callable returning the read value. Typically a
|
set |
A 1-arg function called with the incoming value on write,
or |
set is a side-effectful handler, not a pure transform. It receives
the incoming value and decides what to do — write to a target, write
a transformed value, set an error flag, trigger a side effect, or
drop the write entirely. Because set is a closure, it can read
sibling state for cross-field validation.
Pass set = NULL (or omit it) to make the proxy read-only — writes
are silently dropped. With auto-bind, this lets the input snap back
to the current value via the optimistic-update protocol.
Proxies compose: a proxy is itself a callable (0-arg → get, 1-arg →
set), so another reactiveProxy can use it as either its get or
its set.
A callable with class c("reactiveProxy", "reactive", "function").
Builds a callable hierarchical state tree from a nested list. The shape
rule is simple: a bare named list (length > 0) becomes a navigable
reactiveStore node (every sub-tree is itself a reactiveStore);
everything else becomes a plain shiny::reactiveVal() leaf that accepts
any value on write. To force a bare named list to be treated as a leaf
instead of a store, wrap it in base::I().
reactiveStore(initial)reactiveStore(initial)
initial |
A bare named list describing the initial shape.
Sub-positions classify as follows: bare named lists (length > 0)
become store sub-nodes; anything else (scalars, vectors, |
Every node is callable: node() reads, node(value) writes. Branch
writes replace: the input must list every locked key for that branch.
Both unknown keys and missing keys are an error. Types are not enforced.
Per-field writes (node$key(value)) remain the dedicated single-slot
path — use them when you want to update one field without naming the
rest.
length() on a leaf returns 1 (the underlying reactiveVal is a
callable, and neither R's closure default nor shiny override
length). htmltools' dropNullsOrEmpty calls length() on every
attribute value, so this is what lets value = state$leaf survive
tag construction. Use length(leaf()) for the underlying length.
A callable reactiveStore node with class
c("reactiveStore", "reactive", "function"). Sub-trees share the
same class. Leaf positions are plain shiny::reactiveVal()s with
class c("reactiveVal", "reactive", "function") — the shared
"reactive" class is what auto-bind dispatches on.
These functions are re-exported from shiny for convenience, so you can use them in irid apps without loading shiny explicitly.
reactiveVal(): Create a reactive value
reactive(): Create a reactive expression
observe(): Create an observer
observeEvent(): Create an event-driven observer
isolate(): Run an expression without reactive dependencies
tags: HTML tag builder
tagList(): Combine tags into a list
See the shiny documentation for full details.
The return values are those of the underlying shiny functions:
reactiveVal() returns a function that reads the current value when
called with no argument and sets it when called with one value.
reactive() returns a reactive expression (a function of class
reactiveExpr/reactive) that yields the cached expression value when
called.
observe() and observeEvent() return an observer reference object
(class Observer) invisibly; they are called for their side effects.
isolate() returns the value of expr, evaluated without taking a
reactive dependency.
tags is a named list of functions that construct HTML tag objects, and
tagList() returns a list of tags (class shiny.tag.list).
A render function for use with iridOutput(). Evaluates expr to
produce a irid tag tree, processes it, and mounts reactive bindings and
event handlers after the UI is flushed.
renderIrid(expr, env = parent.frame(), quoted = FALSE)renderIrid(expr, env = parent.frame(), quoted = FALSE)
expr |
An expression that returns a irid tag tree. |
env |
The environment in which to evaluate |
quoted |
If |
A shiny::renderUI() result.
Shorthand for Output(renderTable, tableOutput, ...).
TableOutput(fn, ...)TableOutput(fn, ...)
fn |
A function that produces a table. |
... |
Additional arguments passed to |
A irid output node.
Renders the yes branch when condition is TRUE, and otherwise
(if provided) when it is FALSE. The active branch is fully mounted
and the inactive branch is destroyed.
When(condition, yes, otherwise = NULL)When(condition, yes, otherwise = NULL)
condition |
A reactive expression that returns a logical value. |
yes |
A 0-arg function returning the tag tree to render when the
condition is |
otherwise |
An optional 0-arg function returning the tag tree to
render when the condition is |
Conceptually a fixed-shape binary specialization of Match():
When(\() cond, \() yes, \() no) is equivalent to
Match(\() cond, Case(TRUE, \() yes), Case(FALSE, \() no)).
Bodies are 0-arg functions that return tag trees — not tag trees
directly. When mounts and unmounts the active branch on transition,
so each activation must construct a fresh tag tree (the previous
branch's closures were torn down with its reactives). Reach for
Match() when the branch needs to consume the dispatching value.
A irid control-flow node.
Configure how an event handler or value binding is dispatched between the
browser and the server. A single per-slot carrier, wire(), rides
the slot it configures — an on* handler slot or a value/checked
binding slot — so an event's timing, backpressure, and DOM-listener
options live next to the handler/reactive they govern rather than in
separate element-level lists.
wire_immediate() wire_throttle(ms, leading = TRUE) wire_debounce(ms) wire_dom_opts( prevent_default = FALSE, stop_propagation = FALSE, capture = FALSE, passive = FALSE, filter = NULL ) wire(subject = NULL, timing = NULL, coalesce = NULL, dom_opts = NULL)wire_immediate() wire_throttle(ms, leading = TRUE) wire_debounce(ms) wire_dom_opts( prevent_default = FALSE, stop_propagation = FALSE, capture = FALSE, passive = FALSE, filter = NULL ) wire(subject = NULL, timing = NULL, coalesce = NULL, dom_opts = NULL)
ms |
Minimum interval (throttle) or quiet period (debounce) in milliseconds. |
leading |
If |
prevent_default |
Call |
stop_propagation |
Call |
capture |
Register the listener in the capture phase. |
passive |
Register the listener as passive. |
filter |
A JavaScript expression string, evaluated client-side with
the event object bound to |
subject |
The handler or reactive the wire configures. The slot
decides which: a bare callable means "bind" in |
timing |
An |
coalesce |
Logical scalar, or |
dom_opts |
A |
wire() returns an irid_wire; the timing constructors
return an irid_wire_timing; wire_dom_opts() returns an irid_dom_opts.
The timing constructors are pure shapes — they describe when an event fires and carry no other config:
wire_immediate(): Fires on every event with no rate limiting.
wire_throttle(ms, leading): Fires at most every ms milliseconds
while the event is active.
wire_debounce(ms): Waits until the user pauses for ms milliseconds
before firing.
When a wire carries no timing, irid applies a per-event default keyed on
the DOM event name: input → wire_debounce(200) (typing produces a
flood of intermediate values); the high-frequency continuous streams
(mousemove, pointermove, touchmove, drag, dragover, scroll,
wheel, resize) → wire_throttle(100) (paced "latest position"
stream, with the derived coalesce = TRUE keeping it from outrunning the
server); every other event → wire_immediate(). Explicit timing always
wins over the default.
coalesce)coalesce is universal across timing modes, so it lives on the carrier,
not in the shapes. When TRUE, dispatch gates on server-idle state so
events never queue faster than the server can process them. When NULL
(the default), it derives from the timing mode: FALSE for
wire_immediate(), TRUE for wire_throttle() / wire_debounce().
wire_dom_opts() bundles the DOM-only listener flags. It is legal only
where the event is backed by a real DOM listener (a plain tag, a custom
element emitting cancelable events); placing it on a widget-emitted event
errors. Whether prevent_default has any effect further depends on the
event being cancelable — a runtime fact.
filter is a JavaScript expression string evaluated client-side with the
DOM event object bound to e. When it evaluates falsy the event is
dropped entirely — no prevent_default/stop_propagation, no server
round-trip — so a handler that only cares about some events never floods
the server with the rest. For example, an onKeyDown that acts only on
Enter takes filter = "e.key === 'Enter'".
The event argument passed to handlers is a list containing all
primitive-valued properties (string, numeric, logical) from the browser
event object, plus these element properties:
valueThe element's current value (character).
valueAsNumberNumeric value of the element, or NA if the
input is empty or non-numeric (e.g. a blank text box). Useful for
range and number inputs.
checkedLogical, for checkbox and radio inputs.
Keyboard events additionally include key, code, ctrlKey,
shiftKey, altKey, and metaKey.