AbstractPlutoDingetjes.Bonds.initial_value โ€” Method

The initial value of a bond. In a notebook containing @bind x my_widget, this will be used in two cases:

  1. The value of x will be set to x = AbstractPlutoDingetjes.Bonds.initial_value(my_widget) during the @bind call. This initial value will be used in cells that use x, until the widget is rendered in the browser and the first value is received.
  2. When running a notebook file without Pluto, e.g. shell> julia my_notebook.jl, this value will be used for x.

When not overloaded for your widget, it defaults to returning missing.

Example

import HypertextLiteral: @htl

struct MySlider
    range::AbstractRange{<:Real}
end

function Base.show(io::IO, m::MIME"text/html", s::MySlider)
    show(io, m, @htl(
        "<input type=range min=$(first(s.values)) step=$(step(s.values)) max=$(last(s.values))>"
    ))
end

function AbstractPlutoDingetjes.Bonds.initial_value(s::MySlider)
    first(s.range)
end

# Add the following for the same functionality on Pluto versions 0.17.0 and below. Will be ignored in future Pluto versions. See the compat info below.
Base.get(s::MySlider) = first(s.range)
Note about `transform_value`

If you are also using transform_value for your widget, then the value returned by initial_value should be the value after transformation.

Pluto 0.17.1

This feature only works in Pluto version 0.17.1 or above.

Older versions of Pluto used a Base.get overload for this (to avoid the need for the AbstractPlutoDingetjes package, but we changed our minds ๐Ÿ’•). To support all versions of Pluto, use both methods of declaring the initial value.

Use AbstractPlutoDingetjes.is_supported_by_display if you want to check support inside your widget.

AbstractPlutoDingetjes.Bonds.possible_values โ€” Method

The possible values of a bond. This is used when generating precomputed PlutoSliderServer states, see https://github.com/JuliaPluto/PlutoSliderServer.jl/pull/29. Not relevant outside of this use (for now...).

The returned value should be an iterable object that you can call length on (like a Vector or a Generator without filter) or return InfinitePossibilities() if this set is inifinite.

Examples

import HypertextLiteral: @htl

struct MySlider
    range::AbstractRange{<:Real}
end

function Base.show(io::IO, m::MIME"text/html", s::MySlider)
    show(io, m, @htl(
        "<input type=range min=$(first(s.values)) step=$(step(s.values)) max=$(last(s.values))>"
    ))
end

AbstractPlutoDingetjes.Bonds.possible_values(s::MySlider) = s.range
import HypertextLiteral: @htl

struct MyTextBox end

Base.show(io::IO, m::MIME"text/html", s::MyTextBox) = show(io, m, @htl("<input type=text>"))

AbstractPlutoDingetjes.Bonds.possible_values(s::MySlider) = AbstractPlutoDingetjes.Bonds.InfinitePossibilities()
Note about `transform_value`

If you are also using transform_value for your widget, then the values returned by possible_values should be the values before transformation.

Pluto 0.17.3

This feature only works in Pluto version 0.17.3 or above.

AbstractPlutoDingetjes.Bonds.transform_value โ€” Method

Transform a value received from the browser before assigning it to the bound julia variable. In a notebook containing @bind x my_widget, Pluto will run x = AbstractPlutoDingetjes.Bonds.transform_value(my_widget, $value_from_javascript). Without this hook, widgets in JavaScript can only return simple types (numbers, dictionaries, vectors) into bound variables.

When not overloaded for your widget, it defaults to returning the value unchanged, i.e. x = $value_from_javascript.

Example

import HypertextLiteral: @htl

struct MyVectorSlider
    values::Vector{<:Any} # note! a vector of arbitrary objects, not just numbers
end

function Base.show(io::IO, m::MIME"text/html", s::MyVectorSlider)
    show(io, m, @htl(
        "<input type=range min=1 max=$(length(s.values))>"
    ))
end

AbstractPlutoDingetjes.Bonds.transform_value(s::MyVectorSlider, value_from_javascript::Int) = s.values[value_from_javascript]
Pluto 0.17.1

This feature only works in Pluto version 0.17.1 or above. Values are not transformed in older versions.

Use AbstractPlutoDingetjes.is_supported_by_display if you want to check support inside your widget.

AbstractPlutoDingetjes.Bonds.validate_value โ€” Method

Validate a value received from the browser before "doing the pluto thing". In a notebook containing @bind x my_widget, Pluto will run AbstractPlutoDingetjes.Bonds.validate_value(my_widget, $value_from_javascript). If the result is false, then the value from JavaScript is considered "invalid" or "insecure", and no further code will be executed.

