Skip to content

Semi-curated and generated Moonbit browser WebAPI. Includes APIs - dom, fetch, html, xhr, url, svg, uievents and more.

License

Notifications You must be signed in to change notification settings

bikallem/webapi

Repository files navigation

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

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.

Installation

Add this package to your MoonBit project:

moon add bikallem/webapi@0.3.0

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").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().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").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().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.

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

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

Type Casting with into()

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

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 .into():
  let canvas : HTMLCanvasElement = document
    .get_element_by_id("my-canvas")
    .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 Production

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

Usage

# Trim for a single wasm binary (output: webapi.mjs next to the .wasm file)
make trim WASM=examples/_build/wasm-gc/release/build/counter/counter.wasm

# Trim with explicit output path
make trim WASM=path/to/app.wasm OUT=path/to/output.mjs

# Trim all built examples at once
make trim-examples

HTML Setup

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

<script type="module">
    import { wasmImportObject } from "./_build/wasm-gc/release/build/myapp/webapi.mjs";

    const { instance } = await WebAssembly.instantiateStreaming(
        fetch("./_build/wasm-gc/release/build/myapp/myapp.wasm"),
        wasmImportObject,
        { builtins: ["js-string"], importedStringConstants: "_" }
    );
    instance.exports._start();
</script>

How It Works

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

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

About

Semi-curated and generated Moonbit browser WebAPI. Includes APIs - dom, fetch, html, xhr, url, svg, uievents and more.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors