Package {irid}


Title: Component-Based 'Shiny' UI with Fine-Grained Reactivity
Version: 0.3.0
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.
License: MIT + file LICENSE
URL: https://irid.kylehusmann.com, https://github.com/khusmann/irid
BugReports: https://github.com/khusmann/irid/issues
Depends: R (≥ 4.1.0)
Imports: cli, htmltools, rlang (≥ 1.2.0), shiny
Suggests: bslib, callr, chromote, DT, ggplot2, httpuv, jsonlite, knitr, pkgload, plotly, purrr, rmarkdown, testthat (≥ 3.0.0), tibble, vctrs, withr
VignetteBuilder: knitr
Encoding: UTF-8
RoxygenNote: 7.3.3
Config/testthat/edition: 3
NeedsCompilation: no
Packaged: 2026-07-23 01:54:48 UTC; khusmann
Author: Kyle Husmann [aut, cre]
Maintainer: Kyle Husmann <irid@kylehusmann.com>
Repository: CRAN
Date/Publication: 2026-08-02 16:10:02 UTC

irid: Component-Based 'Shiny' UI with Fine-Grained Reactivity

Description

logo

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.

Author(s)

Maintainer: Kyle Husmann irid@kylehusmann.com

See Also

Useful links:


Define a case for Match()

Description

Define a case for Match()

Usage

Case(predicate, body)

Arguments

predicate

One of: a function ⁠\(v) cond⁠ of the bound value, a function ⁠\() cond⁠ ignoring the bound value (cross-cutting), or a literal value (matched against the bound value via identical()).

body

A function ⁠\(v) tag_tree⁠ or ⁠\() tag_tree⁠ that returns the tag tree to render when this case is active. The function is called fresh on each case activation — the active case's reactives and DOM are torn down on transition, so a tag tree captured outside the function would reference dead closures.

Value

A case definition (a list).


Embed a DT DataTable output in a irid tag tree

Description

Shorthand for Output(DT::renderDT, DT::DTOutput, ...). Requires the DT package.

Usage

DTOutput(fn, ...)

Arguments

fn

A function that produces a DataTable.

...

Additional arguments passed to DT::DTOutput().

Value

A irid output node.


Define a default (fallback) case for Match()

Description

Sugar for ⁠Case(\() TRUE, body)⁠ — matches when no earlier Case does. Place this as the last argument to Match.

Usage

Default(body)

Arguments

body

A function returning the tag tree to render when no other case matches. Same arity rules as Case() body.

Value

A case definition (a list).


Render a reactive list

Description

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:

Usage

Each(items, fn, by = NULL)

Arguments

items

A reactive expression that returns a list.

fn

A function of (item), ⁠(item, pos)⁠, or ⁠()⁠. item is the per-item callable (mini-store or scalar accessor); pos is a 0-arg reactive accessor returning the item's current 1-indexed slot. pos is constant under by = NULL (positional identity) and live under by = fn (fires on reorder).

by

NULL for positional reconciliation, or a function that extracts a unique comparable key from each item for keyed reconciliation.

Details

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.

Value

A irid control-flow node.


Construct a widget — a wrapper for an arbitrary JavaScript library

Description

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.

Usage

IridWidget(
  name,
  props = list(),
  events = list(),
  deps = NULL,
  container = NULL
)

Arguments

name

Widget registry name, matching a JS-side irid.defineWidget("<name>", ...) call. Required, non-empty character scalar.

props

Named list of props. Callable values are two-way-capable bindings; non-callable values are init-only constants. NULL entries are forwarded to JS as null (not dropped). Wrap a value in wire() to tune its write-back timing.

events

Named list of notifications (client → server), keyed by wire event name. Each value is a handler, a wire(), or NULL (dropped). dom_opts is not allowed.

deps

Optional html_dependency or list of them. Required for any widget whose JS isn't already loaded by some other means.

container

