The document, as a package — the same surface slate:dom has,
written in slate on top of external.
slate install github.com/slate-language/dom
import { byId, setText, on } from dom
Forty-four names, and a consumer migrates by changing one import line. slate:dom is thirteen
hand-written builtins in the compiler's JavaScript back end, and while they lived there every name a
page wanted cost a language release: focus, blur and activeElement waited for 0.0.37 and
dispatch and observe waited again. external — slate 0.0.38 — is the door the compiler grew so
that this could be ordinary slate. A name added here ships the moment this package does.
import { byId, setText, on } from slate:dom // before
import { byId, setText, on } from dom // after
A node IS the external. The element itself, not an index into a table the engine keeps. That is
a reversal of what the built-in module chose, and external is what makes it safe: an external is
opaque, it cannot be printed as anything but <external HTMLDivElement>, it cannot be added to or
iterated, and it matches no pattern but a bare name — so a host object reaching a program is already
the thing the handle table was protecting the program from. == on two nodes is identity, which is
what the integer handle promised and what external promises directly.
release is exported and does nothing. There is no handle table and therefore nothing to give
back; the element stays exactly where it is in the page, which is what release always did. It stays
on the export list so that a consumer written against the built-in module — lath calls it in two
places — compiles and runs unchanged.
An event arrives as a record of ten fields, built here. { type, value, checked, key, mods, button, stop, prevent, target, closest }, where mods is { meta, ctrl, shift, alt } and button
is an integer or null. Handing the host's event over as an external would put a value in a
program's hands that it could not print, compare or store, and would make every handler in every
consumer read host properties instead of slate fields. mods and button are there because a link
cannot be written without them: a framework that intercepts a click has to let a cmd-click, a
ctrl-click, a shift-click and a middle click through to the browser.
target is the node the event originated on, the same kind of node value every other name in
this package hands out — not an external kept apart as a special case. It is what a handler installed
on a parent for several children (a menu, a tab list) reads to learn which child was hit.
closest(selector) answers the nearest ancestor-or-self of target matching selector, or null
when none does — Element.closest, read off target after climbing to its parent element where
target is a text node, which has no closest of its own.
off needs the same JavaScript function on handed over, and slate cannot name it, so the
package keeps its own map. A slate closure crosses outward as a fresh JavaScript function every
time it crosses, so the value removeEventListener would be given is never the value
addEventListener was given. The map is keyed by the slate closure itself — slate compares a
function by identity, the rule it states for a set, a map, a promise and a function alike — and what
it holds is an AbortController, whose signal on passes to addEventListener and whose abort()
is what off calls. That is the DOM's own way of removing a listener without naming the function
again.
Everything is reached through one declaration, globalThis. A declaration is eager and total:
the path is walked where it stands, and one that names nothing is a fault quoting it — so
external document at the top of the file would take the whole module with it on a host that has no
document, which is exactly where a server-rendering framework imports this and then guards its calls
with host(). slate:dom's rule is that the module still exists and the import still succeeds,
because a refusal at the import is a complaint about the module and sends a reader looking for a
spelling mistake in the one line that is right. A property read off globalThis answers null for a
name that is not there, which is what keeps that rule true here — and it is what keeps the
interpreter's rule true as well, the declaration itself touching no host.
Navigation goes through window.history and a popstate listener. location() is a record read
off the window; pushPath and replacePath are pushState and replaceState; back and forward
are the history's own. A push does not raise onNavigate, which is the browser's rule — the event
is for a movement the user made — so a router renders after its own push and waits to be told about
everything else. Getting that wrong is how a router renders twice. There is no state object: the url
is the whole of the state.
Under the interpreter every declaration succeeds and only a host operation faults. The package needs no special casing for it and has none.
Every one is slate:dom's, spelled the same and taking the same arguments. Where the meaning
differs, it says so.
createElement(tag), createText(s), createComment(s) |
|
setAttribute, removeAttribute, setProperty |
|
on(node, event, fn), off(node, event, fn) |
off takes the function, not an id — which is what the built-in does and what its own page's table gets wrong |
setChildren(node, kids), setText(node, s) |
|
insertBefore(parent, node, before) |
one child, in front of another; null appends |
removeChild(parent, node) |
one child, out |
byId(id), query(selector) |
|
children(node) |
the child nodes, in order — every one, not only the elements |
tagName(node) |
the tag in lower case, or null for a text node |
nodeText(node) |
what the node says, as text |
attribute(node, name) |
one attribute, or null; true for a bare one |
nodeKind(node) |
"element", "text", "comment", "fragment", "document" or "other" |
property(node, name) |
what the node HOLDS, or null |
splitText(node, at) |
cut a text node in two, in characters; answers the tail |
markup(node) |
the node's outer markup, as a string |
release(node) |
a no-op — there is no handle to give back |
dispatch(node, event) |
send an event; answers whether nothing cancelled it |
observe(node, options, fn) |
be told what changed; answers { disconnect } |
events(url, options) |
read a server's event stream; answers { close } — and says which name the host has not, where it has no EventSource |
focus(node), blur(node) |
move the caret to a node, and take it away |
activeElement() |
the node holding focus, or null for nothing (and for the body) |
location() |
where the page is, as a record |
pushPath(url), replacePath(url) |
move the address bar without a reload |
back(), forward() |
move through what the page has visited |
onNavigate(fn) |
the user moved — not a push the program made |
stored(key), store(key, v) |
localStorage, as results |
unstore(key), storedKeys(), clearStored() |
|
cookies(), cookie(name) |
what the document is carrying, as an object or one value |
setCookie(name, v, options = {}), deleteCookie(name, options = {}) |
- A node prints as
<external HTMLDivElement>where the built-in module printed a number. Nothing else about a node changes:==is identity, it travels through an array, a field and aMapvalue like anything else, andx is externalis the type test. - A node is a key, which every consumer needs: a
Map, aSetand an object settle a key by hashing it and then comparing, and an external hashes by the host value it holds rather than by the wrapper it crossed in — so two reads of one element are==, hash together, and find each other's entry. This module keeps a listener'sAbortControllerunder one, and lath keeps what it last set on each node. - A node given a listener is held until
offtakes it back. The built-in module keeps its listeners in aWeakMapand this one keeps them in an ordinaryMap, there being no weak map in the language — so a page that puts a handler on a node it later drops takes the handler off with it.offempties every level of the table it leaves empty. - The interpreter's refusal names the host operation — calling
createElementon an external reaches the JavaScript host, and the interpreter has no JavaScript host — where the built-in module names the slate command. For most of these the host's spelling is the command; where it is not,byIdreachinggetElementById, the reader is still sent to the right place, which is the way the program was run.
slate test tests
slate test --js tests
npm install
NODE_OPTIONS="--import ./tests-dom/setup.mjs" slate test --js tests-dom
The first two are the same suite on two hosts with no page, and they say the module imports where
there is no document at all, that every name is there, that the export list is slate:dom's name for
name, and that calling one refuses rather than answering something plausible. The interpreter runs all
48; a JavaScript host with no page runs the 32 that reach the document, the window or the store before
they reach a node, and leaves the other 16 out — there being nothing on that host that reads as a node.
The last is the one that says what the names DO — 82 tests against a real
jsdom document, tests-dom/dom.slx for what a node is and what
happens to one and tests-dom/doors.slx for where the page is, where it has been, what it remembers
and what it is carrying. jsdom is a dev dependency of this repository and of nothing else; a program
that uses this package never sees npm.
Run it with no NODE_OPTIONS and it skips rather than fails, which is the cheap check that the
module still loads on a host with no document — what the one external globalThis buys, and what an
external document at the top of the file would take away.
jsdom rather than a fake document written beside the code it checks. A shim written in the
harness would agree with dom.slx by construction: every mistake this package could make about what
setAttribute, replaceChildren or addEventListener do, the shim would make too, and the run
would pass.
slate 0.0.40 or newer, and nothing else. Three things make the floor. external itself, which is
what every function here is built on and which arrived in 0.0.38 — an older compiler fails at the
first declaration. An external hashing by the host value it holds, which arrived in 0.0.39: the
listener table is keyed by the node, so on 0.0.38 off would find nothing for a node read a second
time. And assertFaults no longer panicking the interpreter over a callback that faults below its
own frame, which arrived in 0.0.40 and is what this package's own test suite relies on. A manifest
has no key for a compiler floor — the reader takes name, version, main, modules,
dependencies and devDependencies and names anything else — so this paragraph is where it is
written down.
ISC.