The @bind Pattern

How the reactive binding protocol works, and how Suite.jl implements it.

How @bind Works

In Sessions.jl notebooks, @bind creates a two-way connection between a Julia variable and an HTML widget:

julia
@bind temperature Suite.Slider(0:100; default=20)

# `temperature` is now a reactive Julia variable.
# Moving the slider updates it, re-executing dependent cells.

The lifecycle:

  1. Julia calls show(io, MIME"text/html"(), widget) to render styled HTML
  2. The notebook runtime attaches an event listener to the outermost element
  3. User interacts — the element dispatches an input event
  4. The runtime reads element.value from JavaScript
  5. Julia calls transform_value(widget, js_value) to convert back
  6. The bound variable updates and dependent cells re-execute

The Four Protocol Methods

Suite.jl implements these bond protocol methods for each widget type:

initial_value(widget)

Returns the Julia value before the browser renders. Used for initial cell execution and running notebooks as scripts.

julia
initial_value(s::SliderWidget) = s.default  # e.g., 50

transform_value(widget, value_from_js)

Converts the raw JavaScript value into a Julia value. This is the key innovation — it enables binding arbitrary Julia objects via index mapping.

julia
# Slider: JS sends integer index → map to Julia value
transform_value(s::SliderWidget, idx) = s.values[idx]

# Checkbox: JS sends boolean directly
transform_value(c::CheckboxWidget, val) = val

possible_values(widget)

Returns all possible values (before transformation). Used for precomputing notebook states.

julia
possible_values(s::SliderWidget) = 1:length(s.values)  # indices

validate_value(widget, value_from_js)

Security validation for untrusted input on public deployments. Values are validated before transformation.

julia
validate_value(s::SliderWidget, val) = val isa Integer && 1 <= val <= length(s.values)

The Index-Mapping Pattern

The most important design pattern in the @bind protocol. Widgets that support arbitrary Julia values do not send those values to JavaScript. Instead:

  1. Julia stores a vector of values and assigns integer indices
  2. HTML uses indices as the input value
  3. JavaScript sends the index (integer) back to Julia
  4. transform_value maps the index back to the Julia value

This enables binding to arbitrary Julia objects — even functions or structs:

julia
# Bind to Julia functions!
@bind transform Suite.Select([
    sin => "Sine",
    cos => "Cosine",
    tan => "Tangent"
])

# `transform` is now a Julia function (sin, cos, or tan)
plot(transform, 0:0.1:2π)

The HTML Contract

For the notebook runtime to connect a widget, its rendered HTML must follow these rules:

  • The outermost element is what the runtime attaches the event listener to
  • The element must have a .value property readable from JavaScript
  • The element must dispatch CustomEvent("input") when the value changes
  • Native inputs (<input>, <select>) satisfy this automatically

For custom widgets (like Suite.jl's styled Switch), use Object.defineProperty to define a custom .value getter on the outer element.

Full Example: Suite.Slider

Here's how a complete dual-mode widget is implemented:

julia
# --- Widget struct (for @bind) ---
struct SliderWidget{T} <: AbstractSuiteWidget
    values::Vector{T}
    default::T
    show_value::Bool
end

# Positional arg → struct (widget mode, mirrors Sessions.jl)
function Slider(values::AbstractVector{T};
        default=missing, show_value::Bool=true,
        max_steps::Integer=1_000) where T
    vs = _downsample(collect(values), max_steps)
    d = default === missing ? first(vs) : _closest(vs, default)
    SliderWidget(vs, d, show_value)
end

# Keyword-only → VNode (island mode for Therapy.jl)
@island function Slider(; min=0, max=100, step=1, ...)
    # ... Wasm-compiled interactive slider
end

# --- Bond protocol ---
initial_value(s::SliderWidget) = s.default
possible_values(s::SliderWidget) = 1:length(s.values)
transform_value(s::SliderWidget, idx) = s.values[idx]
validate_value(s::SliderWidget, val) =
    val isa Integer && 1 <= val <= length(s.values)

# --- HTML rendering (for @bind in notebooks) ---
function Base.show(io::IO, ::MIME"text/html", s::SliderWidget)
    idx = _slider_index(s, s.default)
    # Renders styled <input type="range"> with index mapping
    # + inline <script> for .value property + input event
end

Notebook vs Therapy.jl Reactivity

The two systems use different reactivity models, but the widget looks the same:

Sessions.jlTherapy.jl
ReactivityCell re-executionFine-grained signals
Binding@bind x widgetcreate_signal()
State flowJS → JSON → Julia → cellsJS → WebSocket → signal → DOM
GranularityEntire cellIndividual DOM nodes

Next Steps

  • Widget Overview — Full mapping table and dual-mode architecture
  • Switch — An interactive component available as a widget today
  • Toggle — Toggle component with pressed/unpressed state