Optional shiny.tag for the wrapper element. Defaults to tags$div(). irid sets id and data-irid-widget automatically. Configure any DOM events on the container by wrapping their handlers in wire() on the container directly.

Details

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.

Value

A irid widget construct with class irid_widget.


Render the first matching case

Description

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.

Usage

Match(callable, ...)

Arguments

callable

The bound value — any 0-arg callable. Records (named lists) are projected as a mini-store; scalars are passed as the bare callable.

...

One or more Case() / Default() values.

Details

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.

Value

A irid control-flow node.


Embed a Shiny render/output pair in a irid tag tree

Description

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().

Usage

Output(render_fn, output_fn, fn, ...)

Arguments

render_fn

A Shiny render function (e.g. renderPlot).

output_fn

A Shiny output function (e.g. plotOutput).

fn

A function passed to render_fn.

...

Additional arguments passed to output_fn.

Value

A irid output node.


Embed a plot output in a irid tag tree

Description

Shorthand for Output(renderPlot, plotOutput, ...).

Usage

PlotOutput(fn, ...)

Arguments

fn

A function that produces a plot.

...

Additional arguments passed to shiny::plotOutput().

Value

A irid output node.


Reactive plotly output

Description

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).

Usage

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
)

Arguments

spec

A zero-argument function returning a plotly object (from plotly::plot_ly() or plotly::ggplotly()). Re-evaluated reactively; Plotly.react() diffs the result client-side.

...

Named state arguments, each a callable (reactiveVal, store leaf, reactiveProxy) or constant. Recognized: xaxis_range, yaxis_range, ⁠xaxis<n>_range⁠, ⁠yaxis<n>_range⁠, dragmode, hovermode, selected_ids, trace_visibility. NULL means "don't override the spec". Unknown names error — use onRelayout for fields outside the table.

A ⁠*_range⁠ value is a length-2 vector of the axis's own units: numeric for a continuous, log, or category axis; ISO date-time strings (c("2020-04-01", "2020-06-30")) for a date axis, matching how plotly reports and accepts them.

selected_ids is the box/lasso selection, keyed on plotly's per-point ids (a character vector of id values). Binding it requires the plot to supply ids (plot_ly(ids = ~key) / aes(ids = key)); because the value is keyed on identity rather than position, the selection survives data changes that renumber points (filtering, sorting, trace recomposition).

trace_visibility is the per-trace on/off state, a sparse named character vector keyed by trace name (c("8" = "legendonly")), values in "true" / "false" / "legendonly". Like selected_ids it is identity-keyed, so it stays correct across trace recomposition; binding it requires the traces to be named.

onClick, onHover, onUnhover, onDoubleclick

Discrete pointer callbacks. Each receives the (slimmed) plotly event payload.

onDeselect, onSelecting, onBrushing

Selection-lifecycle notifications. onDeselect is a side-effect notification only — when selected_ids is bound, the clear already flows through its prop channel.

onLegendClick, onLegendDoubleclick, onClickAnnotation, onSunburstClick

Discrete interaction callbacks.

onRelayout

Escape hatch — receives the raw plotly_relayout payload (flat dot-notation keys) for fields outside the translation table.

container

Optional shiny.tag for the wrapper element.

Details

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.

Value

An irid_widget construct.


Embed a table output in a irid tag tree

Description

Shorthand for Output(renderTable, tableOutput, ...).

Usage

TableOutput(fn, ...)

Arguments

fn

A function that produces a table.

...

Additional arguments passed to shiny::tableOutput().

Value

A irid output node.


Conditionally render content

Description

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.

Usage

When(condition, yes, otherwise = NULL)

Arguments

condition

A reactive expression that returns a logical value.

yes

A 0-arg function returning the tag tree to render when the condition is TRUE.

otherwise

An optional 0-arg function returning the tag tree to render when the condition is FALSE. With NULL, nothing mounts on the false branch.

Details

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.

Value

A irid control-flow node.


Create a pair of HTML comment anchors bracketing a control-flow range

Description