This is a protection measure when using your widget on a public PlutoSliderServer, where people could write fake requests that set bonds to arbitrary values.

The returned value should be a Boolean.

Example

import HypertextLiteral: @htl

struct MySlider
    range::AbstractRange{<:Real}
end

function Base.show(io::IO, m::MIME"text/html", s::MySlider)
    show(io, m, @htl(
        "<input type=range min=$(first(s.values)) step=$(step(s.values)) max=$(last(s.values))>"
    ))
end

function AbstractPlutoDingetjes.Bonds.validate_value(s::MySlider, from_browser::Real)
    first(s.range) <= from_browser <= last(s.range)
end
Note about `transform_value`

If you are also using transform_value for your widget, then the value validated by validate_value will be the value before transformation.

Info

The fallback method is validate_value(::Any, ::Any) = false. In the example above, this means that if the value is not a Real, it is automatically considered invalid.

Pluto TODO

This feature only works in Pluto version TODO: NOT RELEASED YET or above.

AbstractPlutoDingetjes.Display.published_to_js โ€” Method
AbstractPlutoDingetjes.Display.published_to_js(x)

Make the object x available to the JS runtime of this cell, to be rendered inside a <script> element. This system uses Pluto's optimized data transfer (probably with MsgPack and WebSocket), which is much more efficient for large amounts of data, including lossless transfer for Vector{UInt8} and Vector{Float64} (see the table below), and a global cache to avoid transmitting the same object twice.

The function published_to_js returns a special object that behaves like a piece of JavaScript code. We recommend using HypertextLiteral.jl to interpolate the result into a <script> element.

Example

import HypertextLiteral: @htl
import AbstractPlutoDingetjes.Display: published_to_js

let
    x = Dict(
        "data" => rand(Float64, 20),
        "name" => "juliette",
    )

    @htl("""
    <script>
    // we interpolate into JavaScript:
    const x = $(published_to_js(x))

    console.log(x.name, x.data)
    </script>
    """)
end

Types

JuliaJavaScript
String, Symbolstring
Booleanboolean
Int64, Int32, Int16, Int8, UInt64, UInt32, UInt16, UInt8, Float32, Float64Number
Nothing, Missingnull
DateTimeDate
UUID, MIMEstring
โ€“-โ€“-
Dictobject
NamedTupleobject
VectorArray
TupleArray
Vector{Int8}Int8Array
Vector{UInt8}Uint8Array
Vector{Int16}Int16Array
Vector{UInt16}Uint16Array
Vector{Int32}Int32Array
Vector{UInt32}Uint32Array
Vector{Float32}Float32Array
Vector{Float64}Float64Array

Note about IO context

The object that published_to_js returns needs to be rendered using the IO context that Pluto uses to render cell output. If you are using HypertextLiteral.jl, then this is easy to achieve.

The example above is using HypertextLiteral.@htl, and the cell returns a HypertextLiteral object, which will be rendered by Pluto. This means that Pluto will render it using its magical IO context, and all is good!

Custom show method

Below is a second example, to use when your are writing a custom HTML show method for your own type:

struct MyType
    data
end

function Base.show(io::IO, m::MIME"text/html", x::MyType)

    # โœ… This works
    show(io, m, @htl("""
    <script>
    let data = $(published_to_js(x.data))
    console.log(data)
    </script>
    """))
end

Test it out with:

MyType([1,2,3])

The trick that makes it work is: show(io, m, @htl(...)). This will take your HypertextLiteral object, and render it using the io object that was passed in.

Without HypertextLiteral.jl

The following would not work:

function Base.show(io::IO, m::MIME"text/html", x::MyType)

    # ๐Ÿ›‘ This does not work
    println(io, """
    <script>
    let data = $(published_to_js(x.data))
    console.log(data)
    </script>
    """)
end

This does not work, because the string interpolation (i.e. """ ... $(published_to_js(x.data)) ... """) happens on its own, without the io context used to render it.

The solution is to use HypertextLiteral.jl, passing through the io in your show method. If you can't use HypertextLiteral.jl, you could use repr to manually render published object to a string, using io as the context:

function Base.show(io::IO, m::MIME"text/html", x::MyType)
    
    # ๐ŸŸก This works, but we recommend the HypertextLiteral.jl example instead.
    rendered = repr(published_to_js(x.data); context=io)

    println(io, """
    <script>
    let data = $(rendered)
    console.log(data)
    </script>
    """)
end

