Skip to content

Commit

Permalink
forgot to add docs ...
Browse files Browse the repository at this point in the history
  • Loading branch information
WebReflection committed Nov 13, 2023
1 parent 62b21f4 commit 7f9b882
Show file tree
Hide file tree
Showing 3 changed files with 295 additions and 1 deletion.
294 changes: 294 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
### Template Tag Features

```
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ html | svg
tag`
<div class=${className} ?hidden=${!show}>
┃ ┗━━━━━━━━━━━━━ boolean
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ attribute
<ul @click=${sort} .sort=${order}>
┃ ┗━━━━━━━━━━━━━━━━━━ direct
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ listener
${[...listItems]}
┗━━━━━━┳━━━━━┛
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━ list
</ul>
<hr /> ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ self closing
<p>
${show ? `${order} results` : null}
┗━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┛
┗━━━━━━━━━━━━━━━━━━━━━ hole
</p>
</div>
`;
```

<details>
<summary><strong>tag</strong></summary>
<div markdown=1>

A template literal tag can be either the `html` or the `svg` one, both directly exported from this module:

```js
import { html, svg } from 'uhtml';

html`<button />`;
svg`<circle />`;
```

Used directly from both default and `uhtml/keyed` variant, the returning value will not be a DOM Node, rather a *Hole* representation of that node once rendered,
unless the import was from `uhtml/node`, which instead creates once DOM nodes hence it could be used with any library or framework able to handle these.

The `uhtml/keyed` export though also allows to create tags related to a specific key, where the key is a `ref` / `key` pair and it guarantees the resulting node will always be the same,
given the same *ref* and the same *id*.

```js
import { htmlFor, svgFor } from 'uhtml/keyed';

const Button = key => {
const html = htmlFor(Button, key);
return html`<button />`;
};

const Circle = key => {
const svg = svgFor(Circle, key);
return svg`<circle />`;
};

Button('unique-id') === Button('unique-id');
Circle('unique-id') === Circle('unique-id');
```

In *keyed* cases, the result will always be the same node and not a *Hole*.

##### Keyed or not ?

To some extend, *uhtml* is *keyed by default*, meaning that given the same template all elements in that template
will always be created or referenced once in the stack.

In most common use cases then, using a *keyed* approach might just be overkill, unless you rely on the fact a node must be the same
whenever its attributes or content changes, as opposite of being the previous node with updated values within it.

The use cases that best represent this need are:

* a list items or table rows that somehow are referenced elsewhere for other purposes and could be sorted or swapped and their identity should be preserved
* same node moved elsewhere under some condition, with some expensive computed state attached to it

