webapi

Moonbit browser WebApi - dom, fetch, html, xhr, url, svg, uievents and more

webapi
dom
fetch
html
xhr
url
svg
uievents
moon add bikallem/webapi@0.5.0
Download zip
Author
Version
0.5.0
License
Apache-2.0
Last updated
4 months ago
Downloads
545

Dependencies

README

#MoonBit WebAPI

Type-safe MoonBit bindings for Web Platform APIs, automatically generated from WebIDL specifications. Targets both JS and wasm-gc backends.

#Table of Contents

#Installation

Add this package to your MoonBit project:

moon add bikallem/webapi@0.4.3

To also install the trim CLI tool (for trimming webapi.mjs in wasm-gc deployments):

moon add bikallem/webapi moon build bikallem/webapi/trim --target native --release # Binary: .mooncakes/bikallem/webapi/_build/native/release/build/trim/trim.exe

#Overview

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.

#Quick Start

#Counter Example

A 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")
.unsafe_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 : Element = document().get_element_by_id("app")
app.append_child(count_display) |> ignore
app.append_child(increment_btn) |> ignore
}

#WebSocket Example

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().unsafe_into()
println("Received: " + data)
})
}

#Canvas Drawing Example

Demonstrates the Canvas 2D API with gradients, shapes, and text:

///|
fn readme_canvas() -> Unit {
let canvas : HTMLCanvasElement = document()
.create_element("canvas")
.unsafe_into()
canvas.set_width(800)
canvas.set_height(500)
let app : Element = document().get_element_by_id("app")
app.append_child(canvas) |> ignore

// Get 2D rendering context
let ctx : CanvasRenderingContext2D = canvas
.get_context("2d")
.unwrap()
.unsafe_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)
}

#Examples

Browser examples demonstrating MoonBit WebAPI bindings. Each example targets both JS and wasm-gc backends. Click an example name to see the live demo.

ExampleDescription
calculatorInteractive calculator with keyboard support
canvas2D canvas drawing with shapes, gradients, and animation
classlistAdd/remove/toggle CSS classes via DOMTokenList
clipboard-apisRead/write clipboard content
consoleConsole API (log, warn, error, table)
counterSimple click counter
domCreate, modify, and remove DOM elements
element-opsInsert, replace, and clone elements
encodingTextEncoder/TextDecoder for UTF-8
eventsMouse, keyboard, and custom event handling
fetchHTTP requests with fetch()
fetch-asyncAsync fetch with JsPromise (JS only)
file-apiFile reading and blob creation
formsForm input handling and validation
fullscreenFullscreen API toggle
geometryDOMRect, DOMMatrix geometry types
indexeddbIndexedDB object store operations
intersection-observerLazy-load with visibility detection
notificationsDesktop notification API
performanceHigh-resolution timing measurements
pointereventsPointer event tracking
resize-observerElement resize monitoring
screen-orientationScreen orientation detection
selection-apiText selection and range handling
storagelocalStorage get/set/remove/clear
streamsReadableStream processing
svgSVG element creation and manipulation
timerssetTimeout and setInterval
todoFull todo app with persistence
touch-eventsMulti-touch gesture handling
urlURL parsing and manipulation
vibrationDevice vibration API
web-animationsKeyframe animations
wc-counterCustom elements with Shadow DOM
wc-edit-wordInline editable text web component
websocketsWebSocket connect/send/receive
xhrXMLHttpRequest

#Running Examples

# Build examples for both targets make build-examples # Generate trimmed webapi.mjs for wasm-gc examples make trim-examples # Serve from the repo root npx serve . # then open http://localhost:3000/examples/index.html

#API Patterns

#Global Objects

The library provides zero-argument accessors for 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()
}

#Type Casting

DOM elements are returned as generic Element types. To cast to specific element types, you have two options:

Returns T? — performs a runtime instanceof check. Returns None if the value is not of the expected type:

///|
fn readme_try_into() -> Unit {
let element = document().create_element("canvas")

// Safe: returns None if the element is not an HTMLCanvasElement
match element.try_into() {
Some(canvas : HTMLCanvasElement) => {
let _ctx : CanvasRenderingContext2D = canvas.get_context("2d").unwrap().unsafe_into()
}
None => println("Not a canvas element")
}
}

#Unchecked downcast with unsafe_into()

Performs no runtime type check. If the underlying JS value is not of type T, this silently produces a value with an incorrect type, which can lead to undefined behavior (wrong methods called, silent data corruption, or runtime crashes):

