-
Notifications
You must be signed in to change notification settings - Fork 51
Expand file tree
/
Copy pathhandlers.js
More file actions
139 lines (132 loc) · 5.15 KB
/
handlers.js
File metadata and controls
139 lines (132 loc) · 5.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import {isArray, slice} from 'uarray';
import udomdiff from 'udomdiff';
import {aria, attribute, boolean, event, ref, setter, text} from 'uhandlers';
import {diffable} from 'uwire';
// from a generic path, retrieves the exact targeted node
const reducePath = ({childNodes}, i) => childNodes[i];
// this helper avoid code bloat around handleAnything() callback
const diff = (comment, oldNodes, newNodes) => udomdiff(
comment.parentNode,
// TODO: there is a possible edge case where a node has been
// removed manually, or it was a keyed one, attached
// to a shared reference between renders.
// In this case udomdiff might fail at removing such node
// as its parent won't be the expected one.
// The best way to avoid this issue is to filter oldNodes
// in search of those not live, or not in the current parent
// anymore, but this would require both a change to uwire,
// exposing a parentNode from the firstChild, as example,
// but also a filter per each diff that should exclude nodes
// that are not in there, penalizing performance quite a lot.
// As this has been also a potential issue with domdiff,
// and both lighterhtml and hyperHTML might fail with this
// very specific edge case, I might as well document this possible
// "diffing shenanigan" and call it a day.
oldNodes,
newNodes,
diffable,
comment
);
// if an interpolation represents a comment, the whole
// 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 => {
let oldValue, text, nodes = [];
const anyContent = newValue => {
switch (typeof newValue) {
// primitives are handled as text content
case 'string':
case 'number':
case 'boolean':
if (oldValue !== newValue) {
oldValue = newValue;
if (!text)
text = document.createTextNode('');
text.data = 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 != newValue) {
oldValue = newValue;
nodes = diff(comment, nodes, []);
}
break;
}
// arrays and nodes have a special treatment
if (isArray(newValue)) {
oldValue = newValue;
// arrays can be used to cleanup, if empty
if (newValue.length === 0)
nodes = diff(comment, nodes, []);
// or diffed, if these contains nodes or "wires"
else if (typeof newValue[0] === 'object')
nodes = diff(comment, nodes, newValue);
// 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.
if (oldValue !== newValue && 'ELEMENT_NODE' in newValue) {
oldValue = newValue;
nodes = diff(
comment,
nodes,
newValue.nodeType === 11 ?
slice.call(newValue.childNodes) :
[newValue]
);
}
break;
case 'function':
anyContent(newValue(comment));
break;
}
};
return anyContent;
};
// attributes can be:
// * ref=${...} for hooks and other purposes
// * aria=${...} for aria attributes
// * ?boolean=${...} for boolean attributes
// * .dataset=${...} for dataset related attributes
// * .setter=${...} for Custom Elements setters or nodes with setters
// such as buttons, details, options, select, etc
// * @event=${...} to explicitly handle event listeners
// * onevent=${...} to automatically handle event listeners
// * generic=${...} to handle an attribute just like an attribute
const handleAttribute = (node, name/*, svg*/) => {
switch (name[0]) {
case '?': return boolean(node, name.slice(1), false);
case '.': return setter(node, name.slice(1));
case '@': return event(node, 'on' + name.slice(1));
case 'o': if (name[1] === 'n') return event(node, name);
}
switch (name) {
case 'ref': return ref(node);
case 'aria': return aria(node);
}
return attribute(node, name/*, svg*/);
};
// each mapped update carries the update type and its path
// the type is either node, attribute, or text, while
// the path is how to retrieve the related node to update.
// In the attribute case, the attribute name is also carried along.
export function handlers(options) {
const {type, path} = options;
const node = path.reduceRight(reducePath, this);
return type === 'node' ?
handleAnything(node) :
(type === 'attr' ?
handleAttribute(node, options.name/*, options.svg*/) :
text(node));
};