0x013b
Introducing bound variables
With the new @bind
macro, Pluto.jl can listen to real-time events from HTML objects!
This syntax displays the HTML object as the cell's output, and uses its latest value as the definition of x
. Of course, the variable x
is reactive, and all references to x
come to life ✨
Try it out! 👆
missing
Main.workspace#3
UndefVarError: t1 not defined
Here is what happened, the most recent locations are first:
missing
Another cell defining t3 contains errors.
MethodError: no method matching Timer()
Closest candidates are:
Timer(::Real; interval) at /opt/hostedtoolcache/julia/1.7.3/x64/share/julia/base/asyncevent.jl:86
Timer(::Function, ::Real; interval) at /opt/hostedtoolcache/julia/1.7.3/x64/share/julia/base/asyncevent.jl:267
Timer(::Dates.Period; interval) at /opt/hostedtoolcache/julia/1.7.3/x64/share/julia/stdlib/v1.7/Dates/src/types.jl:474
Here is what happened, the most recent locations are first:
- macro expansionfrom This cell: line 125
- Show more...
Can only bind to html-showable objects, ie types T for which show(io, ::MIME"text/html", x::T) is defined.
Here is what happened, the most recent locations are first:
- error
(s::String) from error.jl:33 - PlutoRunner.Bond
(element::Main.workspace#3.Timer, defines::Symbol) from bonds.jl:82 - create_bond
(element::Main.workspace#3.Timer, defines::Symbol, cell_id::Base.UUID) from bonds.jl:88 - macro expansionfrom This cell: line 127
- Show more...
missing
Can only bind to html-showable objects, ie types T for which show(io, ::MIME"text/html", x::T) is defined.
Here is what happened, the most recent locations are first:
- error
(s::String) from error.jl:33 - PlutoRunner.Bond
(element::Main.workspace#3.Timer, defines::Symbol) from bonds.jl:82 - create_bond
(element::Main.workspace#3.Timer, defines::Symbol, cell_id::Base.UUID) from bonds.jl:88 - macro expansionfrom This cell: line 127
- Show more...
1.2
false
Combining bonds
The @bind
macro returns a Bond
object, which can be used inside Markdown and HTML literals:
How many pets do you have?
Dogs:
Cats:
You have missing dogs and missing cats!
Input types
You can use any DOM element that fires an input
event. For example:
a =
b =
c =
d =
e =
f =
missing
missing
missing
missing
missing
missing
You can also use JavaScript to write more complicated input objects. The input
event can be triggered on any object using
obj.dispatchEvent(new CustomEvent("input"))
Try drawing a rectangle in the canvas below 👇 and notice that the area
variable updates.
MethodError: no method matching getindex(::Missing, ::Int64)
Here is what happened, the most recent locations are first:
missing
Can I use it?
The @bind
macro is built into Pluto.jl — it works without having to install a package.
You can use the (tiny) package PlutoUI
for some predefined <input>
HTML codes. For example, you use PlutoUI
to write
@bind x Slider(5:15)
instead of
@bind x html"<input type='range' min='5' max'15'>"
The @bind
syntax in not limited to html"..."
objects, but can be used for any HTML-showable object!
More packages
In fact, any package can add bindable values to their objects. For example, a geoplotting package could add a JS input
event to their plot that contains the cursor coordinates when it is clicked. You can then use those coordinates inside Julia.
A package does not need to add Pluto.jl
as a dependency to so: only the Base.show(io, MIME("text/html"), obj)
function needs to be extended to contain a <script>
that triggers the input
event with a value. (It's up to the package creator when and what.) This does not affect how the object is displayed outside of Pluto.jl: uncaught events are ignored by your browser.
Tips
Wrap large code blocks
If you have a large block of code that depends on a bound variable t
, it will be faster to wrap that code inside a function f(my_t)
(which depends on my_t
instead of t
), and then call that function from another cell, with t
as parameter.
This way, only the Julia code "f(t)
" needs to be lowered and re-evaluated, instead of the entire code block.
Separate definition and reference
If you put a bond and a reference to the same variable together, it will keep evaluating in a loop.
So do not write
md"""$(@bind r html"<input type='range'>") $(r^2)"""
Instead, create two cells:
md"""$(@bind r html"<input type='range'>")"""
r^2
Behind the scenes
What is x?
It's an Int64
! Not an Observable, not a callback function, but simply the latest value of the input element.
The update mechanism is lossy and lazy, which means that it will skip values if your code is still running - and only send the latest value when your code is ready again. This is important when changing a slider from 0
to 100
, for example. If it would send all intermediate values, it might take a while for your code to process everything, causing a noticable lag.
What does the macro do?
The @bind
macro turns an expression like
@bind x Slider(5:15)
into
begin
local el = Slider(5:15)
global x = if applicable(Base.peek, el)
Base.peek(el)
else
missing
end
PlutoRunner.Bond(el, :x)
end
The if
block in the middle assigns an initial value to x
, which will be missing
, unless an extension of Base.peek
has been declared for the element.
Declaring a default value using Base.peek
is not necessary, as shown by the examples above, but the default value will be used for x
if the notebook.jl
file is run as a plain julia file, without Pluto's interactivity. The package PlutoUI
defines default values.
JavaScript?
Yes! We are using Generator.input
from observablehq/stdlib
to create a JS Generator (kind of like an Observable) that listens to onchange
, onclick
or oninput
events, depending on the element type.
This makes it super easy to create nice HTML/JS-based interaction elements - a package creator simply has to write a show
method for MIME type text/html
that creates a DOM object that triggers the input
event. In other words, Pluto's @bind
will behave exactly like viewof
in observablehq.
If you want to make a cool new UI, go to observablehq.com/@observablehq/introduction-to-views to learn how.
That's it for now! Let us know what you think using the feedback button below! 👇