Api
Public Functions
Bonito.AbstractConnectionIndicator — Type
AbstractConnectionIndicatorAbstract type for connection indicators. Custom indicators should subtype this. Implementations should define jsrender(session::Session, indicator::T) where T is the subtype.
Bonito.AbstractPasswordStore — Type
AbstractPasswordStoreAbstract interface for password storage and authentication.
Required Methods
get_user(store, username::String)::Union{User, Nothing}: Retrieve user by username
Provided Methods
authenticate(store, username::String, password::String)::Bool: Verify credentials (implemented via get_user)
Implementation Guide
Subtypes only need to implement get_user(). The authenticate() method will automatically:
- Call
get_user()to retrieve the user - Verify the password using the User's authenticate method
- Return true if credentials match, false otherwise
For custom authentication logic (e.g., LDAP), you can override authenticate().
Bonito.App — Type
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')""")
endIf 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)
endBonito.Asset — Type
Asset(path_or_url; name=nothing, es6module=false, check_isfile=false, bundle_dir=nothing, mediatype=:inferred)Represent an asset (JavaScript, CSS, image, etc.) that can be included in a Bonito DOM.
Arguments
path_or_url: Local file path or URL to the assetname: Optional name for the asset. For JS assets, this becomes the global variable name when loaded. Defaults to the filename without extension for JS files (e.g., "ace.js" → "ace")es6module: Whether this is an ES6 module that needs bundling (default: false)check_isfile: Verify that local files exist (default: false)bundle_dir: Directory for bundled ES6 modules (default: inferred)mediatype: Media type symbol (:js, :css, :png, etc.). Auto-detected from extension if not specified
JavaScript Asset Loading
Non-module scripts (es6module=false)
For non-module JavaScript assets, interpolating the asset in JS code creates a Promise that resolves with the global object:
ace_asset = Asset("https://cdn.jsdelivr.net/gh/ajaxorg/ace-builds/src-min/ace.js")
# name defaults to "ace" (inferred from filename)
js"""
$(ace_asset).then(ace => {
// ace object is now available
const editor = ace.edit(element);
});
"""To override the global name:
Asset("https://example.com/mylibrary.js"; name="MyLib")ES6 modules (es6module=true)
For ES6 modules, use the ES6Module(path) constructor which automatically sets es6module=true and bundles the module with its dependencies. ES6 modules also support the .then() syntax:
mod = ES6Module("path/to/module.js") # name defaults to "module"
js"""
$(mod).then(exports => {
// ES6 module exports are now available
exports.someFunction();
});
"""Fields
name::Union{Nothing, String}: Asset name (used as global variable name for JS assets)es6module::Bool: Whether this is an ES6 modulemedia_type::Symbol: Type of asset (:js, :css, :png, etc.)online_path::String: URL if asset is hosted onlinelocal_path::Union{String, Path}: Local file system pathbundle_file::Union{String, Path}: Path to bundled file (ES6 modules only)bundle_data::Vector{UInt8}: Bundled file contents (ES6 modules only)content_hash::RefValue{String}: Hash of bundled content (ES6 modules only)
See Also
ES6Module(path): Convenience constructor for ES6 modules with automatic bundling
Bonito.ChoicesBox — Type
ChoicesBox(options; initial_value="", choicejsparams=ChoicesJSParams(...), attributes...)A combo box widget using the Choices.js library that allows both text input and selection from predefined options. Users can either select from the dropdown list or type their own custom values.
Fields
options::Observable{Vector{String}}: Available dropdown optionsvalue::Observable{String}: Current selected or typed valuechoicejsparams::ChoicesJSParams: Choices.js configuration parametersattributes::Dict{Symbol, Any}: DOM attributes applied to the elementChoicesBox(options; initial_value="", choicejsparams=ChoicesJSParams(...), attributes...)
Constructor for creating a ChoicesBox widget.
Arguments
options: Vector or Observable of string options for the dropdowninitial_value="": Initial selected valuechoicejsparams=ChoicesJSParams(searchPlaceholderValue="Type here..."): Choices.js configurationattributes...: Additional DOM attributes
Returns
ChoicesBox: A configured combo box widget instance
Example
App() do
# Create a combo box with some sample options
fruits = ["Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape"]
# Configure Choices.js parameters
params = ChoicesJSParams(
searchPlaceholderValue="Type to search fruits...",
searchEnabled=true,
shouldSort=true,
searchResultLimit=5
)
combobox = ChoicesBox(fruits;
initial_value="Apple",
choicejsparams=params
)
# Display selected value
selected_display = map(combobox.value) do value
isnothing(value) ? "No selection" : "Selected: $value"
end
# Handle value changes
on(combobox.value) do value
@info "ComboBox value changed to: $value"
end
return DOM.div(
DOM.h2("Choices.js ComboBox Example"),
DOM.p("This combo box allows you to select from predefined options or type your own:"),
combobox,
DOM.p(selected_display, style="margin-top: 20px; font-weight: bold;")
)
end
Bonito.ChoicesJSParams — Type
ChoicesJSParams(; kwargs...)Wrapper struct for parameters to the ChoicesJS Choices constructor.
Parameters include search functionality, rendering options, and behavior settings for the Choices.js library. See the official documentation for complete parameter reference: https://github.com/Choices-js/Choices/blob/main/README.md
Fields
addItems::Bool: Allow adding of items (default: true)itemSelectText::String: Text shown when hovering over selectable itemsplaceholder::Bool: Show placeholder text (default: true)placeholderValue::String: Placeholder text to displayremoveItemButton::Bool: Show remove button on items (default: false)renderChoiceLimit::Int: Limit choices rendered (-1 for no limit)searchEnabled::Bool: Enable search functionality (default: true)searchPlaceholderValue::String: Search input placeholder textsearchResultLimit::Int: Limit search results (default: 4)shouldSort::Bool: Sort choices alphabetically (default: true)
Bonito.CodeEditor — Method
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"
)The content of the editor (as a string) is updated in editor.onchange::Observable.
Bonito.ConnectionIndicator — Type
ConnectionIndicator <: AbstractConnectionIndicatorDefault connection indicator showing connection status as an LED-like circle. See the full documentation with ?ConnectionIndicator after loading Bonito.
Bonito.Dropdown — Type
Dropdown(options; index=1, option_to_string=string, style=Styles(), dom_attributes...)A simple Dropdown, which can be styled via the style::Styles attribute, style=nothing turns off the default Bonito styling.
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.FolderServer — Type
FolderServer(folder::String)A simple file server that serves files from the specified folder. Example with a static site generated with Bonito:
# Build a static site
app = App(()-> DOM.div("Hello World"));
routes = Routes(
"/" => app
# Add more routes as needed
)
export_static("build", routes)
# Serve the static site from the build folder
server = Bonito.Server("0.0.0.0", 8982)
route!(server, r".*" => FolderServer("build"))Bonito.NoServer — Type
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.ProtectedRoute — Type
ProtectedRoute{T, PS <: AbstractPasswordStore}A wrapper that adds HTTP Basic Authentication to any type that implements apply_handler. Can wrap Bonito.App, FolderServer, or any other handler type.
Security Notes
- REQUIRES HTTPS: HTTP Basic Auth sends credentials with every request. Without HTTPS, credentials are transmitted in plaintext (Base64 is NOT encryption).
- Experimental: This is a simple implementation for basic use cases with no security guarantees.
- Rate Limiting: Built-in protection against brute force attacks (max 5 attempts per IP per minute).
- PBKDF2 Password Hashing: Uses PBKDF2-HMAC-SHA256 with 10,000 iterations and random salt.
- Extensible: Use custom AbstractPasswordStore implementations for multi-user or database-backed auth.
Fields
handler::T: The wrapped handler (e.g., Bonito.App, FolderServer, etc.)password_store::PS: Password store implementing AbstractPasswordStore interfacerealm::String: Authentication realm name (default: "Protected Area")failed_attempts::Dict{String, Vector{Float64}}: IP -> timestamps of failed attemptsmax_attempts::Int: Maximum failed attempts allowed (default: 5)lockout_window::Float64: Time window in seconds for rate limiting (default: 60.0)auth_required_handler: Handler for 401 responses (default: built-in HTML page)rate_limited_handler: Handler for 429 responses (default: built-in HTML page)
Example
# Create app
app = App() do
return DOM.h1("Hello World")
end
# Create password store
store = SingleUser("admin", "secret123")
# Create protected route
protected_app = ProtectedRoute(app, store)
# With custom error pages
auth_page = App() do
return DOM.div(
DOM.h1("Authentication Required"),
DOM.p("Please log in to access this resource")
)
end
rate_limit_page = App() do
return DOM.div(
DOM.h1("Too Many Attempts"),
DOM.p("Please wait before trying again")
)
end
protected_app = ProtectedRoute(app, store;
auth_required_handler=auth_page,
rate_limited_handler=rate_limit_page)
# Add to server (MUST use HTTPS in production!)
server = Bonito.Server("0.0.0.0", 8443) # Use SSL/TLS
route!(server, "/" => protected_app)Bonito.Session — Type
A web session with a user
Bonito.SingleUser — Type
SingleUser <: AbstractPasswordStoreSimple password store for single-user authentication.
Fields
user::User: The single user
Bonito.SingleUser — Method
SingleUser(username::String, password::String; iterations::Int=10_000)Create a single-user password store with automatic password hashing.
Example
store = SingleUser("admin", "secret123")
authenticated = authenticate(store, "admin", "secret123") # trueBonito.StylableSlider — Method
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.Styles — Type
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"))
endAll Bonito components are stylable this way.
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.
Bonito.User — Type
UserRepresents a user with authentication credentials.
Fields
username::String: The usernamepassword_hash::Vector{UInt8}: PBKDF2 derived keysalt::Vector{UInt8}: Random salt for password hashingiterations::Int: PBKDF2 iteration countmetadata::Dict{String, Any}: Optional user metadata (roles, permissions, etc.)
Bonito.User — Method
User(username::String, password::String; iterations::Int=10_000, metadata::Dict{String, Any}=Dict{String, Any}())Create a new user with automatic password hashing using PBKDF2.
Bonito.WebSocketConnection — Method
handles a new websocket connection to a sessionWidgetsBase.Button — Type
Button(name; style=Styles(), dom_attributes...)A simple button, which can be styled a style::Styles. Set kwarg style=nothing to turn off the default Bonito styling.
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.Checkbox — Type
Checkbox(default_value; style=Styles(), dom_attributes...)A simple Checkbox, which can be styled via the style::Styles attribute.
WidgetsBase.NumberInput — Type
NumberInput(default_value; style=Styles(), dom_attributes...)A simple NumberInput, which can be styled via the style::Styles attribute, style=nothing turns off the default Bonito styling.
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.TextField — Type
TextField(default_text; style=Styles(), dom_attributes...)A simple TextField, which can be styled via the style::Styles attribute, style=nothing turns off the default Bonito styling.
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.Card — Method
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.Centered — Method
Centered(content; style=Styles(), grid_attributes...)Creates an element where the content is centered via Grid.
Bonito.Col — Method
Col(elems...; grid_attributes...)Places objects in a column, based on Grid.
Bonito.ES6Module — Method
ES6Module(path)Create an ES6 module asset that will be bundled using Deno.
ES6 modules are automatically bundled with their dependencies when first loaded. Interpolating an ES6Module in JavaScript code returns a Promise that resolves to the module's exports.
Example
THREE = ES6Module("https://unpkg.com/three@0.136.0/build/three.js")
js"""
$(THREE).then(module => {
// Use the module
const scene = new module.Scene();
})
"""Rebundling
Bonito tracks the timestamp of the main module file and will automatically rebundle if it detects changes. However, changes to imported/included files (e.g., Session.js imported by Bonito.js) are not tracked.
To force a rebundle when you've modified an included file, delete the bundle file:
mod = ES6Module("path/to/module.js")
rm(mod.bundle_file) # Bonito will rebundle on next useFor Bonito's internal JavaScript:
rm(Bonito.BonitoLib.bundle_file)Bonito.Grid — Method
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.Labeled — Method
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.Page — Method
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.Row — Method
Row(elems...; grid_attributes...)Places objects in a row, based on Grid.
Bonito.cleanup_globals — Method
cleanup_globals()Cleans up global state (servers, sessions, tasks) for precompilation compatibility. On Julia 1.11+, this is called automatically via atexit (which runs before serialization). On Julia 1.10, this must be called manually after precompilation workloads.
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, used to declare resources in Bonitos HTML and to establish a websocket connection.
Setting it to `""` or `nothing` will use the url the server listens to.
So, if `listen_url="127.0.0.1"`, this will default to http://localhost:forwarded_port (same as `local_url(server, "")`).
You can also set this to `"."` to use relative urls, e.g. for accessing the webpage on a local network, or when serving it online with your own server.
This is the preferred option for serving a whole website via Bonito, where you dont know in advanced where the page will be served.
If it's more complicated, e.g. when the HTML is served on a different url from the url to proxy through to the Bonito server,
a full URL needs to set, e.g. `proxy_url=https://bonito.makie.org`.Bonito.evaljs — Method
evaljs(session::Session, jss::JSCode)Evaluate a javascript script in session.
Bonito.evaljs_value — Method
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_static — Method
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.get_metadata — Function
get_metadata(session, key::Symbol)
get_metadata(session, key::Symbol, default)Get metadata from the root session. Returns nothing or the provided default if key doesn't exist. Metadata is stored on the root session and shared across all child sessions.
Bonito.interactive_server — Function
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 good, 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.linkjs — Method
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.onjs — Method
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.
Bonito.set_metadata! — Method
set_metadata!(session, key::Symbol, value)Set metadata on the root session. Returns the value. Metadata is stored on the root session and shared across all child sessions.
Private Functions
Bonito.AbstractWebsocketConnection — Type
Websocket based connection type
Bonito.CleanupPolicy — Type
abstract type CleanupPolicy endYou can create a custom cleanup policy by subclassing this type. Implementing the should_cleanup and allow_soft_close methods is required. You can also implement set_cleanup_time!if it makes sense for your policy.
function should_cleanup(policy::MyCleanupPolicy, session::Session)
function allow_soft_close(policy::MyCleanupPolicy)
function set_cleanup_time!(policy::MyCleanupPolicy, time_in_hrs::Real)This is quite low level, and you implementaiton should probably start by copying DefaultCleanupPolicy.
Bonito.DefaultCleanupPolicy — Type
mutable struct DefaultCleanupPolicy <: CleanupPolicy
session_open_wait_time=30
cleanup_time=0.0
endThis is the default cleanup policy. It closes sessions after session_open_wait_time seconds (default 30) if the browser didn't connect back to the displayed session. It also closes sessions after cleanup_time hours (default 0) if the session closes cleanly, indicating that the browser may reconnect if a tab is later restored. It returns true for allowsoftclose(...) when cleanup_time is non-zero.
Bonito.DualWebsocket — Method
handles a new websocket connection to a sessionBonito.FrontendConnection — Type
Inteface for FrontendConnection
struct MyConnection <: FrontendConnection
endNeeds 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/falseBonito.JSCode — Type
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.JSException — Method
Creates a Julia exception from data passed to us by the frondend!
Bonito.JSString — Type
The string part of JSCode.
Bonito.JSUpdateObservable — Type
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.Table — Type
A simple wrapper for types that conform to the Tables.jl Table interface, which gets rendered nicely!
Bonito.TrackingOnly — Type
TrackingOnly(key::String)Represents a cache entry that only needs to be tracked in a session but already exists in the global cache. When deserialized in JS, it self-registers the key to the session's tracked objects without adding anything to the global cache.
Bonito.Label — Method
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::AbstractDict{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.authenticate — Method
authenticate(store::AbstractPasswordStore, username::String, password::String) -> BoolAuthenticate user credentials against the password store. Default implementation retrieves the user via get_user() and verifies the password. Override this method only if you need custom authentication logic.
Bonito.authenticate — Method
authenticate(user::User, password::String) -> BoolAuthenticate a user with the provided password.
Bonito.check_auth — Method
check_auth(request::HTTP.Request, password_store::AbstractPasswordStore) -> BoolCheck if the HTTP request contains valid Basic Authentication credentials. Uses the password store's authenticate method.
Bonito.check_rate_limit — Method
check_rate_limit(protected::ProtectedRoute, client_ip::String) -> BoolCheck if client has exceeded rate limit for failed authentication attempts. Returns true if request should be allowed, false if rate limited.
Bonito.clear_failed_attempts — Method
clear_failed_attempts(protected::ProtectedRoute, client_ip::String)Clear failed attempts for a client after successful authentication.
Bonito.create_auth_challenge — Method
create_auth_challenge(protected::ProtectedRoute, context) -> HTTP.ResponseCreate an HTTP 401 Unauthorized response with WWW-Authenticate header. Renders the authrequiredhandler App.
Bonito.create_rate_limit_response — Method
create_rate_limit_response(protected::ProtectedRoute, context) -> HTTP.ResponseCreate an HTTP 429 Too Many Requests response. Renders the ratelimitedhandler App.
Bonito.default_auth_required_page — Method
default_auth_required_page() -> AppDefault 401 Unauthorized page.
Bonito.default_rate_limited_page — Method
default_rate_limited_page() -> AppDefault 429 Too Many Requests page.
Bonito.dependency_path — Method
dependency_path(paths...)Path to serve downloaded dependencies
Bonito.export_standalone — Method
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.generate_salt — Function
generate_salt(length::Int=16) -> Vector{UInt8}Generate a cryptographically secure random salt.
Bonito.generate_state_key — Method
generate_state_key(values)Generate a consistent key for state values that works identically in Julia and JavaScript. Handles Float64 values specially to ensure consistent string representation.
Bonito.get_client_ip — Method
get_client_ip(request::HTTP.Request) -> StringExtract client IP address from request, checking X-Forwarded-For header first.
Bonito.get_user — Method
get_user(store::AbstractPasswordStore, username::String) -> Union{User, Nothing}Retrieve a user by username from the password store. Must be implemented by all AbstractPasswordStore subtypes.
Bonito.getextension — Method
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.handle_app_error! — Function
handle_app_error!(err, app::App, parent_session=nothing)Unified error handling for App rendering errors.
- Logs the error
- Closes the app's session if it was created (prevents waitforready hanging)
- If session was never set, creates a closed dummy session (prevents waitforready hanging)
- Returns error HTML node
- If parent_session is provided and ready, sends error to browser via evaljs
Bonito.is_online — Method
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.jsrender — Method
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_load — Method
on_document_load(session::Session, js::JSCode)executes javascript after document is loaded
Bonito.onload — Method
onload(session::Session, node::Node, func::JSCode)calls javascript func with node, once node has been displayed.
Bonito.page_html — Method
page_html(session::Session, html_body)Embeds the html_body in a standalone html document!
Bonito.pbkdf2_simple — Method
pbkdf2_simple(password::String, salt::Vector{UInt8}; iterations::Int=10_000) -> Vector{UInt8}Simple PBKDF2-HMAC-SHA256 implementation using MbedTLS.
Arguments
password::String: The password to hashsalt::Vector{UInt8}: Random salt (should be at least 16 bytes)iterations::Int: Number of iterations (default: 10,000)
Returns
Vector{UInt8}: Derived key (32 bytes)
This uses MbedTLS's HMAC-SHA256.
Bonito.process_message — Method
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_failed_attempt — Method
record_failed_attempt(protected::ProtectedRoute, client_ip::String)Record a failed authentication attempt for rate limiting.
Bonito.record_states — Method
record_states(session::Session, dom::Hyperscript.Node)Records widget states and their UI updates for offline/static HTML export. This function captures how the UI changes in response to widget interactions, allowing exported HTML to remain interactive without a Julia backend.
How it works
Each widget's states are recorded independently:
- The function finds all widgets in the DOM that implement the widget interface
- For each widget, it records the UI updates triggered by each possible state
- The resulting state map is embedded in the exported HTML
Widget Interface
To make a widget recordable, implement these methods:
is_widget(::YourWidget) = true # Marks the type as a recordable widget
value_range(w::YourWidget) = [...] # Returns all possible states
to_watch(w::YourWidget) = w.observable # Returns the observable to monitorLimitations
- Large file sizes: Recording all states can significantly increase HTML size
- Independent states only: Widgets are recorded independently. Computed observables that depend on multiple widgets won't update correctly in the exported HTML
- Performance: Not optimized for large numbers of widgets or states
Example
# This will work - independent widgets
s = Slider(1:10)
c = Checkbox(true)
record_states(session, DOM.div(s, c))
# This won't fully work - dependent computed observable
combined = map((s,c) -> "Slider: $s, Checkbox: $c", s.value, c.value)
record_states(session, DOM.div(s, c, combined)) # combined won't updateBonito.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
endThe 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
endThe 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_expressions — Method
replace_expressions(markdown, context)Replaces all expressions inside markdown savely, by only supporting getindex/getfield expression that will index into context
Bonito.run_connection_loop — Method
runs the main connection loop for the websocketBonito.set_cleanup_policy! — Method
set_cleanup_policy!(policy::CleanupPolicy)You can set a custom cleanup policy by calling this function.
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.setup_websocket_connection_js — Method
returns the javascript snippet to setup the connectionBonito.string_to_markdown — Function
string_to_markdown(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.send — Method
send(session::Session; attributes...)Send values to the frontend via MsgPack for now
Bonito.HTTPServer.Server — Type
HTTP server with websocket & http routes
Bonito.HTTPServer.Server — Method
Server( dom, url::String, port::Int; verbose = -1 )
Creates an application that manages the global server state!
Bonito.HTTPServer.browser_display — Method
browser_display()Forces Bonito.App to be displayed in a browser window that gets opened.
Bonito.HTTPServer.local_url — Method
local_url(server::Server, url)The local url to reach the server, on the server
Bonito.HTTPServer.online_url — Method
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.tryrun — Method
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.