Bastion.JS
lib/js.march — Bastion.JS: WASM → JavaScript FFI interface.
Provides typed wrappers for calling JavaScript from WASM island modules. Use this when the built-in Cmd system doesn't cover a specific browser API.
── How it works ──────────────────────────────────────────────────────────────
March islands compile to wasm32-unknown-unknown. JavaScript objects cannot be passed directly across the WASM boundary. Bastion uses an opaque handle table: JS objects are stored in a JS-side registry and identified by integer handles (JsRef values). WASM code passes handles back to JS for further operations.
The March compiler emits import "bastion" "js_*" declarations for these FFI functions, and wasm-bridge.js provides the corresponding imports.
── Usage ──────────────────────────────────────────────────────────────────────
import Bastion.JS as JS
import Bastion.Cmd as Cmd
-- Read the clipboard using raw JS FFI
fn update(state, CopyText) do
let _ = JS.call("navigator.clipboard.writeText", [JS.string_val(state.text)])
(state, Cmd.none())
end
-- Query the DOM and read an attribute
fn update(state, ReadInput) do
let input = JS.query_selector("#search-input")
let value = match input do
Some(el) -> Option.unwrap_or(JS.get_attribute(el, "value"), "")
None -> ""
end
({ state | query = value }, Cmd.none())
end── Note on closures ──────────────────────────────────────────────────────────
JS.add_event_listener accepts a callback function. The callback runs inside the JS event loop and must be a simple function (no captures from the island state) — the March compiler enforces this at the boundary. For reactive updates, prefer data-on-* HTML attributes or Cmd.after/every.
Types
Functions
Add an event listener to a DOM element. The callback receives a JsEvent and returns a message that is dispatched to the island's update function.
Note: the listener persists until the island is destroyed. Use Cmd-based
patterns (data-on-* attributes) for most event handling.
JS.add_event_listener(el, "custom-event", fn ev ->
CustomEvent(JS.to_string(JS.event_detail(ev)) |> Option.unwrap_or(""))
end)Call a JavaScript function by dotted path and return the result as a JsRef.
func_path — dotted path to the function, e.g. "navigator.clipboard.writeText"
args — list of JsRef arguments
Returns JsRef(0) if the function does not exist or throws.
let _ = JS.call("console.log", [JS.string_val("hello from March")])
let result = JS.call("JSON.stringify", [JS.json_val(my_json)])Evaluate a raw JavaScript expression string and return the result.
Use sparingly. Prefer JS.call or JS.global when possible.
let ts = JS.eval("Date.now()")Get the detail property of a CustomEvent.
let detail_str = JS.event_detail(ev) |> JS.to_string()Get the value of an attribute on a DOM element. Returns None if the attribute is absent.
let val = JS.get_attribute(el, "value")
let cls = JS.get_attribute(el, "class")Read a global JavaScript property by dotted path.
let href = JS.global("window.location.href")
let user_agent = JS.global("navigator.userAgent")Wrap a March String as a JSON value for JS.call. Serializes the string as a JSON object, useful for passing complex data.
Query the DOM for the first element matching selector. Returns None if no element matches.
let input = JS.query_selector("#search-input")
let btn = JS.query_selector("button[type=submit]")Remove an attribute from a DOM element.
JS.remove_attribute(el, "disabled")Set the value of an attribute on a DOM element.
JS.set_attribute(el, "class", "active")
JS.set_attribute(el, "disabled", "")Extract a number from a JsRef as an Int. Returns None if not a number.