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! ❤️
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
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));
}
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 browserwindow
itself0
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.
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!
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...
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()
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();
}
}
};
STABLE - used in production
Search or submit any issues for this package
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>
Package sizes (brotli'd, pre-treeshake): ESM: 3.95 KB
Note: @thi.ng/api is in most cases a type-only import (not used at runtime)
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 |
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!
- Karsten Schmidt (Main author)
- Marcus Wågberg
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
}
© 2022 - 2024 Karsten Schmidt // Apache License 2.0