Note on published object caching

Whenever a Julia object is sent to Pluto using published_to_js, its value is cached so that subsequent requests for the same object are served faster. This means that Pluto already optimizes the performance of sending data from Julia to Javascript and it is especially useful when the same object is rendered multiple times within the notebook.

Also: If you use published_to_js twice on the same object within the same cell, or in two different cells, the data is only transmitted once. The second published_to_js just contains a reference to the same data.

It is important to note that mutating an object that has already been sent to JavaScript with published_to_js will not change the value of this object on the JavaScript side, even if the cells with the published_to_js calls are re-run.

Compatibility

Old Pluto versions

Pluto 0.19.28

This feature only works in Pluto version 0.19.28 (July 2023) or above.

Older versions of Pluto used PlutoRunner.publish_to_js for this (to avoid the need for the AbstractPlutoDingetjes package, but we changed our minds ๐Ÿ’•).

Use AbstractPlutoDingetjes.is_supported_by_display if you want to check support inside your widget:

AbstractPlutoDingetjes.is_supported_by_display(io, published_to_js) ? 
    AbstractPlutoDingetjes.Display.published_to_js(x) : 
    PlutoRunner.publish_to_js(x)

(You need a reference to io for this, so this is useful inside a custom Base.show method for your own struct type.)

Outside of Pluto

This feature only works in Pluto-compatible environments (i.e. Pluto). Outside of Pluto, you might be happy with the default HypertextLiteral script interpolation. This is less performant for large objects, and some Julia types are mapped to a different JavaScript type (e.g. Vector{Int32} is mapped to a simple Array instead of Int32Array), but it might be good enough for your use case.

In your interpolation, check for support, and otherwise, just interpolate the object directly:

AbstractPlutoDingetjes.is_supported_by_display(io, published_to_js) ? 
    AbstractPlutoDingetjes.Display.published_to_js(x) : 
    x

Both

To support old versions of Pluto, and also support non-Pluto displays, you can combine the two:

AbstractPlutoDingetjes.is_supported_by_display(io, published_to_js) ? 
    # modern Pluto
    AbstractPlutoDingetjes.Display.published_to_js(x) : 
    isdefined(Main, :PlutoRunner) && isdefined(Main.PlutoRunner, :publish_to_js) ?
    # old Pluto
    PlutoRunner.publish_to_js(x) :
    # not Pluto
    x
AbstractPlutoDingetjes.Display.with_js_link โ€” Function
with_js_link(f::Function[, on_cancellation::Function])

Make a Julia function available to the JS runtime of this cell, to be called from JavaScript. This API allows for more advanced use cases than published_to_js, but is also more difficult to use.

Example

The easiest way to use this API is with HypertextLiteral. Here is a simple example:

@htl("""
<script>
const sqrt_from_julia = $(AbstractPlutoDingetjes.Display.with_js_link(sqrt))

// I can now call sqrt_from_julia like a JavaScript function. It returns a Promise:
const result = await sqrt_from_julia(9.0)
console.log(result)

</script>
""")

API

The use is very similar to Display.published_to_js. with_js_link returns a "piece of JavaScript code" that you interpolate directly into a <script> tag.

In JavaScript, the "piece of JavaScript code" returns a function. You can call this function with an argument (which will be passed to your Julia function), and it returns a Promise that resolves to the answer from your Julia function.

Serialization and inner workings

The request and response use the same communication protocol as published_to_js, so in particular, Vector{Float64} or Vector{UInt8} are really fast.

When not to use it

This API is only meant to support use cases that can not be covered with Display.published_to_js (or @bind). If possible, the use of these APIs is preferred over with_js_link: they will work with the Static HTML export and PlutoSliderServer.

published_to_js vs with_js_link

If the set of possible inputs is quite small, consider precomputing all possible outputs, and using published_to_js to publish everything at once.

For example, in the sqrt example above, if you know that the input will be an integer between 1 and 1 million, then we recommend using publish_to_js(sqrt.(1:1_000_000)) instead.

@bind vs with_js_link

We recommend using @bind instead of with_js_link when your widget also makes sense split into two: an input widget (with @bind) and an output widget (possibly with published_to_js). This will be easier for you to develop, and easier for others to understand. If you are still considering how to design your widget, try to start with @bind. But if with_js_link is exactly what you are looking for, go for it!

For example, you are showing a map of the world, and you want to show a weather forecast for the location where the user clicks. With with_js_link, you could make an awesome GUI where the forecast is shown as an overlay on the map. But a simpler option would be to have one widget where you pick a location on the map, which gets bound to a Julia variable location. Then other cells in the notebook can compute the forecast, which you show in the cell directly above or below the map.

