0x013bIntroducing 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! 👆
missingMain.workspace#3UndefVarError: t1 not defined
Here is what happened, the most recent locations are first:
missingAnother 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...
missingCan 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:
missingCan 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! 👇