Skip to content

Commit

Permalink
Merge 8d318b2 into 8dd94e6
Browse files Browse the repository at this point in the history
  • Loading branch information
WebReflection committed Mar 27, 2020
2 parents 8dd94e6 + 8d318b2 commit c3814f2
Show file tree
Hide file tree
Showing 15 changed files with 668 additions and 238 deletions.
14 changes: 14 additions & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,8 +371,22 @@ These are the rules to follow for attributes:
* if the attribute name starts with `on`, as example, `onclick=${...}`, it will be set as listener. If the listener changes, the previous one will be automatically removed. If the listener is an `Array` like `[listener, {once:true}]`, the second entry of the array would be used as listener's options.
* if the attribute starts with a `.` dot, as in `.setter=${value}`, the value will be passed directly to the element per each update. If such value is a known setter, either native elements or defined via Custom Elements, the setter will be invoked per each update, even if the value is the same
* if the attribute name is `ref`, as in `ref=${object}`, the `object.current` property will be assigned to the node, once this is rendered, and per each update
* if the attribute name is `aria`, as in `aria=${object}`, aria attributes are applied to the node, including the `role` one.
* if the attribute name is `data`, as in `data=${object}`, the `node.dataset` gets populated with all values.


Following an example of both `aria` and `data` cases:

```js
// the aria special case
html`<div aria=${{labelledBy: 'id', role: 'button'}} />`;
//=> <div aria-labelledby="id" role="button"></div>

// the data special case
html`<div data=${{key: 'value', otherKey: 'otherValue'}} />`;
//=> <div data-key="value" data-other-key="otherValue"></div>
```

## API: HTML/SVG Content

