Interactivity with HTML
Pluto notebooks can use the @bind macro for interactive inputs. Neat!
If you're completely new to coding anything interactive in a web browser, the easiest way to get started is with the PlutoUI package in Julia. (Check out the PlutoUI featured notebook if you'd like to learn more about that!)
If you have some experience with HTML, or if you would like to learn how to make your own interactive elements, this notebook is for you! We'll cover the basics of using @bind with HTML elements. Let's get started!
Bound variables
A basic interactive cell looks like this: we use the @bind macro, define a Julia variable, and add a bit of HTML code that creates an <input> element. The HTML element will be rendered as the cell's output.
@bind x html"<input type=range>"That's all you need! Pluto will synchronise the value of x with the latest value of the HTML input. Try moving the slider! π
xA neat aspect of this @bind macro is that on the Julia side, x is just a normal integer:
typeof(x)Not an observable, callback, or something else that you need to wrap your head around. You can use x like you would any other number, and Pluto's reactivity means anything depending on x will be updated with it. π
y = x^2Input types
You can use binds with anything that fires an input event. For example:
@bind a html"<input type=range >"@bind b html"<input type=text >"@bind c html"<input type=checkbox >"@bind d html"""
<select>
<option value='one'>First</option>
<option value='two'>Second</option>
</select>
"""@bind e html"<input type=color >"@bind f html"<input type=date>"(a, b, c, d, e, f)Scripting inputs with Javascript
You can also use Javascript to write more complicated inputs.
To get started with this, add a <script> tag to your cell. The easiest setup is to make the script a child of the cell's root element. Then you can use currentScript.parentElement to select the script's parent element.
For instance, here a HTML snippet with a script added to it.
html"""
<div>
Let's make something interactive here!
<script>
const div = currentScript.parentElement;
</script>
</div>
"""Now we can use the script to handle whatever interaction we want. For a simple element, let's add a button and count the number of clicks.
html"""
<div>
<button>Click me!</button>
<script>
const div = currentScript.parentElement;
const button = div.querySelector("button");
var clicks = 0;
button.addEventListener("click", e => {
clicks += 1;
});
</script>
</div>
"""The script is now keeping track of clicks, but it's not sending that value anywhere.
We need to:
Update the parent's element
valueproperty (this is what Pluto will be reading).Dispatch an
"input"event to trigger change detection.
html"""
<div>
<button>Click me!</button>
<script>
const div = currentScript.parentElement;
const button = div.querySelector("button");
var clicks = 0;
function update() {
div.value = clicks;
div.dispatchEvent(new CustomEvent("input"));
}
button.addEventListener("click", e => {
clicks += 1;
update();
});
// fire an event with the initial value immediately
update();
</script>
</div>
"""Okay, now we can use this HTML snippet with a @bind macro and see the result!
@bind counter html"""
<div>
<button>Click me!</button>
<script>
const div = currentScript.parentElement;
const button = div.querySelector("button");
var clicks = 0;
function update() {
div.value = clicks;
div.dispatchEvent(new CustomEvent("input"));
}
button.addEventListener("click", e => {
clicks += 1;
update();
});
// fire an event with the initial value immediately
update();
</script>
</div>
"""counterTip
In this example, we immediately call update() and trigger an input event with the initial value. If we didn't do that, the value of counter would be nothing until the first interaction. Whether that's appropriate, depends on the interaction you want. In this case, it made sense that the counter should start at 0!
Using Julia values in HTML
We've seen how you can pass on values from an interactive HTML element on to Julia, but for proper interaction, we also want to do it the other way around!
The most basic way to do this is string interpolation. In this example, you can change the value of limit in the code to affect the maximum value of the slider. Try it out!
limit = 5@bind limited HTML("""
<input type="range" min="0" max="$(limit)">
""")limitedKeep in mind that changing the value of limit will re-evaluate the slider cell, so it will reset the value.
For something even fancier, you can also interpolate value in Javascript. For example, here I have adapted my counter so it won't count past limit:
@bind limited_counter HTML("""
<div>
<button>Click me!</button>
<script>
const div = currentScript.parentElement;
const button = div.querySelector("button");
const max = $limit;
var clicks = 0;
function update() {
div.value = clicks;
div.dispatchEvent(new CustomEvent("input"));
}
button.addEventListener("click", e => {
if (clicks < max) {
clicks += 1;
update();
}
});
// fire an event with the initial value immediately
update();
</script>
</div>
""")limited_counterTip
Interpolating values in Javascript is neat, but remember that we are just taking the string representation of the variable and pasting it in the javascript code. For some complex data types like dates, you will need to do a bit of conversion to turn them into proper javascript expressions.
If you're looking for something more sophisticated than simple string interpolation, check out the HyperTextLiteral package!
Reusing elements
Once you've written some neat interactive element, you may want to use it a few times in notebook. The easiest way is to put the HTML literal in a variable.
myslider = html"""<input type="range" min=50 max=100 step=5 style="width: 100%">""";@bind x1 myslider@bind x2 mysliderx1, x2It's as simple as that!
Tip
To avoid confusing your readers, it's a good idea to put a ; at the end of the cell where you define the interactive element, like we did here. This way, we only show sliders that are actually bound to something.
This is is how the PlutoUI package works - it defines HTML literals - sometimes with their own scripts.
If you've written some cool HTML inputs and you want to share them with others, you can also make a UI package! Just publish those definitions as a Julia package and you're done! β¨
Behind the scenes
If you're curious to learn a bit more about how this all works, keep reading!
The output value
As we've mentioned, bound values are just the latest value of the input element, rather than some kind of observable.
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 noticeable lag.
What does the macro do?
The @bind macro does not actually contain the interactivity mechanism, this is built into Pluto itself. Still, it does two things: it assigns a default value to the variable (missing in most cases), and it wraps the second argument in a PlutoRunner.Bond object.
For example, expanding the @bind macro turns this expression:
@bind x Slider(5:15)
into (simplified):
begin
local el = Slider(5:15)
global x = AbstractPlutoDingetjes.intial_value(el)
PlutoRunner.create_bond(el, :x)
end
We see that the macro creates a variable x, which is given the value AbstractPlutoDingetjes.intial_value(el). This function returns missing by default, unless a method was implemented for your widget type. For example, PlutoUI has a Slider type, and it defines a method for intial_value(slider::Slider) that returns the default number.
Declaring a default value using AbstractPlutoDingetjes is not necessary, as shown by the earlier examples in this notebook, 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.
You don't need to worry about this if you are just getting started with Pluto and interactive elements, but more advanced users should take a look at AbstractPlutoDingetjes.jl.
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 learn more about using Javascript in Pluto, check out the JavaScript sample notebook!