Comment nodes are legal children of any element (including ⁠<select>⁠, ⁠<table>⁠, ⁠<tbody>⁠, etc.) so they serve as invisible range markers that the client can use to locate and mutate content without needing a wrapper element.

Usage

anchor_pair(id)

Arguments

id

The control-flow node ID.

Value

An htmltools::HTML() fragment containing the start/end markers.


Test whether a callable can accept a write value

Description

Returns TRUE for any callable that can accept a positional argument (reactiveVal, store leaf, reactiveProxy() with a setter, a primitive, or a closure with at least one formal). Returns FALSE for read-only callables (reactive(...), a ⁠\() expr⁠ closure, a reactiveProxy built with no set), and for non-callables.

Usage

can_accept_write(fn)

Arguments

fn

A value to test.

Details

Gate a write through this when you don't control whether the callable the caller handed you is writable — used by the DOM autobind path and the synthesized widget two-way-prop write-back to silently skip writes to a read-only reactive rather than error.

Value

A length-1 logical.


Create a irid application

Description

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.

Usage

iridApp(fn, ...)

Arguments

fn

A zero-argument function that returns a irid tag tree (e.g. a page_sidebar() call containing reactive attributes and event handlers).

...

Additional arguments passed to shiny::shinyApp().

Value

A Shiny app object.


Run a irid example application

Description

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.

Usage

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")
)

Arguments

example

Name of the example to run (e.g. "todo", "old-faithful") — the same name used in its website URL. If omitted, the available examples are listed.

port

The TCP port the application should listen on. Defaults to getOption("shiny.port"), or a random port if unset.

launch.browser

If TRUE, the system's default web browser is launched automatically. Defaults to TRUE in interactive sessions.

host

The IPv4 address the application should listen on. Defaults to getOption("shiny.host", "127.0.0.1").

display.mode

The display mode. Defaults to "showcase" — the annotated source shown alongside the running app; use "normal" to run the app by itself, or "auto" to defer to the app's own setting.

Details

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().

Value

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).

Examples

# List the available examples
iridExample()

if (interactive()) {
  iridExample("todo")
}

Create a irid UI output placeholder

Description

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.

Usage

iridOutput(id)

Arguments

id

The output ID, matching the corresponding renderIrid call.

Value

An HTML tag with the irid dependency.


irid JavaScript dependency

Description

Returns an htmltools::htmlDependency() for the client-side irid runtime (irid.js).

Usage

irid_dependency()

Value

An html_dependency object.


Create a local ID counter for use within a single process_tags call

Description

Create a local ID counter for use within a single process_tags call

Usage

irid_id_counter(prefix = "irid")

Value

A function that returns the next ID each time it is called.


Mount a pre-processed irid tag tree

Description

Takes the output of process_tags() and wires up Shiny observers for reactive attribute bindings, event listeners, Shiny outputs, and control-flow nodes (When, Each, Match).

Usage

irid_mount_processed(result, session, depth = 0L)

Arguments

result

A list returned by process_tags(), containing ⁠$tag⁠, ⁠$bindings⁠, ⁠$events⁠, ⁠$control_flows⁠, and ⁠$shiny_outputs⁠.

session

A Shiny session object.

depth

Nesting depth used to compute binding priority. Top-level mounts (iridApp, renderIrid) use the default 0; recursive calls from inside When/Each/Match increment it so nested bindings fire before their parent's.

Details

Binding observers run at priority = -100 + depth, so deeper-nested bindings fire before shallower ones in the same flush. Control flow observers stay at the default priority 0 and always fire first. This guarantees that on initial mount (and on any cascading re-render), content inserted by a control flow is fully populated by its inner bindings before any parent attribute binding observes it. The motivating case is ⁠<select value=rv>⁠ whose options come from Each — the parent's value binding must fire after the options exist and have their value attributes set, otherwise the browser silently sets selectedIndex = -1 and the select renders blank.

Value

