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

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

👀 Reading hidden code
320 μs
👀 Reading hidden code
using PlutoUI
169 ms
👀 Reading hidden code
10.1 μs

Essentials

Custom @bind output

👀 Reading hidden code
219 μs

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:

👀 Reading hidden code
237 ms
👀 Reading hidden code
288 μs
ClickCounter (generic function with 2 methods)
ClickCounter(text="Click") = @htl("""
<div>
<button>$(text)</button>

<script>

// Select elements relative to `currentScript`
var div = currentScript.parentElement
var button = div.querySelector("button")

// we wrapped the button in a `div` to hide its default behaviour from Pluto

var count = 0

button.addEventListener("click", (e) => {
count += 1
// we dispatch the input event on the div, not the button, because
// Pluto's `@bind` mechanism listens for events on the **first element** in the
// HTML output. In our case, that's the div.
div.value = count
div.dispatchEvent(new CustomEvent("input"))
e.stopPropagation()
})

// Set the initial value
div.value = count

</script>
</div>
""")
👀 Reading hidden code
705 μs
@bind num_clicks ClickCounter()
👀 Reading hidden code
38.6 ms
missing
num_clicks
👀 Reading hidden code
8.5 μs

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!

👀 Reading hidden code
567 μs

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.

👀 Reading hidden code
285 μs
html"""

<script>

console.info("Can you find this message in the console?")

</script>

"""
👀 Reading hidden code
980 μs
Can you find out which CSS class this is?
👀 Reading hidden code
227 ms

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>
""")
👀 Reading hidden code
556 μs

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.

👀 Reading hidden code
408 μs

Interpolation

Julia has a nice feature: string interpolation:

👀 Reading hidden code
256 μs
"🌍"
who = "🌍"
👀 Reading hidden code
11.5 μs
"Hello 🌍!"
"Hello $(who)!"
👀 Reading hidden code
13.4 μs

With some (frustrating) exceptions, you can also interpolate into Markdown literals:

👀 Reading hidden code
186 μs

Hello 🌍!

md"""
Hello $(who)!
"""
👀 Reading hidden code
26.8 ms

However, you cannot interpolate into an html" string:

👀 Reading hidden code
225 μs

Hello $(who)!

html"""
<p>Hello $(who)!</p>
"""
👀 Reading hidden code
114 μs

😢 For this feature, we highly recommend the new package HypertextLiteral.jl, which has an @htl macro that supports interpolation:

Interpolating into HTML – HypertextLiteral.jl

👀 Reading hidden code
301 μs

Hello 🌍!

@htl("""
<p> Hello $(who)!</p>
""")
👀 Reading hidden code
474 ms

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
@htl("""
<p>It has a bunch of very cool features! Including:</p>
<ul>$([
@htl(
"<li>$(item)</li>"
)
for item in cool_features
])</ul>
""")
👀 Reading hidden code
11.4 ms
👀 Reading hidden code
100 ms

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:

👀 Reading hidden code
280 μs
"euros&pounds"
currencies = "euros&pounds"
👀 Reading hidden code
10.8 μs
HTML("""
<h5> We accept $(currencies)!</h5>
"""),

@htl("""
<h5> We accept $(currencies)!</h5>
""")
👀 Reading hidden code
83.2 μ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:

👀 Reading hidden code
300 μs
my_data = [
(name="Cool", coordinate=[100, 100]),
(name="Awesome", coordinate=[200, 100]),
(name="Fantastic!", coordinate=[fantastic_x, 150]),
]
👀 Reading hidden code
31.4 μs
@bind fantastic_x Slider(0:400)
👀 Reading hidden code
168 ms
@htl("""
<script src="https://cdn.jsdelivr.net/npm/d3@6.2.0/dist/d3.min.js"></script>

<script>

// interpolate the data 🐸
const data = $(JSON.json(my_data))

const svg = DOM.svg(600,200)
const s = d3.select(svg)

s.selectAll("text")
.data(data)
.join("text")
.attr("x", d => d.coordinate[0])
.attr("y", d => d.coordinate[1])
.text(d => d.name)

return svg
</script>
""")
👀 Reading hidden code
309 ms

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")
👀 Reading hidden code
830 μs

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.

👀 Reading hidden code
323 μs

Advanced

👀 Reading hidden code
175 μs

Script output

md"""
## Script output
"""
👀 Reading hidden code
196 μs
cool_thing (generic function with 1 method)
cool_thing() = html"""



"""
👀 Reading hidden code
356 μs

Example:

md"""
### Example:
"""
👀 Reading hidden code
170 μs

Stateful output with this

md"""
## Stateful output with `this`

"""
👀 Reading hidden code
174 μs
"edit me!"
trigger = "edit me!"
👀 Reading hidden code
11.4 μs
I am running for the first time!
let
trigger
html"""
<script id="something">

if(this == null) {
return html`<blockquote>I am running for the first time!</blockqoute>`
} else {
return html`<blockquote><b>I was triggered by reactivity!</b></blockqoute>`
}


</script>
"""
end
👀 Reading hidden code
29.0 μs

Example: d3.js transitions

Type the coordinates of the circles here!

👀 Reading hidden code
223 μs
@bind positions TextField(default="100, 300")
👀 Reading hidden code
44.0 ms
dot_positions = try
parse.([Int], split(replace(positions, ',' => ' ')))
catch e
[100, 300]
end
👀 Reading hidden code
119 ms
# dot_positions = [100, 300] # edit me!
👀 Reading hidden code
12.1 μs

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.

👀 Reading hidden code
247 μs
@htl("""
<script src="https://cdn.jsdelivr.net/npm/d3@6.2.0/dist/d3.min.js"></script>

<script id="hello">

const positions = $(JSON.json(dot_positions))
const svg = this == null ? DOM.svg(600,200) : this
const s = this == null ? d3.select(svg) : this.s

s.selectAll("circle")
.data(positions)
.join("circle")
.transition()
.duration(300)
.attr("cx", d => d)
.attr("cy", 100)
.attr("r", 10)
.attr("fill", "gray")


const output = svg
output.s = s
return output
</script>

""")
👀 Reading hidden code
14.1 ms
script (generic function with 1 method)
script(s) = HTML("""
<script id="something">
$s
</script>
""")
👀 Reading hidden code
378 μs
using HypertextLiteral, JSON
👀 Reading hidden code
60.3 ms