To be able to edit code and run cells, you need to run the notebook yourself. Where would you like to run the notebook?

This notebook takes about 20 seconds to run.

In the cloud (experimental)

Binder is a free, open source service that runs scientific notebooks in the cloud! It will take a while, usually 2-7 minutes to get a session.

On your computer

(Recommended if you want to store your changes.)

  1. Copy the notebook URL:
  2. Run Pluto

    (Also see: How to install Julia and Pluto)

  3. Paste URL in the Open box

Frontmatter

If you are publishing this notebook on the web, you can set the parameters below to provide HTML metadata. This is useful for search engines and social media.

Author 1
0x013b
👀 Reading hidden code
0b0100111011
7.6 μs

Introducing bound variables

With the new @bind macro, Pluto.jl can listen to real-time events from HTML objects!

👀 Reading hidden code
2.1 ms
👀 Reading hidden code
@bind x html"<input type='range'>"
218 ms

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! 👆

👀 Reading hidden code
332 μs
missing
👀 Reading hidden code
9.3 μs
Main.workspace#3
👀 Reading hidden code
@__MODULE__
15.6 μs
👀 Reading hidden code
sleep(1)
1.0 s
👀 Reading hidden code
ts = []
11.8 μs
Error message

UndefVarError: t1 not defined

Stack trace

Here is what happened, the most recent locations are first:

  1. push!(ts, t1); length(ts)
push!(ts, t1); length(ts)
👀 Reading hidden code
---
missing
t2
👀 Reading hidden code
10.5 μs
using PlutoUI
👀 Reading hidden code
179 ms
Error message

Another cell defining t3 contains errors.

computer bad, you GREAT!
t3
👀 Reading hidden code
---
Error message

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

Stack trace

Here is what happened, the most recent locations are first:

  1. macro expansion
  2. Show more...
Maybe time for a break? ☕️
@bind t3 PlutoUI.Timer()
👀 Reading hidden code
---
Error message

Can only bind to html-showable objects, ie types T for which show(io, ::MIME"text/html", x::T) is defined.

Stack trace

