Api

Public Functions

Bonito.AppType
App(callback_or_dom; title="Bonito App")
App((session, request) -> DOM.div(...))
App((session::Session) -> DOM.div(...))
App((request::HTTP.Request) -> DOM.div(...))
App(() -> DOM.div(...))
App(DOM.div(...))

Usage:

using Bonito
app = App() do
    return DOM.div(DOM.h1("hello world"), js"""console.log('hello world')""")
end

If you depend on global observable, make sure to bind it to the session. This is pretty important, since every time you display the app, listeners will get registered to it, that will just continue staying there until your Julia process gets closed. bind_global prevents that by binding the observable to the life cycle of the session and cleaning up the state after the app isn't displayed anymore. If you serve the App via a Server, be aware, that those globals will be shared with everyone visiting the page, so possibly by many users concurrently.

global some_observable = Observable("global hello world")
App() do session::Session
    bound_global = bind_global(session, some_observable)
    return DOM.div(bound_global)
end
Bonito.AssetType

Represent an asset stored at an URL. We try to always have online & local files for assets

Bonito.CodeEditorMethod
CodeEditor(language::String; initial_source="", theme="chrome", editor_options...)

Defaults for editor_options:

(
    autoScrollEditorIntoView = true,
    copyWithEmptySelection = true,
    wrapBehavioursEnabled = true,
    useSoftTabs = true,
    enableMultiselect = true,
    showLineNumbers = false,
    fontSize = 16,
    wrap = 80,
    mergeUndoDeltas = "always"
)
Bonito.DropdownType
Dropdown(options; index=1, option_to_string=string, style=Styles(), dom_attributes...)

A simple Dropdown, which can be styled via the style::Styles attribute.

Example

App() do
    style = Styles(
        CSS("font-weight" => "500"),
        CSS(":hover", "background-color" => "silver"),
        CSS(":focus", "box-shadow" => "rgba(0, 0, 0, 0.5) 0px 0px 5px"),
    )
    dropdown = Dropdown(["a", "b", "c"]; index=2, style=style)
    on(dropdown.value) do value
        @info value
    end
    return dropdown
end
Bonito.NoServerType

We don't serve files and include anything directly as raw bytes. Interpolating the same asset many times, will only upload the file to JS one time though.

Bonito.StylableSliderMethod
StylableSlider(
    range::AbstractVector;
    value=first(range),
    slider_height=15,
    thumb_width=slider_height,
    thumb_height=slider_height,
    track_height=slider_height / 2,
    track_active_height=track_height + 2,
    backgroundcolor="transparent",
    track_color="#eee",
    track_active_color="#ddd",
    thumb_color="#fff",
    style::Styles=Styles(),
    track_style::Styles=Styles(),
    thumb_style::Styles=Styles(),
    track_active_style::Styles=Styles(),
)

Creates a Stylable Slider, where the basic attributes are easily custimizable via keyword arguments, while the more advanced details can be styled via the style, track_style, thumb_style and track_active_style arguments with the whole might of CSS. This does not use <input type="range"> but is a custom implementation using <div>s javascript, since it is not easily possible to style the native slider in a cross-browser way. For using pure HTML sliders, use Bonito.Slider.

Example

App() do
    Bonito.StylableSlider(
        1:10;
        value=5,
        slider_height=20,
        track_color="lightblue",
        track_active_color="#F0F8FF",
        thumb_color="#fff",
        style=Styles(
            CSS("hover", "background-color" => "lightgray"),
            CSS("border-radius" => "0px"),
        ),
        track_style=Styles(
            "border-radius" => "3px",
            "border" => "1px solid black",
        ),
        thumb_style=Styles(
            "border-radius" => "3px",
            "border" => "1px solid black",
        ),
    )
end
Bonito.StylesType
Styles(css::CSS...)

Creates a Styles object, which represents a Set of CSS objects. You can insert the Styles object into a DOM node, and it will be rendered as a <style> node. If you assign it directly to DOM.div(style=Style(...)), the styling will be applied to the specific div. Note, that per Session, each unique css object in all Styles across the session will only be rendered once. This makes it easy to create Styling inside of components, while not worrying about creating lots of Style nodes on the page. There are a two more convenience constructors to make Styles a bit easier to use:

Styles(pairs::Pair...) = Styles(CSS(pairs...))
Styles(priority::Styles, defaults...) = merge(Styles(defaults...), priority)

For styling components, it's recommended, to always allow user to merge in customizations of a Style, like this:

function MyComponent(; style=Styles())
    return DOM.div(style=Styles(style, "color" => "red"))
end

All Bonito components are stylable this way.

Info

Why not Hyperscript.Style? While the scoped styling via Hyperscript.Style is great, it makes it harder to create stylable components, since it doesn't allow the deduplication of CSS objects across the session. It's also significantly slower, since it's not as specialized on the deduplication and the camelcase keyword to css attribute conversion is pretty costly. That's also why CSS uses pairs of strings instead of keyword arguments.

WidgetsBase.ButtonType
Button(name; style=Styles(), dom_attributes...)

A simple button, which can be styled a style::Styles.

Example

App() do
    style = Styles(
        CSS("font-weight" => "500"),
        CSS(":hover", "background-color" => "silver"),
        CSS(":focus", "box-shadow" => "rgba(0, 0, 0, 0.5) 0px 0px 5px"),
    )
    button = Button("Click me"; style=style)
    on(button.value) do click::Bool
        @info "Button clicked!"
    end
    return button
end
WidgetsBase.NumberInputType
NumberInput(default_value; style=Styles(), dom_attributes...)

A simple NumberInput, which can be styled via the style::Styles attribute.

Example

App() do
    style = Styles(
        CSS("font-weight" => "500"),
        CSS(":hover", "background-color" => "silver"),
        CSS(":focus", "box-shadow" => "rgba(0, 0, 0, 0.5) 0px 0px 5px"),
    )
    numberinput = NumberInput(0.0; style=style)
    on(numberinput.value) do value::Float64
        @info value
    end
    return numberinput
end
WidgetsBase.TextFieldType
TextField(default_text; style=Styles(), dom_attributes...)

A simple TextField, which can be styled via the style::Styles attribute.

Example

App() do
    style = Styles(
        CSS("font-weight" => "500"),
        CSS(":hover", "background-color" => "silver"),
        CSS(":focus", "box-shadow" => "rgba(0, 0, 0, 0.5) 0px 0px 5px"),
    )
    textfield = TextField("write something"; style=style)
    on(textfield.value) do text::String
        @info text
    end
    return textfield
end
Bonito.CardMethod
Card(
    content;
    style::Styles=Styles(),
    backgroundcolor=RGBA(1, 1, 1, 0.2),
    shadow_size="0 4px 8px",
    padding="12px",
    margin="2px",
    shadow_color=RGBA(0, 0, 0.2, 0.2),
    width="auto",
    height="auto",
    border_radius="10px",
    div_attributes...,
)

A Card is a container with a shadow and rounded corners. It is a good way to group elements together and make them stand out from the background. One can easily style them via the above keyword arguments or via the style argument with any CSS attribute.

Example

    App() do
        Card(
            DOM.h1("This is a card");
            width="200px",
            height="200px",
            backgroundcolor="white",
            shadow_size="0 0 10px",
            shadow_color="blue",
            padding="20px",
            margin="20px",
            border_radius="20px",
            style = Styles(
                CSS("hover", "background-color" => "lightgray")
            )
        )
    end
Bonito.CenteredMethod
Centered(content; style=Styles(), grid_attributes...)

Creates an element where the content is centered via Grid.

Bonito.ColMethod
Col(elems...; grid_attributes...)

Places objects in a column, based on Grid.

Bonito.GridMethod
Grid(
    elems...;
    gap="10px",
    width="100%",
    height="100%",
    # All below Attributes are set to the default CSS values:
    columns="none",
    rows="none",
    areas="none",
    justify_content="normal",
    justify_items="legacy",
    align_content="normal",
    align_items="legacy",
    style::Styles=Styles(),
    div_attributes...,
)

A Grid is a container that lays out its children in a grid, based on the powerful css display: grid property.

