Skip to content

Latest commit

 

History

History

wasm-api-dom

@thi.ng/wasm-api-dom

npm version npm downloads Mastodon Follow

Note

This is one of 199 standalone projects, maintained as part of the @thi.ng/umbrella monorepo and anti-framework.

🚀 Please help me to work full-time on these projects by sponsoring me on GitHub. Thank you! ❤️

About

Browser DOM bridge API for hybrid TypeScript & WASM (Zig) applications. This is a support package for @thi.ng/wasm-api.

This package provides a small TypeScript core API and related Ziglang bindings for UI & DOM creation/manipulation via WebAssembly.

Current key features for the Zig (WASM) side:

  • Fully declarative or imperative DOM tree creation & manipulation
  • ID handle management for WASM created DOM elements & listeners
  • Canvas element creation (with HDPI support, see @thi.ng/adapt-dpi)
  • Attribute setters/getters (string, numeric, boolean)
  • .innerHTML & .innerText setters
  • Event handlers, event types (see generated types in api.zig for details):
    • drag 'n drop (WIP)
    • focus
    • input
    • key
    • mouse
    • pointer
    • scroll
    • touch
    • wheel
  • Fullscreen API wrapper
  • (Browser) window info queries

Module initialization

Before the Zig WASM API module can be used, it must be initialized (automatically or manually) with a standard std.mem.Allocator. The current recommended pattern looks something like this:

const std = @import("std");
const wasm = @import("wasm-api");
const dom = @import("wasm-api-dom");

// expose thi.ng/wasm-api core API (incl. panic handler & allocation fns)
pub usingnamespace wasm;

// allocator, also exposed & used by JS-side WasmBridge & DOM module
// see further comments in:
// https://github.com/thi-ng/umbrella/blob/develop/packages/wasm-api/zig/lib.zig
// https://github.com/thi-ng/umbrella/blob/develop/packages/wasm-api-dom/zig/events.zig
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
pub const WASM_ALLOCATOR = gpa.allocator();

/// Since various initialization functions can return errors
/// we're bundling them all in a single fn, which is then called by start()
/// and so only needs one code site for error handling
fn init() !void {
    // all WASM API modules auto-initialize themselves if the root source
    // file exposes a `WASM_ALLOCATOR`, otherwise you'll have to initialize manually:
    // try dom.init(customAllocator);

    // ...
}

/// Main entry point
export fn start() void {
    init() catch |e| @panic(@errorName(e));
}

ID handles

Since DOM related resources created on the JS side cannot be returned to the WASM module directly, the bridge API caches those on the host side and uses managed ID (integer) handles to exchange them. These IDs can then be used in subsequent API calls to refer to certain DOM elements, listeners etc.

For element & event related functionality the following IDs are reserved:

  • -1 the browser window itself
  • 0 document.head
  • 1 document.body

These are exposed in the Zig module as window, head, body constants to help avoiding magic numbers in userland code.

DOM tree creation

Single DOM elements and entire element trees (incl. event handler setup and custom attributes) can be created via the createElement() function.

Attribute definitions need to be wrapped using dom.attribs() and child elements via dom.children(), as shown here:

const wasm = @import("wasm-api");
const dom = @import("wasm-api-dom");

const Attrib = dom.Attrib;

// snippet taken from the zig-todo-list example project

const handle = dom.createElement(&.{
    // element name
    .tag = "div",
    // CSS classes
    .class = "flex flex-column mb3",
    // nested child elements
    .children = dom.children(&.{
        .{ .tag = "h3", .text = "Add new task" },
        .{
            .tag = "input",
            // element's ID attribute
            .id = "newtask",
            // attribute & event listener definitions
            .attribs = dom.attribs(&.{
                Attrib.string("placeholder", "What needs to be done?"),
                Attrib.flag("autofocus", true),
                // event listener setup:
                // last arg is optional opaque pointer to arbitrary user state/context
                Attrib.event("keydown", onKeydown, &STATE),
                Attrib.event("input", onInput, null),
            }),
        },
        .{
            .tag = "button",
            // Element .innerText content
            .text = "Add Task",
            .attribs = dom.attribs(&.{
                Attrib.event("click", onAddTask, null),
            }),
        },
    }),
});

fn onInput(e: *const dom.Event, _: ?*anyopaque) callconv(.C) void {
    wasm.printStr(e.body.input.getValue());
}

