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:
@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:
- Julia calls
show(io, MIME"text/html"(), widget)to render styled HTML - The notebook runtime attaches an event listener to the outermost element
- User interacts — the element dispatches an
inputevent - The runtime reads
element.valuefrom JavaScript - Julia calls
transform_value(widget, js_value)to convert back - 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.
initial_value(s::SliderWidget) = s.default # e.g., 50transform_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.
# 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) = valpossible_values(widget)
Returns all possible values (before transformation). Used for precomputing notebook states.
possible_values(s::SliderWidget) = 1:length(s.values) # indicesvalidate_value(widget, value_from_js)
Security validation for untrusted input on public deployments. Values are validated before transformation.
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:
- Julia stores a vector of values and assigns integer indices
- HTML uses indices as the input value
- JavaScript sends the index (integer) back to Julia
transform_valuemaps the index back to the Julia value
This enables binding to arbitrary Julia objects — even functions or structs:
# 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
.valueproperty 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:
# --- 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
endNotebook vs Therapy.jl Reactivity
The two systems use different reactivity models, but the widget looks the same:
| Sessions.jl | Therapy.jl | |
|---|---|---|
| Reactivity | Cell re-execution | Fine-grained signals |
| Binding | @bind x widget | create_signal() |
| State flow | JS → JSON → Julia → cells | JS → WebSocket → signal → DOM |
| Granularity | Entire cell | Individual 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