Using JavaScript inside Pluto
You have already seen that Pluto is designed to be interactive. You can make fantastic explorable documents using just the basic inputs provided by PlutoUI, together with the diff
Essentials
Custom @bind
output
You can use JavaScript to write input widgets. The input
event can be triggered on any object using
obj.value = ...
obj.dispatchEvent(new CustomEvent("input"))
For example, here is a button widget that will send the number of times it has been clicked as the value:
For better readability, you can view the script from the cell below with syntax highlighting:
ClickCounter (generic function with 2 methods)
missing
As an exercise to get familiar with these techniques, you can try the following:
👉 Add a "reset to zero" button to the widget above.
👉 Make the bound value an array that increases size when you click, instead of a single number.
👉 Create a "two sliders" widget: combine two sliders (
<input type=range>
) into a single widget, where the bound value is the two-element array with both values.👉 Create a "click to send" widget: combine a text input and a button, and only send the contents of the text field when the button is clicked, not on every keystroke.
Questions? Ask them on our GitHub Discussions!
Debugging
The HTML, CSS and JavaScript that you write run in the browser, so you should use the browser's built-in developer tools to debug your code.
Selecting elements
When writing the javascript code for a widget, it is common to select elements inside the widgets to manipulate them. In the number-of-clicks example above, we selected the <div>
and <button>
elements in our code, to trigger the input event, and attach event listeners, respectively.
There are a numbers of ways to do this, and the recommended strategy is to create a wrapper <div>
, and use currentScript.parentElement
to select it.
currentScript
When Pluto runs the code inside <script>
tags, it assigns a reference to that script element to a variable called currentScript
. You can then use properties like previousElementSibling
or parentElement
to "navigate to other elements".
Let's look at the "wrapper div strategy" again.
@htl("""
<!-- the wrapper div -->
<div>
<button id="first">Hello</button>
<button id="second">Julians!</button>
<script>
var wrapper_div = currentScript.parentElement
// we can now use querySelector to select anything we want
var first_button = wrapper_dir.querySelector("button#first")
console.log(first_button)
</script>
</div>
""")
Why not just select on document.body
?
In the example above, it would have been easier to just select the button directly, using:
// ⛔ do no use:
var first_button = document.body.querySelector("button#first")
However, this becomes a problem when combining using the widget multiple times in the same notebook, since all selectors will point to the first instance.
Similarly, try not to search relative to the <pluto-cell>
or <pluto-output>
element, because users might want to combine multiple instances of the widget in a single cell.
Interpolation
Julia has a nice feature: string interpolation:
"🌍"
"Hello 🌍!"
With some (frustrating) exceptions, you can also interpolate into Markdown literals:
Hello 🌍!
However, you cannot interpolate into an html"
string:
Hello $(who)!
😢 For this feature, we highly recommend the new package HypertextLiteral.jl, which has an @htl
macro that supports interpolation:
Interpolating into HTML – HypertextLiteral.jl
Hello 🌍!
It has a bunch of very cool features! Including:
Interpolate any HTML-showable object, such as plots and images, or another
@htl
literal.Interpolated lists are expanded (like in this cell!).
- Easy syntax for CSS
Interpolate any HTML-showable object, such as plots and images, or another @htl
literal.
Interpolated lists are expanded (like in this cell!).
"Easy syntax for CSS"
Why not just HTML(...)
?
You might be thinking, why don't we just use the HTML
function, together with string interpolation? The main problem is correctly handling HTML escaping rules. For example:
"euros£s"
We accept euros£s!
We accept euros£s!
Interpolating into JS – JSON.jl
Using HypertextLiteral.jl, we can interpolate objects into HTML output, great! Next, we want to interpolate data into scripts. The easiest way to do so is to use JSON.jl
, in combination with string interpolation:
"Cool"
100
100
"Awesome"
200
100
"Fantastic!"
0
150
Script loading
To use external javascript dependencies, you can load them from a CDN, such as:
Just like when writing a browser app, there are two ways to import JS dependencies: a <script>
tag, and the more modern ES6 import.
Loading method 1: ES6 imports
we recommend that you use an ES6 import if the library supports it.
Awkward note about syntax
Normally, you can import libraries inside JS using the import syntax:
import confetti from 'https://cdn.skypack.dev/canvas-confetti'
import { html, render, useEffect } from "https://cdn.jsdelivr.net/npm/htm@3.0.4/preact/standalone.mjs"
In Pluto, this is currently not yet supported, and you need to use a different syntax as workaround:
const { default: confetti } = await import("https://cdn.skypack.dev/canvas-confetti@1")
const { html, render, useEffect } = await import( "https://cdn.jsdelivr.net/npm/htm@3.0.4/preact/standalone.mjs")
Loading method 2: script tag
<script src="...">
tags with a src
attribute set, like this tag to import the d3.js library:
<script src="https://cdn.jsdelivr.net/npm/d3@6.2.0/dist/d3.min.js"></script>
will work as expected. The execution of other script tags within the same cell is delayed until a src
script finished loading, and Pluto will make sure that every source file is only loaded once.
Advanced
Script output
cool_thing (generic function with 1 method)
Example:
Stateful output with this
"edit me!"
I am running for the first time!
Example: d3.js transitions
Type the coordinates of the circles here!
100
300
Notice that, even though the cell below re-runs, we smoothly transition between states. We use this
to maintain the d3 transition states inbetween reactive runs.
script (generic function with 1 method)