Bonito.LabeledMethod
Labeled(object, label; label_style=Styles(), attributes...)

A Labeled container with a simople layout to put a label next to an object.

App() do
    label_style = Styles(
        "color" => "white",
        "padding" => "3px",
        "font-size" => "1.5rem",
        "text-shadow" => "0px 0px 10px black, 1px 1px 3px black")
    slider = StylableSlider(1:10)
    Card(Labeled(slider, slider.value; label_style=label_style, width="auto"); backgroundcolor="gray")
end
Bonito.PageMethod
Page(;
    offline=false, exportable=true,
    connection::Union{Nothing, FrontendConnection}=nothing,
    server_config...
)

A Page can be used for resetting the Bonito state in a multi page display outputs, like it's the case for Pluto/IJulia/Documenter. For Documenter, the page needs to be set to exportable=true, offline=true, but doesn't need to, since Page defaults to the most common parameters for known Packages. Exportable has the effect of inlining all data & js dependencies, so that everything can be loaded in a single HTML object. offline=true will make the Page not even try to connect to a running Julia process, which makes sense for the kind of static export we do in Documenter. For convenience, one can also pass additional server configurations, which will directly get put into configure_server!(;server_config...). Have a look at the docs for configure_server! to see the parameters.

Bonito.RowMethod
Row(elems...; grid_attributes...)

Places objects in a row, based on Grid.

Bonito.configure_server!Method
configure_server!(;
        listen_url::String=SERVER_CONFIGURATION.listen_url[],
        listen_port::Integer=SERVER_CONFIGURATION.listen_port[],
        forwarded_port::Integer=listen_port,
        proxy_url=nothing,
        content_delivery_url=nothing
    )

Configures the parameters for the automatically started server.

Parameters:

* listen_url=SERVER_CONFIGURATION.listen_url[]
    The address the server listens to.
    must be 0.0.0.0, 127.0.0.1, ::, ::1, or localhost.
    If not set differently by an ENV variable, will default to 127.0.0.1

* listen_port::Integer=SERVER_CONFIGURATION.listen_port[],
    The Port to which the default server listens to
    If not set differently by an ENV variable, will default to 9384

* forwarded_port::Integer=listen_port,
    if port gets forwarded to some other port, set it here!

* proxy_url=nothing
    The url from which the server is reachable.
    If served on "127.0.0.1", this will default to http://localhost:forwarded_port
    if listen_url is "0.0.0.0", this will default to http://$(Sockets.getipaddr()):forwarded_port
    so that the server is reachable inside the local network.
    If the server should be reachable from some external dns server,
    this needs to be set here.
Bonito.evaljsMethod
evaljs(session::Session, jss::JSCode)

Evaluate a javascript script in session.

Bonito.evaljs_valueMethod
evaljs_value(session::Session, js::JSCode)

Evals js code and returns the jsonified value. Blocks until value is returned. May block indefinitely, when called with a session that doesn't have a connection to the browser.

Bonito.export_staticMethod
export_static(html_file::Union{IO, String}, app::App)
export_static(folder::String, routes::Routes)

Exports the app defined by app with all its assets a single HTML file. Or exports all routes defined by routes to folder.

Bonito.interactive_serverFunction
interactive_server(f, paths, modules=[]; url="127.0.0.1", port=8081, all=true)

Revise base server that will serve a static side based on Bonito and will update on any code change!

Usage:

using Revise, Website
using Website.Bonito

# Start the interactive server and develop your website!
routes, task, server = interactive_server(Website.asset_paths()) do
    return Routes(
        "/" => App(index, title="Makie"),
        "/team" => App(team, title="Team"),
        "/contact" => App(contact, title="Contact"),
        "/support" => App(support, title="Support")
    )
end

# Once everything looks goo, export the static site
dir = joinpath(@__DIR__, "docs")
# only delete the bonito generated files
rm(joinpath(dir, "bonito"); recursive=true, force=true)
Bonito.export_static(dir, routes)

For the complete code, visit the Makie website repository which is using Bonito: MakieOrg/Website

Bonito.linkjsMethod
linkjs(session::Session, a::Observable, b::Observable)

