@sprs/dom
@sprs/dom
is a minimal library for dealing with DOM elements.
The feature set aims to be intentional, pointed, and free from unnecessary complexity and mental overhead.
It integrates directly with @sprs/state
, which provides primitives for reactive states.
Utilities
render
The render
function accepts a single parameter, which may be:
- a primitive
- a DOM node
- a
NodeProducer
(see below) - a reactive state from
@sprs/state
- an array of any of these items
The output of render
is a DocumentFragment
containing the rendered DOM nodes. Use HTMLElement.appendChild
to add the resulting nodes to the DOM at runtime.
Example:
import { render, NodeProducer } from '@sprs/dom';
/*
* Trivial example class to show that `NodeProducer` is
* understood by `render`.
*/
class RedDiv extends NodeProducer {
constructor() {
this.div = document.createElement('div');
this.div.style.backgroundColor = 'red';
}
getNode() {
return this.div;
}
}
// Make a small fragment
const fragment = render([
document.createElement('body'),
[
new RedDiv(),
123e2,
'a string',
true,
],
]);
// Attach it to the body of the page
document.querySelector('body').appendChild(fragment);
NodeProducer
NodeProducer
is a base class which serves as a hook into render
. It contains one method: getNode(): Node
, which render
will use to delegate the responsibility of getting or generating DOM elements.
Example:
import { NodeProducer } from '@sprs/dom';
class MakesDivsOnTheFly extends NodeProducer {
getNode() {
return document.createElement('div');
}
}
const docFragment = render(new MakesDivsOnTheFly());
This is mostly useful for building structures that add extra functionality to an element in the DOM by wrapping it instead of adding new properties to it.
A real-life example of this is the Peg
class from this package.
html
, svg
, and mathml
The html
export is a thin wrapper around document.createElement
which supports inline attribute, listener, and child assignment. It just returns DOM nodes, and so does not require render
except when combined with other types of renderable structures.
This is useful for expressive construction of DOM elements in JS. It is quite similar to HyperScript, with the exception that it does a little bit less (in a good way, I think).
html
may be invoked in two basic forms:
Form 1: Creating elements from scratch
Pass a tag name as a string as the first parameter to create an element of that type
const myDiv = html('div');
Form 2: Creating elements using other elements as a template
Pass an existing element instead of a tag name as the first parameter to deeply clone that element, assign new properties to it, and append new children to it.
const myBlueDiv = html('div', { style: { backgroundColor: 'blue' });
const myBlueDivWithPadding = html(myBlueDiv, { style: { padding: '6px' } });
myBlueDiv === myBlueDivWithPadding; // -> false
myBlueDiv.style.backgroundColor = 'red';
myBlueDivWithPadding.style.backgroundColor; // -> 'blue'
The options object, which may be specified as the second parameter, contains attribute names and values to assign to the resulting element. Any attribute may be assigned a state from @sprs/state
and it will be automatically bound to that state value.
There are two keys available on the options object which are treated specially.
style
accepts either a string, which sets the styles wholesale, overwriting any existing styles, or an object with the same properties as a CSSStyleDeclaration
.
const myDiv1 = html('div', { style: 'background-color: blue; padding: 6px;' });
const myDiv2 = html('div', { style: { backgroundColor: 'blue', padding: '6px' } });
on
accepts an object which associates event names with event handlers, and adds those event handlers as listeners on the element.
const myButton = html('button', { on: { click: () => alert('Clicked!') } });
svg
and mathml
are nearly identical to html
, with the exception that they assign the appropriate XML namespace when creating the node type they represent.
Because these utilities just return Nodes, their results can be directly appended to the document:
import { html } from '@sprs/dom';
// Make an array of 1000 <li /> elements
const listItems = new Array(1000)
.fill(null)
.map((_, i) => `This is list item: ${i}`)
.map((text) => html('li', [text]));
// Make a page with a header and a list
const myPage = html('section', [
html('h1', ['This is a heading']),
html('p', ['This is some text']),
html('label', ['This is a list of items']),
html('ul', listItems),
]);
document
.querySelector('#root')
.appendChild(myPage);
Peg
The Peg
class represents a known position in the DOM. It maintains an internal reference to a hidden <div />
, which it uses to mark a location in the document. The affix
and clear
methods accept any kind of DOM node, and may be used to insert or remove content at the location maintained by the Peg
.
Following is an example that uses Peg
to add and remove elements from a fixed position in the document.
Note that this is a toy example to demonstrate direct usage of Peg
, and if you were actually to build this, you would probably be better off using @sprs/state
and relying on the data binding capabilities of render
and html
/svg
/mathml
.
import { render, html, Peg } from '@sprs/dom';
const content = html('p', ['This is some simple content in a <p /> tag']);
const contentPeg = new Peg();
let attached = false;
const toggleContent = () => {
if (attached) contentPeg.clear();
else contentPeg.affix(content);
attached = !attached;
};
const toggleButton = html(
'button',
{ on: { click: toggleContent } },
);
const myApp = render([
html('h1', ['This example shows how a Peg helps to make interactivity easier to implement']),
contentPeg,
toggleButton,
]);
document
.querySelector('#root')
.appendChild(myApp);
Many use cases for Pegs are wrapped in other helpers in the @sprs
suite. See the Reactive
class from this package, or the drag-and-drop utilities in @sprs/dnd
for some examples.
setAttrs
The setAttrs
utility applies the same attribute logic as html
and svg
, and can be used to modify an existing element with the same data structure.
import { html, setAttrs } from '@sprs/dom';
const div1 = html('div');
const div2 = document.createElement('div');
setAttrs(div1, { style: 'background-color: red;' });
setAttrs(div2, { style: 'background-color: black;' });
applyListeners
In the same vein as setAttrs
, applyListeners
applies event listeners to an element using the same data structure accepted by html
and svg
. Additionally, it accepts listeners at the root level, without the need for the on
key.
import { html, applyListeners } from '@sprs/dom';
const button1 = html('button');
const button2 = document.createElement('button');
applyListeners(div1, { on: { click: () => alert('Clicked') }});
applyListeners(div2, { on: { click: () => alert('Clicked') }});
applyListeners(div1, { click: () => alert('Clicked') });
applyListeners(div2, { click: () => alert('Clicked') });