The CreateElementOpts struct has some additional options and more are planned. All WIP!

HTML canvas support

A separate function (createCanvas()) with similar options exist for <canvas> elements.

Also see the thi.ng/wasm-api-canvas sibling package for working with canvases...

Attribute creation & accessors

As already shown above, attributes can be provided as part of the CreateElementOpts and/or accessed imperatively:

Zig example:

// creating & configuring an <input type="range"> element
_ = dom.createElement(&.{
    .tag = "input",
    .parent = dom.body,
    // optional attrib declarations
    .attribs = dom.attribs(&.{
        // string attrib
        dom.Attrib.string("type", "range"),
        // numeric attribs
        dom.Attrib.number("min", 0),
        dom.Attrib.number("max", 100),
        dom.Attrib.number("step", 10),
        dom.Attrib.number("value", 20),
        // boolean attrib (only will be created if true)
        dom.Attrib.flag("disabled", true),
    }),
});

The following accessors are provided (see /zig/lib.zig for documentation):

  • getStringAttrib() / setStringAttrib()
  • getNumericAttrib() / setNumericAttrib()
  • getBooleanAttrib() / setBooleanAttrib()

Event listeners

Once a DOM element has been created, event listeners can be attached to it. All listeners take two arguments: an Event struct and an optional opaque pointer for passing arbitrary user context.

A more advanced version of the following click counter button component can be seen in action in the zig-counter example project. Also check other supplied Zig examples for more realworld usage examples.

IMPORTANT: In Zig v0.12+ all event handlers must explicitly specify callconv(.C) See docs for more reference.

const wasm = @import("wasm-api");
const dom = @import("wasm-api-dom");

/// Simple click counter component
const Counter = struct {
    elementID: i32,
    clicks: usize,
    step: usize,

    const Self = @This();

    /// Initialize internal state & DOM element w/ listener
    pub fn init(self: *Self, parent: i32, step: usize) !void {
        self.clicks = 0;
        self.step = step;
        // create DOM button element
        self.elementID = dom.createElement(&.{
            .tag = "button",
            // Tachyons CSS class names
            .class = "db w5 ma2 pa2 tc bn",
            .text = "click me!",
            .parent = parent,
            .attribs = dom.attribs(&.{
                // define & add click event listener w/ user context arg
                dom.Attrib.event("click", onClick, self),
            }),
        });
    }

    fn update(self: *const Self) void {
        // format new button label
        var buf: [32]u8 = undefined;
        var label = std.fmt.bufPrintZ(&buf, "clicks: {d:0>4}", .{self.clicks}) catch return;
        // update DOM element
        dom.setInnerText(self.elementID, label);
    }

    /// event listener & state update
    fn onClick(_: *const dom.Event, raw: ?*anyopaque) callconv(.C) void {
        // safely cast raw pointer
        if (wasm.ptrCast(*Self, raw)) |self| {
            self.clicks += self.step;
            self.update();
        }
    }
};

Status

STABLE - used in production

Search or submit any issues for this package

Installation

yarn add @thi.ng/wasm-api-dom

ESM import:

import * as wad from "@thi.ng/wasm-api-dom";

Browser ESM import:

<script type="module" src="https://esm.run/@thi.ng/wasm-api-dom"></script>

JSDelivr documentation

Package sizes (brotli'd, pre-treeshake): ESM: 3.95 KB

Dependencies

Note: @thi.ng/api is in most cases a type-only import (not used at runtime)

Usage examples

Five projects in this repo's /examples directory are using this package:

Screenshot Description Live demo Source
Zig-based DOM creation & canvas drawing app Demo Source
Zig-based 2D multi-behavior cellular automata Demo Source
Simple Zig/WASM click counter DOM component Demo Source
Zig-based To-Do list, DOM creation, local storage task persistence Demo Source
Basic Zig/WebAssembly WebGL demo Demo Source

API

Generated API docs

For now, please see the package docs, source code comments (TS & Zig) and the various comments in the above linked example projects for further reference and usage patterns! Thank you!

Authors

If this project contributes to an academic publication, please cite it as:

@misc{thing-wasm-api-dom,
  title = "@thi.ng/wasm-api-dom",
  author = "Karsten Schmidt and others",
  note = "https://thi.ng/wasm-api-dom",
  year = 2022
}

License

© 2022 - 2024 Karsten Schmidt // Apache License 2.0