Interactions
Animations in Bonito are done via Observables.jl, much like it's the case for Makie.jl, so the same docs apply:
But lets quickly get started with a Bonito specific example:
App() do session
s = Slider(1:3)
value = map(s.value) do x
return x ^ 2
end
# record_states captures widget interactions for static HTML export
# This allows the slider to remain interactive even without a Julia backend!
return Bonito.record_states(session, DOM.div(s, value))
end
The s.value
is an Observable
which can be mapp'ed
to take on new values, and one can insert observables as an input to DOM.tag
or as any attribute. The value of the observable
will be rendered via jssrender(session, observable[])
, and then updated whenever the value changes. So anything that supports being inserted into the DOM
can be inside an observable, and the fallback is to use the display system (so plots etc. work as well). This way, one can also return DOM
elements as the result of an observable:
App() do session
s = Slider(1:3)
# use map!(result_observable, ...)
# To use any as the result type, otherwise you can't return
# different types from the map callback
value = map!(Observable{Any}(), s.value) do x
if x == 1
return DOM.h1("hello from slider: $(x)")
elseif x == 2
return DOM.img(src="https://docs.makie.org/stable/logo.svg", width="200px")
else
return x^2
end
end
return Bonito.record_states(session, DOM.div(s, value))
end
hello from slider: 1
In other words, the whole app can just be one big observable:
import Bonito.TailwindDashboard as D
App() do session
s = D.Slider("Slider: ", 1:3)
checkbox = D.Checkbox("Chose:", true)
menu = D.Dropdown("Menu: ", [sin, tan, cos])
dependent = map(checkbox.widget.value, s.widget.value, menu.widget.value) do args...
D.FlexCol(DOM.div.(args)...)
end
return Bonito.record_states(session, D.FlexRow(
D.Card(D.FlexCol(checkbox, s, menu)),
D.Card(D.FlexCol(D.Title("Independent (supported)"), checkbox.value, s.value, menu.value)),
D.Card(D.FlexCol(D.Title("Dependent (not supported)"), dependent))
))
end
Chose:
Slider:
1
Menu:
Independent (supported)
true 1sin (generic function with 14 methods)
Dependent (not supported)
sin (generic function with 14 methods)
Understanding record_states
Limitations
The record_states
function is designed to capture widget interactions for static HTML export. However, it has an important limitation: only independent widget states are recorded.
In the example above:
- Independent widgets work: The slider, checkbox, and dropdown values update correctly because they're displayed directly
- Dependent observables don't work: The
dependent
observable that combines multiple widget values won't update correctly in the exported HTML
This happens because record_states
records each widget's states independently to avoid exponential growth in file size. For complex interactions, consider using JavaScript-based solutions (shown later in this guide).
Creating Interactive Examples
Despite these limitations, you can still create engaging interactive examples:
import Bonito.TailwindDashboard as D
function create_svg(sl_nsamples, sl_sample_step, sl_phase, sl_radii, color)
width, height = 900, 300
cxs_unscaled = [i*sl_sample_step + sl_phase for i in 1:sl_nsamples]
cys = sin.(cxs_unscaled) .* height/3 .+ height/2
cxs = cxs_unscaled .* width/4pi
rr = sl_radii
# DOM.div/svg/etc is just a convenience in Bonito for using Hyperscript, but circle isn't wrapped like that yet
geom = [SVG.circle(cx=cxs[i], cy=cys[i], r=rr, fill=color(i)) for i in 1:sl_nsamples[]]
return SVG.svg(SVG.g(geom...);
width=width, height=height
)
end
app = App() do session
colors = ["black", "gray", "silver", "maroon", "red", "olive", "yellow", "green", "lime", "teal", "aqua", "navy", "blue", "purple", "fuchsia"]
color(i) = colors[i%length(colors)+1]
sl_nsamples = D.Slider("nsamples", 1:200, value=100)
sl_sample_step = D.Slider("sample step", 0.01:0.01:1.0, value=0.1)
sl_phase = D.Slider("phase", 0.0:0.1:6.0, value=0.0)
sl_radii = D.Slider("radii", 0.1:0.1:60, value=10.0)
svg = map(create_svg, sl_nsamples.value, sl_sample_step.value, sl_phase.value, sl_radii.value, color)
return DOM.div(D.FlexRow(D.FlexCol(sl_nsamples, sl_sample_step, sl_phase, sl_radii), svg))
end
nsamples
100
sample step
0.1
phase
0.0
radii
10.0
As you notice, when exporting this example to the docs which get statically hosted, all interactions requiring Julia cease to exist. One way to create interactive examples that stay active is to move the parts that need Julia to Javascript:
app = App() do session
colors = ["black", "gray", "silver", "maroon", "red", "olive", "yellow", "green", "lime", "teal", "aqua", "navy", "blue", "purple", "fuchsia"]
nsamples = D.Slider("nsamples", 1:200, value=100)
nsamples.widget[] = 100
sample_step = D.Slider("sample step", 0.01:0.01:1.0, value=0.1)
sample_step.widget[] = 0.1
phase = D.Slider("phase", 0.0:0.1:6.0, value=0.0)
radii = D.Slider("radii", 0.1:0.1:60, value=10.0)
radii.widget[] = 10
svg = DOM.div()
evaljs(session, js"""
const [width, height] = [900, 300]
const colors = $(colors)
const observables = $([nsamples.value, sample_step.value, phase.value, radii.value])
function update_svg(args) {
const [nsamples, sample_step, phase, radii] = args;
const svg = (tag, attr) => {
const el = document.createElementNS('http://www.w3.org/2000/svg', tag);
for (const key in attr) {
el.setAttributeNS(null, key, attr[key]);
}
return el
}
const color = (i) => colors[i % colors.length]
const svg_node = svg('svg', {width: width, height: height});
for (let i=0; i<nsamples; i++) {
const cxs_unscaled = (i + 1) * sample_step + phase;
const cys = Math.sin(cxs_unscaled) * (height / 3.0) + (height / 2.0);
const cxs = cxs_unscaled * width / (4 * Math.PI);
const circle = svg('circle', {cx: cxs, cy: cys, r: radii, fill: color(i)});
svg_node.appendChild(circle);
}
$(svg).replaceChildren(svg_node);
}
Bonito.onany(observables, update_svg)
update_svg(observables.map(x=> x.value))
""")
return DOM.div(D.FlexRow(D.FlexCol(nsamples, sample_step, phase, radii), svg))
end
nsamples
100
sample step
0.1
phase
0.0
radii
10.0
This works, because the Javascript side of Bonito, will still update the observables in Javascript (which are mirrored from Julia), and therefore keep working without a running Julia process. You can use js_observable.on(value=> ....)
and Bonito.onany(array_of_js_observables, values=> ...)
to create interactions, pretty similar to how you would work with Observables in Julia.