Background task

JS link calculations are executed as a background task (not a thread). They can run in parallel with other computations in the notebook.

Don't make too many requests!

If you make too many requests from JS, then the notebook can become almost unusable. As a developer using this API, you need to take care to keep your users' notebooks responsive.

The JS link request returns a Promise that resolves to the response. Consider keeping track of whether you are currently requesting something from Julia, and avoid making more requests in the meantime.

It can also help to use throttling or debouncing to reduce the number of requests that you make.

For example, starting requests at a regular interval can lead to big trouble. Instead, wait for the last request to finish, set a delay, and then make the next request.

Advanced topics

Multiple clients

Since this API is designed for one-off requests, this communication does not go through Pluto's state management (the request and response are not stored in the state). If multiple clients are connected in parallel, then the messages are not shared between clients. The client that made the request will receive the response.

Cancellation

For advanced use cases, you can also provide a second argument to with_js_link โ€“ a function that will be called when the link is cancelled. This can be useful to clean up resources or to cancel a long-running process.

Cancellation happens not when the browser disconnects, but right before the cell or one of its dependency cells re-evaluates. This is done to prevent using data defined in the notebook that is no longer well-defined.

If your function is long-running, does async I/O work or runs threaded, then it is possible that your link gets cancelled during an execution. Use the on_cancellation callback wisely!

Bidirectional communication

The primary purpose of this API is for JavaScript to ask a question to Julia, and receive an answer. So there is communication is both directions, but it needs to be initiated from JavaScript.

If you need to send unrequested updates from Julia to JavaScript, then you could use polling. Long polling works well with this API, but you need to use the on_cancellation callback to clean up resources. Remember that Pluto notebooks can be viewed by multiple clients connected in parallel. Here is an example.

Pluto 0.19.41

This feature only works in Pluto version 0.19.41 or above.

Use AbstractPlutoDingetjes.is_supported_by_display if you want to check support inside your widget.

AbstractPlutoDingetjes.AbstractPlutoDingetjes โ€” Module

A more technical package meant for people who develop widgets for other Pluto users. By using and implementing methods from this package, you can give your widget more advanced features.

This package is very small, and contains no functional code (all functionality is implemented by Pluto). This means that you can add AbstractPlutoDingetjes.jl as a dependency to your package with almost no overhead!

Common use

Most functions in AbstractPlutoDingetjes are most useful when used with the typeโ€“showโ€“@htl recipe. A basic example:

import HypertextLiteral: @htl

struct MyCoolSlider
    min::Real
    max::Real
end

function Base.show(io::IO, m::MIME"text/html", d::MyCoolSlider)
    show(io, m, @htl(
        """
        <input type=range min=$(d.min) max=$(d.max)>
        """
    ))
end

# Use:
@bind value MyCoolSlider(5, 10)

This example does not use AbstractPlutoDingetjes, but AbstractPlutoDingetjes can be used to gradually enhance this widget.

AbstractPlutoDingetjes.is_inside_pluto โ€” Method
is_inside_pluto(io::IO)::Bool

Are we rendering inside a Pluto notebook?

This function should be used inside a Base.show method, and the first argument should be the io provided to the Base.show method.

Example

function Base.show(io::IO, m::MIME"text/html", d::MyCoolWidget)
    if is_inside_pluto(io)
        Base.show(io, m, @htl("..."))
    else
        # do something else
    end
end
AbstractPlutoDingetjes.is_supported_by_display โ€” Method
is_supported_by_display(io::IO, f::Union{Function,Module})::Bool

Check whether the current runtime/display (Pluto) supports a given feature from AbstractPlutoDingetjes (i.e. is the Pluto version new enough). This function should be used inside a Base.show method. The first argument should be the io provided to the Base.show method, and the second argument is the feature to check.

You can use this information to:

  • Error the show method of your widget if the runtime/display does not support the required features, or
  • Render a simpler version of your widget that does not depend on the advanced Pluto features.

Example

import AbstractPlutoDingetjes: is_supported_by_display, Bonds

struct MyCoolDingetje
    values::Vector
end

function Base.show(io::IO, m::MIME"text/html", d::MyCoolDingetje)
    if !(is_supported_by_display(io, Bonds.initial_value) && is_supported_by_display(io, Bonds.transform_value))
        error("This widget does not work in the current version of Pluto.")
    end

    write(io, html"...")
end

See also: is_inside_pluto.