There are really not many other edge cases to prefer *keyed* over non keyed, but whenever you feel like *keyed* would be better, `uhtml/keyed` will provide
that extra feature, without compromising too much performance or bundle size (it's just ~0.1K increase and very little extra logic involved).

</div>
</details>

<details>
<summary><strong>boolean</strong></summary>
<div markdown=1>

Fully inspired by *lit*, boolean attributes are simply a **toggle** indirection to either have, or not, such attribute.

```js
import { render, html } from 'uhtml';

render(document.body, html`
<div ?hidden=${false}>I am visible</div>
<div ?hidden=${true}>I am invisible</div>
`);

/** results into
<body>
<div>I am visible</div>
<div hidden>I am invisible</div>
<body>
*/
```

</div>
</details>

<details>
<summary><strong>attribute</strong></summary>
<div markdown=1>

Every attribute that doesn't have a specialized syntax prefix, such as `?`, `@` or `.`, is handled in the following way and only if different from its previous value:

* if the exported `attr` *Map* knows the attribute, a callback related to it will be used to update
* `aria` attribute accepts and handle an object literal with `role` and other *aria* attributes
* `class` attribute handles a direct `element.className` assignment
* `data` attribute accepts and handle an object literal with `dataset` names to directly set to the node
* `ref` attribute handles *React* like *ref* property by updating the `ref.current` value to the current node, or invoking `ref(element)` when it's a callback
* `style` attribute handles a direct `element.style.cssText` assignment
* it is possible to augment the `attr` *Map* with any custom attribute name that doesn't have an already known prefix and it's not part of the already known list (although one could override known attributes too). In this case, `attr.set("my-attr", (element, newValue, name, oldValue) => newValue)` is the expected signature to augment attributes in the wild, as the stack retains only the current value and it will invoke the callback only if the new value is different.
* if the attribute is unknown in the `attr` map, a `name in element` check is performed once (per template, not per element) and if that's `true`, a *direct* assignment will be used to update the value
* `"onclick" in element`, like any other native listener, will directly assign the callback via `element[name] = value`, when `value` is different, providing a way to simplify events handling in the wild
* `"value" in input`, like any other understood accessor for the currently related node, will directly use `input[name] = value`, when `value` is different
* `"hidden" in element`, as defined by standard, will also directly set `element[name] = value`, when `value` is different, somehow overlapping with the *boolean* feature
* any other `"accessor" in element` will simply follow the exact same rule and use the direct `element[name] = value`, when `value` is different
* in all other cases the attribute is set via `element.setAttribute(name, value)` and removed via `element.removeAttribute(name)` when `value` is either `null` or `undefined`

</div>
</details>

<details>
<summary><strong>direct</strong></summary>
<div markdown=1>

A direct attribute is simply passed along to the element, no matter its name or special standard behavior.

```js
import { render, html } from 'uhtml';

const state = {
some: 'special state'
};

render(document.body, html`
<div id='direct' .state=${state}>content</div>
`);

document.querySelector('#direct').state === state;
// true
```

If the name is already a special standard accessor, this will be set with the current value, whenever it's different from the previous one, so that *direct* syntax *could* be also used to set `.hidden` or `.value`, for input or textarea, but that's just explicit, as these accessors would work regardless that way, without needing special syntax hints and as already explained in the *attribute* section.

</div>
</details>

<details>
<summary><strong>listener</strong></summary>
<div markdown=1>

As already explained in the *attribute* section, common listeners can be already attached via `onclick=${callback}` and everything would work already as expected, with also less moving parts behind the scene ... but what if the listener is a custom event name or it requires options such as `{ once: true }` ?

This is where `@click=${[handler, { once: true }]}` helps, so that `addEventListener`, and `removeEventListener` when the listener changes, are used instead of direct `on*=${callback}` assignment.

```js
import { render, html } from 'uhtml';

const handler = {
handleEvent(event) {
console.log(event.type);
}
};

render(document.body, html`
<div @custom:type=${handler}, @click=${[handler, { once: true }]}>
content
</div>
`);

const div = document.querySelector('div');

div.dispatchEvent(new Event('custom:type'));
// logs "custom:type"

div.click();
// logs "click"

div.click();
// nothing, as it was once
```

**Please note** that even if options such as `{ once: true }` are used, if the handler / listener is different each time the listener itself will be added, as for logic sake that's indeed a different listener.

</div>
</details>

<details>
<summary><strong>list</strong></summary>
<div markdown=1>

Most of the time, the template defines just static parts of the content and this is not likely to grow or shrink over time *but*, when that's the case or desired, it is possible to use an *array* to delimit an area that over time could grow or shrink.

`<ul>`, `<ol>`, `<tr>` and whatnot, are all valid use cases to use a list placeholder and not some unique node, together with `<article>` and literally any other use case that might render or not multiple nodes in the very same place after updates.


```js
import { render, html } from 'uhtml';

const handler = {
handleEvent(event) {
console.log(event.type);
}
};

render(document.querySelector('#todos'), html`
<ul>
${databaseResults.map(value => html`<li>${value}</li>`)}
</ul>
`);
```

Please note that whenever a specific placeholder in the template might shrink in the future, it is always possible to still use an array to represent a single content:

```js
html`
<div>
${items.length ? items : [
html`...loading content`
// still valid hole content
// or a direct DOM node to render
]}
</div>
`
```

**Please also note** that an *array* is always expected to contain a *hole* or an actual DOM Node.

</div>
</details>

<details>
<summary><strong>self closing</strong></summary>
<div markdown=1>

Fully inspired by *XHTML* first and *JSX* after, any element that self closes won't result into surprises so that *custom-elements* as well as any other standard node that doesn't have nodes in it works out of the box.

```js
import { render, html } from 'uhtml';

render(document.body, html`
<my-element />
<my-other-element />
`);

/** results into
<body>
<my-element></my-element>
<my-other-element></my-other-element>
<body>
*/
```

</div>
</details>

<details>
<summary><strong>hole</strong></summary>
<div markdown=1>

Technically speaking, in the template literal tags world all values part of the template are called *interpolations*.

```js
const tag = (template, interpolations) => {
console.log(template.join());
// logs "this is , and this is ,"
console.log(interpolations);
// logs [1, 2]
};

tag`this is ${1} and this is ${2}`;
```

Mostly because the name *Interpolation* is both verbose and boring plus it doesn't really describe the value *kind* within a DOM context, in *uhtml* the chosen name for "*yet unknown content to be rendered*" values is *hole*.

By current TypeScript definition, a *hole* can be either:

* a `string`, a `boolean` or a `number` to show as it is on the rendered node
* `null` or `undefined` to signal that *hole* has currently no content whatsoever
* an actual `instanceof Hole` exported class, which is what `html` or `svg` tags return once invoked
* an *array* that contains a list of instances of *Hole* or DOM nodes to deal with

</div>
</details>
- - -
2 changes: 1 addition & 1 deletion esm/keyed.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Hole, unroll } from './rabbit.js';
import { empty, set } from './utils.js';
import { html, svg } from './index.js';
import { attr } from './handler.js';
import render from './render-any.js';
import render from './render-keyed.js';

/** @typedef {import("./literals.js").Cache} Cache */
/** @typedef {import("./literals.js").Target} Target */
Expand Down
File renamed without changes.

0 comments on commit 7f9b882

Please sign in to comment.