for an open session, link a and b on the javascript side. This will also Link the observables in Julia, but only as long as the session is active.

Bonito.onjsMethod
onjs(session::Session, obs::Observable, func::JSCode)

Register a javascript function with session, that get's called when obs gets a new value. If the observable gets updated from the JS side, the calling of func will be triggered entirely in javascript, without any communication with the Julia session.

Private Functions

Bonito.FrontendConnectionType

Inteface for FrontendConnection

struct MyConnection <: FrontendConnection
end

Needs to have a constructor with 0 arguments:

MyConnection()

Needs to overload Base.write for sending binary data

Base.write(connection::MyConnection, bytes::AbstractVector{UInt8})

Needs to implement isopen to indicate status of connection

Base.isopen(c::MyConnection)

Setup connection will be called before rendering any dom with session. The return value will be inserted into the DOM of the rendered App and can be used to do the JS part of opening the connection.

Bonito.setup_connection(session::Session{IJuliaConnection})::Union{JSCode, Nothing}

One can overload use_parent_session, to turn on rendering dom objects inside sub-sessions while keeping one parent session managing the connection alive. This is handy for IJulia/Pluto, since the parent session just needs to be initialized one time and can stay active and globally store objects used multiple times across doms.

Bonito.use_parent_session(::Session{MyConnection}) = false/false
Bonito.JSCodeType

Javascript code that supports interpolation of Julia Objects. Construction of JSCode via string macro:

jsc = js"console.log($(some_julia_variable))"

This will decompose into:

