Type-safe MoonBit bindings for Web Platform APIs, automatically generated from WebIDL specifications. Targets both JS and wasm-gc backends.
- Overview
- Installation
- Quick Start
- Examples
- API Patterns
- WebIDL to MoonBit Conversion
- Supported Specifications
- Building from Source
- License
This library provides MoonBit FFI bindings for browser APIs including:
- DOM - Document Object Model manipulation
- HTML - HTML elements, forms, media, and attributes
- Canvas 2D - Graphics and drawing operations
- Events - Mouse, keyboard, pointer, touch, and custom events
- Fetch - HTTP requests and responses
- URL - URL parsing and manipulation
- WebSocket - Real-time bidirectional communication
- Storage - localStorage and sessionStorage
- SVG - Scalable Vector Graphics
- CSSOM - CSS Object Model and viewport queries
- Web Animations - Keyframe animations API
- IndexedDB - Client-side structured storage
- Streams - Readable/writable streams
- Notifications - Desktop notifications
- File API - File reading and blob handling
- Clipboard - Clipboard read/write access
- Intersection Observer - Visibility detection
- Resize Observer - Element size monitoring
- Performance - High-resolution timing
All bindings are automatically generated from official WebIDL specifications, ensuring type safety and API completeness.
Add this package to your MoonBit project:
moon add bikallem/webapi@0.3.0A simple counter application demonstrating DOM manipulation and event handling:
///|
fn readme_counter() -> Unit {
let mut count = 0
// Create count display element
let count_display : HTMLDivElement = document.create_element("div").into()
count_display
..set_attribute("id", "count-display")
.set_attribute("style", "font-size: 3em; margin: 0.5em 0;")
count_display.set_text_content("0")
fn update_display() {
count_display.set_text_content(count.to_string())
}
// Create increment button — closures are accepted directly
let increment_btn = document.create_element("button")
increment_btn.set_text_content("+")
increment_btn.add_event_listener("click", fn(_event) {
count = count + 1
update_display()
})
// Append to DOM
let app = document.get_element_by_id("app").unwrap()
app.append_child(count_display) |> ignore
app.append_child(increment_btn) |> ignore
}Demonstrates WebSocket connections with event handler closures:
///|
fn readme_websocket() -> Unit {
let socket = WebSocket::new("wss://echo.websocket.events")
// Event handlers accept closures directly
socket.set_onopen(fn(_e) { socket.send("Hello from MoonBit!") })
socket.set_onmessage(fn(e) {
let data : String = e.data().into()
println("Received: " + data)
})
}Demonstrates the Canvas 2D API with gradients, shapes, and text:
///|
fn readme_canvas() -> Unit {
let canvas : HTMLCanvasElement = document.create_element("canvas").into()
canvas.set_width(800)
canvas.set_height(500)
document.get_element_by_id("app").unwrap().append_child(canvas) |> ignore
// Get 2D rendering context
let ctx : CanvasRenderingContext2D = canvas.get_context("2d").unwrap().into()
// Create gradient and draw
let gradient = ctx.create_linear_gradient(0.0, 0.0, 0.0, 300.0)
gradient.add_color_stop(0.0, "#1e3c72")
gradient.add_color_stop(1.0, "#87CEEB")
ctx.set_fill_style(gradient)
ctx.fill_rect(0.0, 0.0, 800.0, 300.0)
// Draw text
ctx.set_font("bold 24px sans-serif")
ctx.set_fill_style("#FFFFFF")
ctx.fill_text("Hello, MoonBit!", 320.0, 400.0)
}Browser examples demonstrating MoonBit WebAPI bindings. Each example targets both JS and wasm-gc backends. Click an example name to see the live demo.
| Example | Description |
|---|---|
| calculator | Interactive calculator with keyboard support |
| canvas | 2D canvas drawing with shapes, gradients, and animation |
| classlist | Add/remove/toggle CSS classes via DOMTokenList |
| clipboard-apis | Read/write clipboard content |
| console | Console API (log, warn, error, table) |
| counter | Simple click counter |
| dom | Create, modify, and remove DOM elements |
| element-ops | Insert, replace, and clone elements |
| encoding | TextEncoder/TextDecoder for UTF-8 |
| events | Mouse, keyboard, and custom event handling |
| fetch | HTTP requests with fetch() |
| fetch-async | Async fetch with JsPromise (JS only) |
| file-api | File reading and blob creation |
| forms | Form input handling and validation |
| fullscreen | Fullscreen API toggle |
| geometry | DOMRect, DOMMatrix geometry types |
| indexeddb | IndexedDB object store operations |
| intersection-observer | Lazy-load with visibility detection |
| notifications | Desktop notification API |
| performance | High-resolution timing measurements |
| pointerevents | Pointer event tracking |
| resize-observer | Element resize monitoring |
| screen-orientation | Screen orientation detection |
| selection-api | Text selection and range handling |
| storage | localStorage get/set/remove/clear |
| streams | ReadableStream processing |
| svg | SVG element creation and manipulation |
| timers | setTimeout and setInterval |
| todo | Full todo app with persistence |
| touch-events | Multi-touch gesture handling |
| url | URL parsing and manipulation |
| vibration | Device vibration API |
| web-animations | Keyframe animations |
| wc-counter | Custom elements with Shadow DOM |
| wc-edit-word | Inline editable text web component |
| websockets | WebSocket connect/send/receive |
| xhr | XMLHttpRequest |
# Build examples for both targets
cd examples && moon build --target js --release
cd examples && moon build --target wasm-gc --release
# Serve from the repo root
npx serve .
# then open http://localhost:3000/examples/index.htmlThe library provides direct access to browser global objects:
///|
fn readme_globals() -> Unit {
// Access the document object
let _ = document.get_element_by_id("my-id")
// Access the window object
let _ = window.inner_width()
// Access the navigator object
let _ = navigator.user_agent()
}DOM elements are returned as generic Element types. Use into() to cast to specific element types:
///|
fn readme_casting() -> Unit {
// Create an element and cast to specific type
let canvas : HTMLCanvasElement = document.create_element("canvas").into()
// Cast to access type-specific methods
let _ctx : CanvasRenderingContext2D = canvas.get_context("2d").unwrap().into()
}Event listeners and handlers accept closures directly:
///|
fn readme_events() -> Unit {
let element = document.create_element("button")
// addEventListener with closure
element.add_event_listener("click", fn(_event) { println("Clicked!") })
}Use JsPromise to chain async operations like fetch():
///|
fn readme_promises() -> Unit {
window
.fetch("https://api.example.com/data")
.then(fn(response : Response) {
response.text().then(fn(text : String) { Console::log([text]) }) |> ignore
})
.catch_(fn(_err) { Console::error(["Fetch failed"]) })
|> ignore
}On the JS backend, the bikallem/webapi/js_promise subpackage bridges JsPromise to MoonBit's async via to_async_promise():
///|
async fn readme_fetch_async(url : String) -> Unit {
let response : Response = @js_promise.to_async_promise(window.fetch(url)).wait()
let text : String = @js_promise.to_async_promise(response.text()).wait()
Console::log([text])
}WebIDL variadic parameters map to Array[&TJsValue], accepting any mix of JS-interop types:
///|
fn readme_variadic() -> Unit {
Console::log(["count:", 42, true])
}Register Web Components with define_custom_element. The on_create callback runs once per element — attach Shadow DOM, build the template, and wire events here:
///|
fn readme_custom_element() -> Unit {
define_custom_element("my-greeting", fn(host) {
let shadow = host.attach_shadow(ShadowRootInit::new(ShadowRootMode::Open))
shadow.set_inner_html("<p>Hello from Shadow DOM!</p>")
})
}Optional lifecycle callbacks are available for on_connected, on_disconnected, on_adopted, and on_attribute_changed.
Most setter methods return Unit, enabling method chaining with .. (use . for the last call in the chain):
///|
fn readme_chaining() -> Unit {
let element = document.create_element("div")
element..set_attribute("id", "my-element").set_attribute("class", "container")
element.set_text_content("Hello!")
}Many methods have optional parameters using MoonBit's ? syntax:
fn readme_optional() -> Unit {
// With default options
let _ = document.create_element("div")
// With explicit options
let _ = document.create_element(
"div",
options=ElementCreationOptions::new(is="custom-div"),
)
}This library is automatically generated from WebIDL specifications using a custom code generator written in MoonBit. The generator transforms WebIDL definitions into idiomatic MoonBit code.
| WebIDL Type | MoonBit Type |
|---|---|
DOMString, USVString |
String |
boolean |
Bool |
long, short |
Int |
unsigned long |
UInt |
long long |
Int64 |
double, float |
Double, Float |
any, object |
JsValue |
sequence<T> |
Array[T] |
Promise<T> |
JsPromise[T] |
T? (nullable) |
T? (Option) |
WebIDL interfaces are converted to MoonBit traits and external types:
WebIDL:
interface Element : Node {
attribute DOMString id;
DOMString? getAttribute(DOMString name);
undefined setAttribute(DOMString name, DOMString value);
};Generated MoonBit:
///|
#external
pub type Element
///|
pub trait TElement: TNode {
id(self : Self) -> String = _
set_id(self : Self, id : String) -> Unit = _
get_attribute(self : Self, name : String) -> String? = _
set_attribute(self : Self, name : String, value : String) -> Unit = _
}
///|
impl TElement with get_attribute(self : Self, name : String) -> String? {
element_get_attribute_ffi(TJsValue::to_js(self), TJsValue::to_js(name)).to_option()
}WebIDL enums become MoonBit enums with string conversion:
WebIDL:
enum ShadowRootMode { "open", "closed" };Generated MoonBit:
///|
pub(all) enum ShadowRootMode {
Open
Closed
} derive(Eq, Show)
///|
pub impl TJsValue for ShadowRootMode with to_js(self : ShadowRootMode) -> JsValue {
match self {
ShadowRootMode::Open => TJsValue::to_js("open")
ShadowRootMode::Closed => TJsValue::to_js("closed")
}
}
///|
pub fn ShadowRootMode::from(value : String) -> ShadowRootMode? {
match value {
"open" => Some(ShadowRootMode::Open)
"closed" => Some(ShadowRootMode::Closed)
_ => None
}
}WebIDL dictionaries become constructor functions:
WebIDL:
dictionary EventInit {
boolean bubbles = false;
boolean cancelable = false;
};Generated MoonBit:
///|
#external
pub type EventInit
///|
pub fn EventInit::new(bubbles? : Bool, cancelable? : Bool) -> EventInit {
event_init_ffi(opt_to_js(bubbles), opt_to_js(cancelable))
}Interface inheritance is modeled using trait bounds:
// Element extends Node, which extends EventTarget
pub trait TElement: TNode { ... }
pub trait TNode: TEventTarget { ... }
pub trait TEventTarget { ... }
// Implementations chain correctly
pub impl TElement for Element
pub impl TNode for Element
pub impl TEventTarget for ElementThe generator processes the following WebIDL specifications:
clipboard-apis, console, cssom, cssom-view, dom, encoding, fetch, FileAPI, fullscreen, geometry, hr-time, html, IndexedDB, intersection-observer, notifications, performance-timeline, pointerevents, referrer-policy, requestidlecallback, resize-observer, screen-orientation, selection-api, storage, streams, SVG, touch-events, trusted-types, uievents, url, vibration, web-animations, webidl, websockets, xhr
- MoonBit toolchain (
moon) - Node.js and npm
# Install npm dependencies (WebIDL specs)
cd webapi_gen && npm install
# Full pipeline: generate, check, format, build, test
make clean all
# Or individual steps:
make gen-test # Run generator tests
make clean gen # Regenerate bindings
make check # Type-check both JS and wasm-gc targets
make fmt # Format all code
make build-examples # Build examples for both targets
make test-playwright # Run end-to-end testsApache-2.0