A mount handle with ⁠$tag⁠ (the processed HTML) and ⁠$destroy()⁠ (a function that tears down all observers).


Test whether a value is a irid-reactive function

Description

Returns TRUE for any callable irid treats as reactive — plain functions, Shiny reactives, reactiveStore nodes, and reactiveProxy wrappers. Used by process_tags() to decide which attributes participate in auto-bind / event extraction.

Usage

is_irid_reactive(x)

Arguments

x

An object to test.

Value

Logical.


Mini-store projection over a record

Description

Builds a reactiveStore-shaped callable tree that projects a single record out of a parent collection. Reads route through get_record(); writes (whole-record or per-field synthetic setters) route through set_record(). The mini-store never owns the record's state – the parent collection is the single source of truth.

Usage

make_mini_store(get_record, set_record, scope)

Arguments

get_record

A 0-arg reactive callable that returns the current record (a fully named list).

set_record

A 1-arg function called with the new record on whole-record write or after a per-field synthetic setter has built the complete record.

scope

A scope from make_scope(). The propagating observer is created against scope$session and registered for cleanup so scope$destroy() tears it down.

Details

Used by Each (record items) and Match (record bound value) to project fine-grained leaf reactivity out of a coarse-grained parent.

Recursive – nested named lists in the initial record become sub-mini-stores (so mini$user$name(v) writes through the same chain as ⁠mini$user(<full-user>)⁠ would). Each level threads a sub-get/set pair down the tree; writes at any depth fan out through the chain of synthetic setters until they reach the user-supplied set_record. Shape uses the same shared branch-vs-leaf rules as reactiveStore() (is_branch, is_bare_list, strip_asis are reused), and the same recursive validate_write() enforces "no unknown keys, no missing keys" at every level on whole-record writes.

Shape is strict. Writes are validated against the keys captured at construction – same contract as a reactiveStore() branch. Callers that need to change a slot's shape reshape the parent collection directly (e.g. for Each(items, ...), write the new items() with the slot already replaced); the outer reconciler observes the parent change and rebuilds this mini-store with a fresh leaf tree of the new shape on the same flush.

Writes replace, like reactiveStore(). A branch write must include every locked key; missing keys are an error. The complete record is what reaches set_record. Per-field writes (mini$field(v)) remain the dedicated single-slot path – use them when you want to update one field without naming the rest. Dropping a field is a parent-level operation: write the reshaped collection through the source callable (items(...)) and the outer reconciler rebuilds.

Internally, every leaf holds a reactiveVal kept in sync with the parent by a single root-level propagating observer. The observer walks the tree top-down via each branch's internal set_internal, which recurses to children's set_internal; only at leaves does an actual reactiveVal write occur, gated by identical(old, new). This is what delivers the "only changed fields fire" promise – the diff happens inside the projection rather than at the call site, so callers can't get it wrong.

Value

A callable with class c("reactiveStore", "reactive", "function"), shaped like a reactiveStore branch – mini() reads the record, mini(record) writes it, mini$field() reads a leaf or sub-branch, and mini$field(v) writes through the parent.


Per-item / per-case reactive scope

Description

Wraps a Shiny session into a small "scope" object whose responsibility is to bound the lifetime of reactives and observers created inside per-item (Each) or per-case (Match) mounts. Every per-item / per-case mount creates one scope at construction; on unmount, scope$destroy() tears down everything created against it.

Usage

make_scope(session, id)

Arguments

session

A Shiny session, a child scope proxy, or NULL in tests.

id

Scope identifier for the shiny#4372 child scope (a non-empty string; the caller's element/wrapper counter token). Required — every call site already has a unique counter token. Unused on the fallback path, but always supplied so the scope's identity is explicit.

Details

Two implementations, chosen by runtime feature detection.

The seam is forward-compatible: every per-item / per-case reactive is constructed through with_scope, so the same call sites reclaim correctly once a user upgrades to a shiny carrying #4372 — no irid change required.