It is possible to place interpolations within any kind of node, and together with text or other nodes too.
Expand Down
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,21 @@ These are the rules to follow for attributes:
* if the attribute name starts with `on`, as example, `onclick=${...}`, it will be set as listener. If the listener changes, the previous one will be automatically removed. If the listener is an `Array` like `[listener, {once:true}]`, the second entry of the array would be used as listener's options.
* if the attribute starts with a `.` dot, as in `.setter=${value}`, the value will be passed directly to the element per each update. If such value is a known setter, either native elements or defined via Custom Elements, the setter will be invoked per each update, even if the value is the same
* if the attribute name is `ref`, as in `ref=${object}`, the `object.current` property will be assigned to the node, once this is rendered, and per each update. If a callback is passed instead, the callback will receive the node right away, same way [React ref](https://reactjs.org/docs/refs-and-the-dom.html) does.
* if the attribute name is `aria`, as in `aria=${object}`, aria attributes are applied to the node, including the `role` one.
* if the attribute name is `data`, as in `data=${object}`, the `node.dataset` gets populated with all values.


Following an example of both `aria` and `data` cases:

```js
// the aria special case
html`<div aria=${{labelledBy: 'id', role: 'button'}} />`;
//=> <div aria-labelledby="id" role="button"></div>

// the data special case
html`<div data=${{key: 'value', otherKey: 'otherValue'}} />`;
//=> <div data-key="value" data-other-key="otherValue"></div>
```

</div>
</details>
Expand Down Expand Up @@ -249,7 +264,7 @@ Following a list of other points to consider when choosing _µhtml_ instead of _
* _uhtml_ should *not* suffer any of the IE11/Edge issues, or invalid SVG attributes warnings, as the parsing is done differently 🎉
* nested `html` and `svg` are allowed like in _lighterhtml_. The version `0` of this library didn't allow that, hence it was more "_surprise prone_". _uhtml_ in that sense is more like a drop-in replacement for _lighterhtml_, and vice-versa
* keyed results via `htmlfor(...)` or `svg.for(...)`, as well as one-off node creation, via `html.node` or `svg.node` are the same found in _lighterhtml_
* the `ref=${...}` attribute works same as _lighterhtml_, enabling hooks, or _React_ style, out of the box
* both `aria=${object}`, `data=${object}`, or `ref=${...}` attributes work same as _lighterhtml_
* the `.property=${...}` *direct setter* is still available
* self closing nodes are also supported, go wild with `<custom-elements />` or even `<span />`
* the wire parsing logic has been simplified even more, resulting in slightly [better bootstrap and update performance](https://github.com/krausest/js-framework-benchmark/pull/698)
Expand Down
99 changes: 25 additions & 74 deletions cjs/handlers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';
const {isArray, slice} = require('uarray');
const udomdiff = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('udomdiff'));
const {aria, attribute, data, event, ref, setter, text} = require('uhandlers');
const {diffable} = require('uwire');

const {reducePath} = require('./node.js');
Expand Down Expand Up @@ -33,8 +34,8 @@ const diff = (comment, oldNodes, newNodes) => udomdiff(
// diffing will be related to such comment.
// This helper is in charge of understanding how the new
// content for such interpolation/hole should be updated
const handleAnything = (comment, nodes) => {
let oldValue, text;
const handleAnything = comment => {
let oldValue, text, nodes = [];
const anyContent = newValue => {
switch (typeof newValue) {
// primitives are handled as text content
Expand All @@ -43,17 +44,18 @@ const handleAnything = (comment, nodes) => {
case 'boolean':
if (oldValue !== newValue) {
oldValue = newValue;
if (!text)
text = document.createTextNode('');
text.textContent = newValue;
if (text)
text.textContent = newValue;
else
text = document.createTextNode(newValue);
nodes = diff(comment, nodes, [text]);
}
break;
// null, and undefined are used to cleanup previous content
case 'object':
case 'undefined':
if (newValue == null) {
if (oldValue) {
if (oldValue != newValue) {
oldValue = newValue;
nodes = diff(comment, nodes, []);
}
Expand All @@ -71,13 +73,14 @@ const handleAnything = (comment, nodes) => {
// in all other cases the content is stringified as is
else
anyContent(String(newValue));
break;
}
// if the new value is a DOM node, or a wire, and it's
// different from the one already live, then it's diffed.
// if the node is a fragment, it's appended once via its childNodes
// There is no `else` here, meaning if the content
// is not expected one, nothing happens, as easy as that.
else if ('ELEMENT_NODE' in newValue && newValue !== oldValue) {
if ('ELEMENT_NODE' in newValue && oldValue !== newValue) {
oldValue = newValue;
nodes = diff(
comment,
Expand All @@ -94,81 +97,29 @@ const handleAnything = (comment, nodes) => {

// attributes can be:
// * ref=${...} for hooks and other purposes
// * aria=${...} for aria attributes
// * data=${...} for dataset related attributes
// * .setter=${...} for Custom Elements setters or nodes with setters
// such as buttons, details, options, select, etc
// * onevent=${...} to automatically handle event listeners
// * generic=${...} to handle an attribute just like an attribute
const handleAttribute = (node, name) => {
// hooks and ref
if (name === 'ref')
return ref => {
if (typeof ref === 'function')
ref(node);
else
ref.current = node;
};
return ref(node);

// direct setters
if (name.slice(0, 1) === '.') {
const setter = name.slice(1);
return value => { node[setter] = value; }
}
if (name === 'aria')
return aria(node);

let oldValue;
if (name === 'data')
return data(node);

// events
if (name.slice(0, 2) === 'on') {
let type = name.slice(2);
if (!(name in node) && name.toLowerCase() in node)
type = type.toLowerCase();
return newValue => {
const info = isArray(newValue) ? newValue : [newValue, false];
if (oldValue !== info[0]) {
if (oldValue)
node.removeEventListener(type, oldValue, info[1]);
if (oldValue = info[0])
node.addEventListener(type, oldValue, info[1]);
}
};
}
if (name.slice(0, 1) === '.')
return setter(node, name.slice(1));

// all other cases
let noOwner = true;
const attribute = document.createAttribute(name);
return newValue => {
if (oldValue !== newValue) {
oldValue = newValue;
if (oldValue == null) {
if (!noOwner) {
node.removeAttributeNode(attribute);
noOwner = true;
}
}
else {
attribute.value = newValue;
// There is no else case here.
// If the attribute has no owner, it's set back.
if (noOwner) {
node.setAttributeNode(attribute);
noOwner = false;
}
}
}
};
};
if (name.slice(0, 2) === 'on')
return event(node, name);

// style and textarea nodes can change only their text
// without any possibility to accept child nodes.
// in these two cases the content is simply updated, or cleaned,
// accordingly with the passed value.
const handleText = node => {
let oldValue;
return newValue => {
if (oldValue !== newValue) {
oldValue = newValue;
node.textContent = newValue == null ? '' : newValue;
}
};
return attribute(node, name);
};

// each mapped update carries the update type and its path
Expand All @@ -177,11 +128,11 @@ const handleText = node => {
// In the attribute case, the attribute name is also carried along.
function handlers(options) {
const {type, path} = options;
const node = path.reduce(reducePath, this);
const node = path.reduceRight(reducePath, this);
return type === 'node' ?
handleAnything(node, []) :
handleAnything(node) :
(type === 'attr' ?
handleAttribute(node, options.name) :
handleText(node));
text(node));
}
exports.handlers = handlers;
4 changes: 2 additions & 2 deletions cjs/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const createContent = (m => m.__esModule ? /* istanbul ignore next */ m.default
const {indexOf} = require('uarray');

// from a generic path, retrieves the exact targeted node
const reducePath = (node, i) => node.childNodes[i];
const reducePath = ({childNodes}, i) => childNodes[i];
exports.reducePath = reducePath;

// from a fragment container, create an array of indexes
Expand All @@ -13,7 +13,7 @@ const createPath = node => {
const path = [];
let {parentNode} = node;
while (parentNode) {
path.unshift(indexOf.call(parentNode.childNodes, node));
path.push(indexOf.call(parentNode.childNodes, node));
node = parentNode;
parentNode = node.parentNode;
}
Expand Down

0 comments on commit c3814f2

Please sign in to comment.