jsc.source == [JSString("console.log("), some_julia_variable, JSString(""")]
Bonito.JSExceptionMethod

Creates a Julia exception from data passed to us by the frondend!

Bonito.JSUpdateObservableType

Functor to update JS part when an observable changes. We make this a Functor, so we can clearly identify it and don't sent any updates, if the JS side requires to update an Observable (so we don't get an endless update cycle)

Bonito.TableType

A simple wrapper for types that conform to the Tables.jl Table interface, which gets rendered nicely!

Bonito.LabelMethod
Label(value; style=Styles(), attributes...)

A Label is a simple text element, with a bold font and a font size of 1rem.

Bonito.add_cached!Method
add_cached!(create_cached_object::Function, session::Session, message_cache::Dict{String, Any}, key::String)

Checks if key is already cached by the session or it's root session (we skip any child session between root -> this session). If not cached already, we call create_cached_object to create a serialized form of the object corresponding to key and cache it. We return nothing if already cached, or the serialized object if not cached. We also handle the part of adding things to the message_cache from the serialization context.

Bonito.export_standaloneMethod
export_standaloneexport_standalone(
    app::App, folder::String;
    clear_folder=false, write_index_html=true,
    absolute_urls=false, content_delivery_url="file://" * folder * "/",
    single_html=false)

Exports the app defined by app::Application with all its assets to folder. Will write the main html out into folder/index.html. Overwrites all existing files! If this gets served behind a proxy, set absolute_urls=true and set content_delivery_url to your proxy url. If clear_folder=true all files in folder will get deleted before exporting again! single_html=true will write out a single html instead of writing out JS depencies as separate files.

Bonito.getextensionMethod
getextension(path)

Get the file extension of the path. The extension is defined to be the bit after the last dot, excluding any query string.

Examples

julia> Bonito.getextension("foo.bar.js")
"js"
julia> Bonito.getextension("https://my-cdn.net/foo.bar.css?version=1")
"css"

Taken from WebIO.jl

Bonito.is_onlineMethod
is_online(path)

Determine whether or not the specified path is a local filesystem path (and not a remote resource that is hosted on, for example, a CDN).

Bonito.jsrenderMethod
jsrender([::Session], x::Any)

Internal render method to create a valid dom. Registers used observables with a session And makes sure the dom only contains valid elements. Overload jsrender(::YourType) To enable putting YourType into a dom element/div. You can also overload it to take a session as first argument, to register messages with the current web session (e.g. via onjs).

Bonito.on_document_loadMethod
on_document_load(session::Session, js::JSCode)

executes javascript after document is loaded

Bonito.onloadMethod
onload(session::Session, node::Node, func::JSCode)

calls javascript func with node, once node has been displayed.

Bonito.page_htmlMethod
page_html(session::Session, html_body)

Embeds the html_body in a standalone html document!

Bonito.process_messageMethod
process_message(session::Session, bytes::AbstractVector{UInt8})

Handles the incoming websocket messages from the frontend. Messages are expected to be gzip compressed and packed via MsgPack.

Bonito.record_statesMethod
record_states(session::Session, dom::Hyperscript.Node)

Records the states of all widgets in the dom. Any widget that implements the following interface will be found in the DOM and can be recorded:

# Implementing interface for Bonito.Slider!
is_widget(::Slider) = true
value_range(slider::Slider) = 1:length(slider.values[])
to_watch(slider::Slider) = slider.index # the observable that will trigger JS state change
Warn

This is experimental and might change in the future! It can also create really large HTML files, since it needs to record all combinations of widget states. It's also not well optimized yet and may create a lot of duplicated messages.

Bonito.register_asset_server!Method
register_asset_server!(condition::Function, ::Type{<: AbstractAssetServer})

Registers a new asset server type. condition is a function that should return nothing, if the asset server type shouldn't be used, and an initialized asset server object, if the conditions are right. E.g. The Bonito.NoServer be used inside an IJulia notebook so it's registered like this:

register_asset_server!(NoServer) do
    if isdefined(Main, :IJulia)
        return NoServer()
    end
    return nothing
end

The last asset server registered takes priority, so if you register a new connection last in your Package, and always return it, You will overwrite the connection type for any other package. If you want to force usage temporary, try:

force_asset_server(YourAssetServer()) do
    ...
end
# which is the same as:
force_asset_server!(YourAssetServer())
...
force_asset_server!()
Bonito.register_connection!Method
register_connection!(condition::Function, ::Type{<: FrontendConnection})

Registers a new Connection type.

condition is a function that should return nothing, if the connection type shouldn't be used, and an initialized Connection, if the conditions are right. E.g. The IJulia connection should only be used inside an IJulia notebook so it's registered like this:

register_connection!(IJuliaConnection) do
    if isdefined(Main, :IJulia)
        return IJuliaConnection()
    end
    return nothing
end

The last connection registered take priority, so if you register a new connection last in your Package, and always return it, You will overwrite the connection type for any other package. If you want to force usage temporary, try:

force_connection(YourConnectionType()) do
    ...
end
# which is the same as:
force_connection!(YourConnectionType())
...
force_connection!()
Bonito.replace_expressionsMethod
replace_expressions(markdown, context)

Replaces all expressions inside markdown savely, by only supporting getindex/getfield expression that will index into context

Bonito.set_cleanup_time!Method
set_cleanup_time!(time_in_hrs::Real)

Sets the time that sessions remain open after the browser tab is closed. This allows reconnecting to the same session. Only works for Websocket connection inside VSCode right now, and will display the same App again from first display. State that isn't stored in Observables inside that app is lost.

Bonito.string_to_markdownMethod
string_to_markdown(session::Session, source::String; eval_julia_code=false)

Replaces all interpolation expressions inside markdown savely, by only supporting getindex/getfield expression that will index into context. You can eval Julia code blocks by setting eval_julia_code to a Module, into which the code gets evaluated!

Bonito.update_nocycle!Method

Update the value of an observable, without sending changes to the JS frontend. This will be used to update updates from the forntend.

Sockets.sendMethod
send(session::Session; attributes...)

Send values to the frontend via JSON for now

Bonito.HTTPServer.ServerMethod

Server( dom, url::String, port::Int; verbose = -1 )

Creates an application that manages the global server state!

Base.waitMethod
wait(server::Server)

Wait on the server task, i.e. block execution by bringing the server event loop to the foreground.

Bonito.HTTPServer.online_urlMethod
online_url(server::Server, url)

The url to connect to the server from the internet. Needs to have server.proxy_url set to the IP or dns route of the server

Bonito.HTTPServer.tryrunMethod
tryrun(cmd::Cmd)

Try to run a command. Return true if cmd runs and is successful (exits with a code of 0). Return false otherwise.