npm package: @sprs/dom
Description: Minimal utilities for browser UI development
Author: Brett Mitchell
Latest version: 0.0.3


# `@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:

```js
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:

```js
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](https://github.com/hyperhype/hyperscript/tree/master), 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

```js
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.

```js
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`.

```js
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.

```js
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:

```js

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`.

```js

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.

```js

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.

```js

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') });

```