JavaScript DOM API

JavaScript bindings — schema methods and the @read / @write / @validate / @computed resolvers declared as JavaScript modules — can traverse and build markup with a WHATWG-style DOM API. It is exposed through the ambient document global and through node/element handles.

This is a subset chosen to make isomorphic (server and browser) bindings writable; anything not listed in these pages is not available server-side. Each interface is documented on its own page:

Interfaces and instanceof

The WHATWG node interfaces exist as real classes, with working instanceof and constructor.name, in the standard hierarchy:

So p instanceof Element, p instanceof Node, and textNode instanceof CharacterData all hold — a binding can branch on node kind the same way it would in the browser. HTML elements (HTML-parsed or createElement) are HTMLElement (so htmlEl instanceof HTMLElement, and htmlEl.constructor.name is "HTMLElement"); elements in another namespace (createElementNS, e.g. SVG) are plain Element. Members are interface-scoped: reaching for a member that is not part of a node's interface yields undefined (for example textNode.tagName and element.data are both undefined) rather than throwing. Within a node's own interface, an accessor with no value returns null (for example element.parentNode on a detached element).

Per-tag element interfaces. Every non-deprecated HTML tag that has its own dedicated interface in the WHATWG spec has one here too, below HTMLElement — an <a> is an HTMLAnchorElement, an <input> an HTMLInputElement, and so on (a instanceof HTMLAnchorElement, a.constructor.name === "HTMLAnchorElement"; and the whole chain up to Node holds). Each has its own reference page listing its reflected IDL attributes (a.href, img.src, input.required, …) — see the Element interfaces index for the full list of interfaces and their pages. A handful of obsolete WHATWG §16.3 elements (<font>, <marquee>, …) also have their interface, marked deprecated on their page. An HTML tag with no dedicated interface — either because the spec never gave it one (<section>, <article>, …) or it isn't yet covered — is a plain HTMLElement with no reflected properties beyond HTMLElement's own (so section.href is undefined); use getAttribute / setAttribute there. A single flattened node still backs every node internally — the interfaces are a prototype layer over it.

Availability and the read-only boundary

The ambient document global is present whenever the binding runs against a document. Whether it is writable depends on the binding slot:

Binding slot Ambient document
A default resolver (the value materialised for a new instance) Writable
@read, @write, @validate, @computed; schema methods; trigger when/action Read-only tree, writable construction
A document you create yourself with new DOMParser().parseFromString(…) Writable

A read-only ambient document protects the existing tree: mutating a node that is already in the document — setting an attribute, changing text, inserting or removing a child — throws a NoModificationAllowedError (message: "document is read-only in this binding context"). Read-only membership is a property of the tree as it stood when the binding started, not of how you reached a node: every node that was in the document is read-only through any path (document.body, querySelector, parentNode, a NodeList, classList), so a node cannot be laundered into a writable handle by reaching it a different way.

Constructing new nodes is always allowed, even against a read-only document. createElement, createElementNS, createTextNode, createComment, createDocumentFragment, and cloneNode return fresh, writable detached nodes; building a subtree from them and returning it works with the standard DOM API:

export default (title) => {
  const li = document.createElement("li");
  li.textContent = title;
  return li;   // return an element — see "Returning DOM"
};

The one thing you cannot do is move a node out of the read-only tree into your constructed subtree — that would mutate the tree it belongs to, so card.appendChild(document.querySelector("li")) throws NoModificationAllowedError. To reuse existing content, copy it with cloneNode(true), which yields a writable copy you can attach freely:

const copy = document.querySelector("li").cloneNode(true);
card.appendChild(copy);   // fine — a clone is a fresh, writable node

Mutations to a writable ambient document (a default resolver, or a document you parsed yourself) are serialised back and persisted after the binding returns.

Returning DOM from a binding

The value a binding returns is spliced into the composed document:

Returned value Result
An element node Serialised and spliced in place.
A NodeList or array of elements Each element serialised and spliced, in order.
A non-element node (text, comment, document, fragment) An error — the message tells you the fix, e.g. return document.documentElement instead of the document, or use .textContent instead of a text node.

Errors

Thrown errors are DOMException instances with the standard name/code/message. new DOMException(message?, name?) is available, and the full legacy name → code table is supported — see the DOMException reference. The names actually raised by this API:

Name Raised when
SyntaxError Invalid CSS selector; invalid insertAdjacentHTML position.
HierarchyRequestError A tree mutation would create a cycle.
NotFoundError removeChild / insertBefore / replaceChild reference is not a child.
NamespaceError Malformed qualified name in createElementNS / setAttributeNS.
NoModificationAllowedError Mutating a node already in a read-only tree, or moving such a node into a constructed subtree (copy it with cloneNode(true) instead); outerHTML set on a parentless element.

Divergences from the browser DOM

See also