-
Notifications
You must be signed in to change notification settings - Fork 41
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
62b21f4
commit d30be4c
Showing
3 changed files
with
295 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,294 @@ | ||
### Template Tag Features | ||
|
||
``` | ||
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ tag | ||
html` | ||
<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> | ||
- - - |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.