Teardown ordering. On unmount, callers MUST destroy the per-item / per-case mount handle before the scope. The mount's observers (auto-bind bindings, event handlers) read mini-store / slot-accessor leaves that live in the scope; under #4372 a destroyed leaf throws on access (it is actively destroyed, not lazily GC'd), so tearing the scope down first would make those observers error on their next read. The order is mount → scope at every site that owns both.

shiny#4372: https://github.com/rstudio/shiny/pull/4372 merged 2026-05-29, not yet on CRAN as of this writing. Verified against shiny 1.13.0.9000 (HEAD 44fd783); re-confirm method names / weak-ref semantics on bump. Every site that depends on this seam is tagged ⁠# shiny#4372:⁠ for grep-ability.

Value

A list with session, register_observer(obs), with_scope(expr), and destroy().


Scalar slot accessor for Each (positional and keyed)

Description

Builds the scalar-item analogue of make_mini_store() – a callable (reactiveProxy) over a single value held in the parent collection. Reads route through an internal reactiveVal that is kept in sync with get_value() by a propagating observer; writes route through set_value() to the parent. The internal reactiveVal is what gives fine-grained reactivity: when the parent list is patched somewhere else, an unchanged slot's observers don't fire.

Usage

make_slot_accessor(get_value, set_value, scope)

Arguments

get_value

0-arg callable returning the slot's current value.

set_value

1-arg function called with the new value on write.

scope

Scope from make_scope(); the propagating observer is registered here for cleanup.

Value

A reactiveProxy that reads from the internal leaf and writes through set_value.


Overlay one wire over another

Description

Override-wins overlay used where a widget wrapper layers a caller's input over its own default config. Each field of y (subject, timing, coalesce, dom_opts) wins when non-NULL; otherwise x's field carries through. y may be NULL (identity) or a bare callable (fills in only the subject), both normalized first.

Usage

## S3 method for class 'irid_wire'
merge(x, y, ...)

Arguments

x

The default wire.

y

The override: a wire, a bare callable, or NULL.

...

Unused.

Details

Extends the base merge() generic rather than introducing a new one.

Value

A wire.


Plotly html dependencies for PlotlyOutput

Description

The plotly.js bundle (sourced from the suggested {plotly} package) plus the irid-plotly factory script, carried by PlotlyOutput() as its widget deps and delivered at mount time via insertUI (see deliver_widget_deps in mount.R).

Usage

plotly_dependency()

Value

A list of html_dependency objects.


Walk a tag tree and extract reactive bindings

Description

Recursively walks an HTML tag tree, replacing reactive attributes and event handlers with plain IDs. Returns the cleaned tag along with lists of bindings, events, control-flow nodes, and Shiny outputs to be mounted by irid_mount_processed().

Usage

process_tags(tag, counter = irid_id_counter())

Arguments

tag

A Shiny tag, tag list, or irid control-flow node.

Value

A list with elements ⁠$tag⁠, ⁠$bindings⁠, ⁠$events⁠, ⁠$control_flows⁠, and ⁠$shiny_outputs⁠.


Wrap a reader and optional writer as a callable

Description

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.

Usage

reactiveProxy(get, set = NULL)

Arguments

get

A 0-arg callable returning the read value. Typically a reactiveVal, a reactiveStore leaf, another reactiveProxy, or a closure like ⁠\() transform(rv())⁠.

set

A 1-arg function called with the incoming value on write, or NULL for a read-only proxy. Defaults to NULL.

Details

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.

Value

A callable with class c("reactiveProxy", "reactive", "function").


Hierarchical reactive state container

Description

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().

Usage

reactiveStore(initial)

Arguments

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, NULL, unnamed/empty bare lists, classed lists, or any value wrapped in I()) becomes a leaf. Partially-named bare lists are an error.

Details

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.

Value

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.


Shiny reactive primitives

Description

These functions are re-exported from shiny for convenience, so you can use them in irid apps without loading shiny explicitly.

Details