///|
fn readme_unsafe_into() -> Unit {
// Only use when you are certain of the type (e.g., you just created the element)
let canvas : HTMLCanvasElement = document()
.create_element("canvas")
.unsafe_into()
let _ctx : CanvasRenderingContext2D = canvas
.get_context("2d")
.unwrap()
.unsafe_into()
}

Guideline: Use try_into() when the type is uncertain (e.g., querySelector results, event targets, user-provided elements). Use unsafe_into() when you control the value's origin and know the exact type (e.g., immediately after create_element).

#Nullable Returns and _opt Methods

Methods that return a nullable interface type (e.g., Element?) have two variants:

  • method_name() — Convenience trait method that returns the declared type (e.g., Element), panics if null. Works on all subtypes through trait inheritance.
  • method_name_opt() — Returns T? (Option). Use when you need to handle the None case.

///|
fn readme_opt_methods() -> Unit {
// Convenience: returns Element directly (panics if not found)
let app = document().get_element_by_id("app")

// Cast to a specific subtype with .unsafe_into():
let canvas : HTMLCanvasElement = document()
.get_element_by_id("my-canvas")
.unsafe_into()

// Works on subtypes too (e.g., ShadowRoot inherits query_selector from trait):
// shadow.query_selector("[data-ref=display]")

// _opt variant: returns Option for null-checking
match document().get_element_by_id_opt("maybe-missing") {
Some(el) => el.set_text_content("found")
None => ()
}

ignore(app)
ignore(canvas)
}

#Event Handling

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!") })
}

#Promises

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])
}

#Variadic Arguments

WebIDL variadic parameters map to Array[&TJsValue], accepting any mix of JS-interop types:

///|
fn readme_variadic() -> Unit {
Console::log(["count:", 42, true])
}

#Custom Elements

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.

#Method Chaining

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!")
}

#Optional Parameters

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"),
)
}

#Trimming webapi.mjs for wasm-gc Production

The full src/webapi.mjs JS runtime (~8,400 lines) contains modules for every supported Web API. For wasm-gc deployments, the trim tool produces a minimal version containing only the modules your .wasm binary actually imports — typically 500–1,000 lines (~90% smaller).

Note: This tool is only relevant for the wasm-gc backend. The JS backend does not use webapi.mjs.

#Installing trim

Build the trim CLI from the MoonBit package:

moon add bikallem/webapi moon build bikallem/webapi/trim --target native --release

The binary is at .mooncakes/bikallem/webapi/_build/native/release/build/trim/trim.exe.

#Usage

TRIM=.mooncakes/bikallem/webapi/_build/native/release/build/trim/trim.exe # Trim for a single wasm binary (output: webapi.mjs next to the .wasm file) $TRIM path/to/app.wasm --source path/to/webapi.mjs # Trim with explicit output path $TRIM path/to/app.wasm --source path/to/webapi.mjs -o path/to/output.mjs

When building from source, you can also use the Makefile targets:

# Trim all built wasm-gc examples at once make trim-examples # Trim a single wasm binary make trim WASM=path/to/app.wasm OUT=path/to/output.mjs

#HTML Setup

Point your wasm-gc HTML page at the trimmed file instead of the full bundle:

<script type="module"> import { wasmImportObject } from "./webapi.mjs"; const { instance } = await WebAssembly.instantiateStreaming( fetch("./myapp.wasm"), wasmImportObject, { builtins: ["js-string"], importedStringConstants: "_" } ); instance.exports._start(); </script>

#How It Works

trim parses the wasm binary's import section to find which webapi_ JS modules are referenced, then extracts only those modules (plus the shared wasmImportObject export) from the full webapi.mjs. No runtime behavior changes — just fewer unused modules shipped to the browser.

#WebIDL to MoonBit Conversion

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.

#Type Mappings

WebIDL TypeMoonBit Type
DOMString, USVStringString
booleanBool
long, shortInt
unsigned longUInt
long longInt64
double, floatDouble, Float
any, objectJsValue
sequence<T>Array[T]
Promise<T>JsPromise[T]
T? (nullable)T? (Option)

#Interface Generation

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()
}

#Enum Generation

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
}
}

#Dictionary Generation

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))
}

#Inheritance

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 Element

#Supported Specifications

The 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

#Building from Source

#Prerequisites

  • MoonBit toolchain (moon)
  • Node.js and npm

#Commands

# Install npm dependencies (WebIDL specs) cd src/gen && npm install # Full pipeline: generate, check, format, build, trim, 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 trim-examples # Produce trimmed webapi.mjs per wasm-gc example make test-playwright # Run end-to-end tests

#License

Apache-2.0

⚠️ Package is too large to be displayed

This package contains over 1506 items.

Source Files