Here is what happened, the most recent locations are first:

  1. error(s::String)
  2. PlutoRunner.Bond(element::Main.workspace#3.Timer, defines::Symbol)
  3. create_bond(element::Main.workspace#3.Timer, defines::Symbol, cell_id::Base.UUID)
  4. macro expansion
  5. Show more...
@bind t2 Timer(0.5, true)
👀 Reading hidden code
---
missing
t
👀 Reading hidden code
8.2 μs
Error message

Can only bind to html-showable objects, ie types T for which show(io, ::MIME"text/html", x::T) is defined.

Stack trace

Here is what happened, the most recent locations are first:

  1. error(s::String)
  2. PlutoRunner.Bond(element::Main.workspace#3.Timer, defines::Symbol)
  3. create_bond(element::Main.workspace#3.Timer, defines::Symbol, cell_id::Base.UUID)
  4. macro expansion
  5. Show more...
¯\_(ツ)_/¯
@bind t Timer()
👀 Reading hidden code
---
struct Timer
interval::Real
fixed::Bool
Timer(interval=1.0, fixed=false) = interval >= 0 ? new(interval, fixed) : error("interval must be non-negative")
end
👀 Reading hidden code
1.5 ms
Timer(1.2)
👀 Reading hidden code
9.8 μs
Base.show(io::IO, ::MIME"text/html", timer::Timer) = begin
tb = read("/mnt/c/dev/julia/timer_back.svg", String)
tf = read("/mnt/c/dev/julia/timer_front.svg", String)
tz = read("/mnt/c/dev/julia/timer_zoof.svg", String)
write(io, """
<timer$(timer.fixed ? " class='fixed'" : "")>
<clock>
<back>$(tb)</back>
<front>$(tf)</front>
<zoof style="opacity: 0">$(tz)</zoof>
</clock>
<button></button>
<span>speed: </span>
<input type="number" value="$(timer.interval)" min=0 step=any lang="en-001">
<span id="unit" title="Click to invert"></span>
</timer>
<script>
const timer = this.querySelector("timer")
const tpsInput = timer.querySelector("input")
const clockfront = timer.querySelector("clock front")
const clockzoof = timer.querySelector("clock zoof")
const unit = timer.querySelector("span#unit")
const button = timer.querySelector("button")
var t = 1
tpsInput.oninput = (e) => {
var dt = tpsInput.valueAsNumber
if(timer.classList.contains("inverted")){
dt = 1.0 / dt
}
dt = (dt == Infinity || dt == 0) ? 1e9 : dt
clockzoof.style.opacity = 0.8 - Math.pow(dt,.2)
clockfront.style.animationDuration = dt + "s"
e && e.stopPropagation()
}
tpsInput.oninput()
clockfront.onanimationiteration = (e) => {
t++
timer.value = t
timer.dispatchEvent(new CustomEvent("input"))
}
unit.onclick = (e) => {
timer.classList.toggle("inverted")
//tpsInput.value = 1.0 / tpsInput.valueAsNumber
tpsInput.oninput()
}
button.onclick = (e) => {
timer.classList.toggle("stopped")
if(!timer.classList.contains("stopped")) {
t = 1 - 1
}
}
</script>
<style>
timer {
display: flex;
flex-direction: row;
}
timer > * {
align-self: center;
margin-right: .3rem;
}
clock {
display: block;
position: relative;
overflow: hidden;
width: 20px;
height: 20px;
}
clock > * {
display: block;
width: 100%;
height: 100%;
}
clock > * > svg {
width: 100%;
height: 100%;
}
timer clock front, timer clock zoof {
position: absolute;
left: 0;
top: 0;
animation: 1s linear 🔁 infinite;
}
timer.stopped clock front {
animation-play-state: paused;
}
timer.stopped clock zoof {
display: none;
}
timer input {
width: 3rem;
}
timer span {
font-family: "Roboto Mono", monospace;
font-size: .75rem;
word-spacing: -.2rem;
}
timer span#unit {
font-style: italic;
cursor: pointer;
}
timer span#unit::after {
content: "secs / tick";
}
timer.inverted span#unit::after {
content: "ticks / sec";
}
timer button {
margin-left: 1rem;
margin-right: 1rem;
}
timer button::after {
content: "Stop";
}
timer.stopped button::after {
content: "Start";
}
timer.fixed span,
timer.fixed input {
display: none;
}
@keyframes 🔁 {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
""")
end
👀 Reading hidden code
3.3 ms

👀 Reading hidden code
82.4 μs
👀 Reading hidden code
123 μs

Combining bonds

The @bind macro returns a Bond object, which can be used inside Markdown and HTML literals:

👀 Reading hidden code
231 μs

How many pets do you have?

Dogs:

Cats:

begin
🐶slider = @bind 🐶 html"<input type='range'>"
🐱slider = @bind 🐱 html"<input type='range'>"
md"""**How many pets do you have?**
Dogs: $(🐶slider)

Cats: $(🐱slider)"""
end
👀 Reading hidden code
2.6 ms

You have missing dogs and missing cats!

md"You have $(🐶) dogs and $(🐱) cats!"
👀 Reading hidden code
25.5 ms
👀 Reading hidden code
115 μs

Input types

You can use any DOM element that fires an input event. For example:

👀 Reading hidden code
278 μs

a =

b =

c =

d =

e =

f =

👀 Reading hidden code
15.6 ms
(a,b,c,d,e,f)
👀 Reading hidden code
13.6 μs

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.

👀 Reading hidden code
283 μs
👀 Reading hidden code
629 μs
Error message

MethodError: no method matching getindex(::Missing, ::Int64)

Stack trace

Here is what happened, the most recent locations are first:

  1. area = abs(dims[1] * dims[2])
area = abs(dims[1] * dims[2])
👀 Reading hidden code
---
missing
dims
👀 Reading hidden code
9.6 μs
👀 Reading hidden code
127 μs

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!

👀 Reading hidden code
579 μs

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.

👀 Reading hidden code
568 μs

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.

👀 Reading hidden code
383 μs

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
👀 Reading hidden code
340 μs
👀 Reading hidden code
118 μs

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.

👀 Reading hidden code
548 μs

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.

👀 Reading hidden code
444 μs

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.

👀 Reading hidden code
614 μs
👀 Reading hidden code
141 μs
👀 Reading hidden code
115 μs

That's it for now! Let us know what you think using the feedback button below! 👇

👀 Reading hidden code
190 μs