See the shiny documentation for full details.

Value

The return values are those of the underlying shiny functions:


Render irid content inside a Shiny app

Description

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.

Usage

renderIrid(expr, env = parent.frame(), quoted = FALSE)

Arguments

expr

An expression that returns a irid tag tree.

env

The environment in which to evaluate expr.

quoted

If TRUE, expr is already a quoted expression.

Value

A shiny::renderUI() result.


Materialize a node's own htmltools render hooks / tag function

Description

bslib (and htmltools generally) defer structure to render time: a shiny.tag can carry .renderHooks that build its final DOM (layout_sidebar()'s grid wrapper, card()'s fill plumbing, ...), and a shiny.tag.function produces its tags only when called. process_tags() rebuilds each tag from name/attribs/children, so without running these first the deferred wrapper is silently dropped (see GH #27).

Usage

resolve_render_hooks(node)

Arguments

node

A node from the tag tree.

Details

This runs only the node's own hooks (one level), looping until the node is a resolved tag / non-tag. Unlike htmltools::as.tags() it does not recurse into children — process_tags' walker descends into them itself and resolves each child's hooks as it arrives. That separation is what lets irid's own children (reactive functions, irid_output, irid_widget, control-flow nodes) survive: they never carry render hooks, so they pass straight through, while a child hook that moved them into a new wrapper still gets walked.

Value

The node with its top-level render hooks / tag function resolved.


Serialize a plotly object to the JSON string PlotlyOutput ships

Description

Pre-encodes with plotly's own to_JSON (digits/encoding identical to what the {plotly} htmlwidget itself ships) and returns a plain JSON string. The substrate's encoder then ships the string verbatim; the JS side JSON.parses it. Works for both plot_ly() and ggplotly() — both build to the same structure.

Usage

to_plotly_spec(p, require_ids = FALSE, require_names = FALSE)

Arguments

p

A plotly or ggplotly object.

require_ids

When TRUE (the selected_ids binding is in use), validate that the built traces carry ids: error on absence, warn on duplicates. Identity-keyed selection resolves against ⁠data[*].ids⁠, so a missing key is a hard, fixable error and a non-unique key is advisory.

require_names

When TRUE (the trace_visibility binding is in use), validate the built traces carry names: error on absence, warn on duplicates. Visibility is keyed by trace name.

Value

A length-1 character vector of JSON.


Event & binding dispatch config

Description

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.

Usage

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)

Arguments

ms

Minimum interval (throttle) or quiet period (debounce) in milliseconds.

leading

If TRUE (default), fire immediately on the first event. If FALSE, wait for the timer before firing.

prevent_default

Call event.preventDefault() before dispatch.

stop_propagation

Call event.stopPropagation() before dispatch.

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 e; the event is dropped when it is falsy. NULL (the default) applies no filter.

subject

The handler or reactive the wire configures. The slot decides which: a bare callable means "bind" in ⁠value =⁠ / ⁠checked =⁠ and "handle" in an ⁠on*⁠ slot. NULL (the default) carries config only — used by widget wrappers that supply a default shape for the caller to override via merge().

timing

An irid_wire_timing shape (wire_immediate(), wire_throttle(), wire_debounce()), or NULL for the per-event default.

coalesce

Logical scalar, or NULL to derive from the timing mode.

dom_opts

A wire_dom_opts() record, or NULL.

Value

wire() returns an irid_wire; the timing constructors return an irid_wire_timing; wire_dom_opts() returns an irid_dom_opts.

Timing shapes

The timing constructors are pure shapes — they describe when an event fires and carry no other config:

When a wire carries no timing, irid applies a per-event default keyed on the DOM event name: inputwire_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.

Backpressure (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().

DOM listener options

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 object

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:

value

The element's current value (character).

valueAsNumber

Numeric 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.

checked

Logical, for checkbox and radio inputs.

Keyboard events additionally include key, code, ctrlKey, shiftKey, altKey, and metaKey.