ajo 0.0.4 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.cjs +167 -0
- package/index.js +165 -167
- package/index.test.js +27 -12
- package/package.json +20 -16
- package/readme.md +61 -1
package/index.cjs
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from2, except, desc) => {
|
|
10
|
+
if (from2 && typeof from2 === "object" || typeof from2 === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from2))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from2[key], enumerable: !(desc = __getOwnPropDesc(from2, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
+
var ajo_exports = {};
|
|
19
|
+
__export(ajo_exports, {
|
|
20
|
+
Fragment: () => Fragment,
|
|
21
|
+
cleanup: () => cleanup,
|
|
22
|
+
clx: () => clx,
|
|
23
|
+
component: () => component,
|
|
24
|
+
consume: () => consume,
|
|
25
|
+
h: () => h,
|
|
26
|
+
intercept: () => intercept,
|
|
27
|
+
keb: () => keb,
|
|
28
|
+
propagate: () => propagate,
|
|
29
|
+
provide: () => provide,
|
|
30
|
+
refresh: () => refresh,
|
|
31
|
+
render: () => render,
|
|
32
|
+
stx: () => stx
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(ajo_exports);
|
|
35
|
+
const Fragment = ({ children }) => children, h = (tagName, props, ...children) => {
|
|
36
|
+
children = children.length == 0 ? null : children.length == 1 ? children[0] : children;
|
|
37
|
+
return { children, ...props, tagName };
|
|
38
|
+
}, component = (setup) => ({ is, key, ref, host, ...props }) => h(is ?? setup.is ?? "div", {
|
|
39
|
+
key,
|
|
40
|
+
...setup.host,
|
|
41
|
+
...host,
|
|
42
|
+
skip: true,
|
|
43
|
+
ref: (host2) => (refresh(host2, props, setup), isFn(ref) && ref(host2))
|
|
44
|
+
}), render = (h2, host) => {
|
|
45
|
+
let child = host.firstChild, node, byKey = keyed.get(host);
|
|
46
|
+
for (h2 of normalize(h2, host)) {
|
|
47
|
+
if (typeof h2 == str) {
|
|
48
|
+
for (node = child; node; node = node.nextSibling)
|
|
49
|
+
if (node.nodeType == 3)
|
|
50
|
+
break;
|
|
51
|
+
node ? node.data !== h2 && (node.data = h2) : node = document.createTextNode(h2);
|
|
52
|
+
} else if (hasOwn(h2, "tagName")) {
|
|
53
|
+
const { tagName, key, skip, ref, children, block, ...props } = h2;
|
|
54
|
+
if (key != null && (node = byKey?.get(key)))
|
|
55
|
+
;
|
|
56
|
+
else
|
|
57
|
+
for (node = child; node; node = node.nextSibling)
|
|
58
|
+
if (node.localName == tagName)
|
|
59
|
+
break;
|
|
60
|
+
node ||= document.createElement(tagName);
|
|
61
|
+
key != null && (byKey ||= keyed.set(host, createMap(Map))).set(key, node);
|
|
62
|
+
update(props, node);
|
|
63
|
+
!(skip || block != null && every(deps.get(node), deps.set(node, block))) && render(children, node);
|
|
64
|
+
isFn(ref) && ref(node);
|
|
65
|
+
} else
|
|
66
|
+
node = h2;
|
|
67
|
+
node === child ? child = child.nextSibling : before(host, child, node);
|
|
68
|
+
}
|
|
69
|
+
while (child) {
|
|
70
|
+
const next = child.nextSibling;
|
|
71
|
+
host.removeChild(child).nodeType == 1 && dispose(child);
|
|
72
|
+
child = next;
|
|
73
|
+
}
|
|
74
|
+
}, refresh = (host, props, setup) => {
|
|
75
|
+
try {
|
|
76
|
+
if (setup && !isFn(setup))
|
|
77
|
+
throwTypeError("Setup", setup, fn);
|
|
78
|
+
props = props ? memo.set(host, { ...setup?.props, ...props }) : memo.get(host) ?? {};
|
|
79
|
+
render((renders.get(host) ?? renders.set(host, setup(props, host)))(props, host), host);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
propagate(host, error);
|
|
82
|
+
}
|
|
83
|
+
}, provide = (host, key, value) => (provisions.get(host) ?? provisions.set(host, createMap(Map))).set(key, value), consume = (host, key, fallback) => {
|
|
84
|
+
let map;
|
|
85
|
+
while (host) {
|
|
86
|
+
if ((map = provisions.get(host)) && map.has(key))
|
|
87
|
+
return map.get(key);
|
|
88
|
+
host = host.parentNode;
|
|
89
|
+
}
|
|
90
|
+
return fallback;
|
|
91
|
+
}, intercept = (host, interceptor) => {
|
|
92
|
+
if (!isFn(interceptor))
|
|
93
|
+
throwTypeError("Interceptor", interceptor, fn);
|
|
94
|
+
interceptors.set(host, interceptor);
|
|
95
|
+
}, propagate = (host, error) => {
|
|
96
|
+
for (let interceptor; host; host = host.parentNode)
|
|
97
|
+
if (interceptor = interceptors.get(host))
|
|
98
|
+
return render(host, interceptor(error));
|
|
99
|
+
throw error;
|
|
100
|
+
}, cleanup = (host, cleaner) => {
|
|
101
|
+
if (!isFn(cleaner))
|
|
102
|
+
throwTypeError("Cleaner", cleaner, fn);
|
|
103
|
+
(cleaners.get(host) ?? cleaners.set(host, /* @__PURE__ */ new Set())).add(cleaner);
|
|
104
|
+
}, clx = (o) => keys(o).filter((k) => o[k]).join(" ") || null, stx = (o) => entries(o).map((t) => t.join(":")).join(";") || null, keb = (o) => keys(o).reduce((r, k) => (r[k.replace(search, replace).toLowerCase()] = o[k], r), {});
|
|
105
|
+
const createMap = (constructor, ...args) => {
|
|
106
|
+
const instance = new constructor(...args);
|
|
107
|
+
const { set } = instance;
|
|
108
|
+
instance.set = (key, value) => (set.call(instance, key, value), value);
|
|
109
|
+
return instance;
|
|
110
|
+
}, throwTypeError = (name, value, expected) => {
|
|
111
|
+
throw new TypeError(`Expected ${name} to be of type ${expected}, got ${typeof value} instead`);
|
|
112
|
+
}, every = (a, b) => a === b || isArray(a) && isArray(b) && a.length == b.length && a.every((v, i) => v === b[i]), apply = (o, { name, value }) => (o[name] = value, o), reduce = (list) => from(list).reduce(apply, {}), isFn = (v) => typeof v == fn, search = /([a-z0-9])([A-Z])/g, replace = "$1-$2", str = "string", fn = "function", { keys, entries, hasOwn } = Object, { isArray, from } = Array, wm = WeakMap, deps = createMap(wm), memo = createMap(wm), keyed = createMap(wm), cache = createMap(wm), renders = createMap(wm), cleaners = createMap(wm), provisions = createMap(wm), interceptors = createMap(wm), normalize = function* (h2, host) {
|
|
113
|
+
let type, buffer = "";
|
|
114
|
+
for (h2 of isFn(h2?.[Symbol.iterator]) ? h2 : [h2]) {
|
|
115
|
+
if (h2 == null || (type = typeof h2) == "boolean")
|
|
116
|
+
continue;
|
|
117
|
+
if (h2 instanceof Node || hasOwn(h2, "tagName")) {
|
|
118
|
+
if (isFn(h2.tagName)) {
|
|
119
|
+
yield* normalize(h2.tagName(h2, host), host);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (buffer) {
|
|
123
|
+
yield buffer;
|
|
124
|
+
buffer = "";
|
|
125
|
+
}
|
|
126
|
+
yield h2;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
type == str || !isFn(h2[Symbol.iterator]) ? buffer += h2 : yield* normalize(h2, host);
|
|
130
|
+
}
|
|
131
|
+
if (buffer)
|
|
132
|
+
yield buffer;
|
|
133
|
+
}, update = (props, host) => {
|
|
134
|
+
const prev = cache.get(host) ?? (host.hasAttributes() ? reduce(host.attributes) : {});
|
|
135
|
+
for (const name in { ...prev, ...props }) {
|
|
136
|
+
let value = props[name];
|
|
137
|
+
if (value === prev[name])
|
|
138
|
+
continue;
|
|
139
|
+
if (name.startsWith("set:")) {
|
|
140
|
+
host[name.slice(4)] = value;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (value === true)
|
|
144
|
+
value = "";
|
|
145
|
+
else if (value == null || value === false) {
|
|
146
|
+
host.removeAttribute(name);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
host.setAttribute(name, value);
|
|
150
|
+
}
|
|
151
|
+
cache.set(host, props);
|
|
152
|
+
}, before = (host, child, node) => {
|
|
153
|
+
if (node.contains?.(document.activeElement)) {
|
|
154
|
+
const ref = node.nextSibling;
|
|
155
|
+
while (child && child !== node) {
|
|
156
|
+
const next = child.nextSibling;
|
|
157
|
+
host.insertBefore(child, ref);
|
|
158
|
+
child = next;
|
|
159
|
+
}
|
|
160
|
+
} else
|
|
161
|
+
host.insertBefore(node, child);
|
|
162
|
+
}, dispose = (host) => {
|
|
163
|
+
for (const child of host.children)
|
|
164
|
+
dispose(child);
|
|
165
|
+
for (const cleaner of cleaners.get(host) ?? [])
|
|
166
|
+
cleaner(host);
|
|
167
|
+
};
|
package/index.js
CHANGED
|
@@ -1,205 +1,203 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
1
|
+
export const
|
|
2
|
+
Fragment = ({ children }) => children,
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
h = (tagName, props, ...children) => {
|
|
5
|
+
children = children.length == 0 ? null : children.length == 1 ? children[0] : children
|
|
6
|
+
return { children, ...props, tagName }
|
|
7
|
+
},
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
component = setup => ({ is, key, ref, host, ...props }) => h(is ?? setup.is ?? 'div', {
|
|
10
|
+
key, ...setup.host, ...host, skip: true, ref: host => (refresh(host, props, setup), isFn(ref) && ref(host))
|
|
11
|
+
}),
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
props = { ...props }
|
|
12
|
-
children = props.children ?? children
|
|
13
|
-
if (children.length > 0) props.children = children.length === 1 ? children[0] : children
|
|
14
|
-
props[Element] = name
|
|
15
|
-
return props
|
|
16
|
-
}
|
|
13
|
+
render = (h, host) => {
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
const vnodes = isArray(vnode) ? vnode : [vnode]
|
|
20
|
-
let data = ''
|
|
15
|
+
let child = host.firstChild, node, byKey = keyed.get(host)
|
|
21
16
|
|
|
22
|
-
|
|
23
|
-
vnode = vnodes.shift()
|
|
17
|
+
for (h of normalize(h, host)) {
|
|
24
18
|
|
|
25
|
-
|
|
26
|
-
if (typeof vnode === 'string') data += vnode
|
|
27
|
-
else if (hasOwn(vnode, Element)) {
|
|
28
|
-
const { [Element]: name } = vnode
|
|
19
|
+
if (typeof h == str) {
|
|
29
20
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
continue
|
|
33
|
-
}
|
|
21
|
+
for (node = child; node; node = node.nextSibling) if (node.nodeType == 3) break
|
|
22
|
+
node ? node.data !== h && (node.data = h) : node = document.createTextNode(h)
|
|
34
23
|
|
|
35
|
-
if (
|
|
36
|
-
yield { [Element]: '#text', data }
|
|
37
|
-
data = ''
|
|
38
|
-
}
|
|
24
|
+
} else if (hasOwn(h, 'tagName')) {
|
|
39
25
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
26
|
+
const { tagName, key, skip, ref, children, block, ...props } = h
|
|
27
|
+
|
|
28
|
+
if (key != null && (node = byKey?.get(key)));
|
|
29
|
+
else for (node = child; node; node = node.nextSibling) if (node.localName == tagName) break
|
|
45
30
|
|
|
46
|
-
|
|
47
|
-
}
|
|
31
|
+
node ||= document.createElement(tagName)
|
|
48
32
|
|
|
49
|
-
|
|
50
|
-
if (host?.nodeType !== Node.ELEMENT_NODE) return
|
|
33
|
+
key != null && (byKey ||= keyed.set(host, createMap(Map))).set(key, node)
|
|
51
34
|
|
|
52
|
-
|
|
35
|
+
update(props, node)
|
|
53
36
|
|
|
54
|
-
|
|
37
|
+
!(skip || block != null && every(deps.get(node), deps.set(node, block))) && render(children, node)
|
|
55
38
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
39
|
+
isFn(ref) && ref(node)
|
|
40
|
+
|
|
41
|
+
} else node = h
|
|
42
|
+
|
|
43
|
+
node === child ? child = child.nextSibling : before(host, child, node)
|
|
59
44
|
}
|
|
60
45
|
|
|
61
|
-
|
|
46
|
+
while (child) {
|
|
47
|
+
const next = child.nextSibling
|
|
48
|
+
host.removeChild(child).nodeType == 1 && dispose(child)
|
|
49
|
+
child = next
|
|
50
|
+
}
|
|
51
|
+
},
|
|
62
52
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
53
|
+
refresh = (host, props, setup) => {
|
|
54
|
+
try {
|
|
55
|
+
if (setup && !isFn(setup)) throwTypeError('Setup', setup, fn)
|
|
56
|
+
props = props ? memo.set(host, { ...setup?.props, ...props }) : memo.get(host) ?? {}
|
|
57
|
+
render((renders.get(host) ?? renders.set(host, setup(props, host)))(props, host), host)
|
|
58
|
+
} catch (error) {
|
|
59
|
+
propagate(host, error)
|
|
66
60
|
}
|
|
61
|
+
},
|
|
67
62
|
|
|
68
|
-
|
|
69
|
-
? document.createTextNode('')
|
|
70
|
-
: document.createElement(name)
|
|
63
|
+
provide = (host, key, value) => (provisions.get(host) ?? provisions.set(host, createMap(Map))).set(key, value),
|
|
71
64
|
|
|
72
|
-
|
|
65
|
+
consume = (host, key, fallback) => {
|
|
66
|
+
let map
|
|
67
|
+
while (host) {
|
|
68
|
+
if ((map = provisions.get(host)) && map.has(key)) return map.get(key)
|
|
69
|
+
host = host.parentNode
|
|
70
|
+
}
|
|
71
|
+
return fallback
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
intercept = (host, interceptor) => {
|
|
75
|
+
if (!isFn(interceptor)) throwTypeError('Interceptor', interceptor, fn)
|
|
76
|
+
interceptors.set(host, interceptor)
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
propagate = (host, error) => {
|
|
80
|
+
for (let interceptor; host; host = host.parentNode)
|
|
81
|
+
if (interceptor = interceptors.get(host)) return render(host, interceptor(error))
|
|
82
|
+
throw error
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
cleanup = (host, cleaner) => {
|
|
86
|
+
if (!isFn(cleaner)) throwTypeError('Cleaner', cleaner, fn);
|
|
87
|
+
(cleaners.get(host) ?? cleaners.set(host, new Set)).add(cleaner)
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
clx = o => keys(o).filter(k => o[k]).join(' ') || null,
|
|
91
|
+
stx = o => entries(o).map(t => t.join(':')).join(';') || null,
|
|
92
|
+
keb = o => keys(o).reduce((r, k) => ((r[k.replace(search, replace).toLowerCase()] = o[k]), r), {})
|
|
93
|
+
|
|
94
|
+
const
|
|
95
|
+
createMap = (constructor, ...args) => {
|
|
96
|
+
const instance = new constructor(...args)
|
|
97
|
+
const { set } = instance
|
|
98
|
+
instance.set = (key, value) => (set.call(instance, key, value), value)
|
|
99
|
+
return instance
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
throwTypeError = (name, value, expected) => {
|
|
103
|
+
throw new TypeError(`Expected ${name} to be of type ${expected}, got ${typeof value} instead`)
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
every = (a, b) => a === b || isArray(a) && isArray(b) && a.length == b.length && a.every((v, i) => v === b[i]),
|
|
107
|
+
apply = (o, { name, value }) => ((o[name] = value), o),
|
|
108
|
+
reduce = list => from(list).reduce(apply, {}),
|
|
109
|
+
isFn = v => typeof v == fn,
|
|
110
|
+
|
|
111
|
+
search = /([a-z0-9])([A-Z])/g,
|
|
112
|
+
replace = '$1-$2',
|
|
113
|
+
str = 'string',
|
|
114
|
+
fn = 'function',
|
|
115
|
+
|
|
116
|
+
{ keys, entries, hasOwn } = Object,
|
|
117
|
+
{ isArray, from } = Array,
|
|
118
|
+
wm = WeakMap,
|
|
119
|
+
|
|
120
|
+
deps = createMap(wm),
|
|
121
|
+
memo = createMap(wm),
|
|
122
|
+
keyed = createMap(wm),
|
|
123
|
+
cache = createMap(wm),
|
|
124
|
+
renders = createMap(wm),
|
|
125
|
+
cleaners = createMap(wm),
|
|
126
|
+
provisions = createMap(wm),
|
|
127
|
+
interceptors = createMap(wm),
|
|
128
|
+
|
|
129
|
+
normalize = function* (h, host) {
|
|
130
|
+
|
|
131
|
+
let type, buffer = ''
|
|
132
|
+
|
|
133
|
+
for (h of isFn(h?.[Symbol.iterator]) ? h : [h]) {
|
|
134
|
+
|
|
135
|
+
if (h == null || (type = typeof h) == 'boolean') continue
|
|
136
|
+
|
|
137
|
+
if (h instanceof Node || hasOwn(h, 'tagName')) {
|
|
138
|
+
|
|
139
|
+
if (isFn(h.tagName)) {
|
|
140
|
+
yield* normalize(h.tagName(h, host), host)
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (buffer) {
|
|
145
|
+
yield buffer
|
|
146
|
+
buffer = ''
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
yield h
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
73
152
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
render(children, node)
|
|
153
|
+
(type == str || !isFn(h[Symbol.iterator])) ? buffer += h : yield* normalize(h, host)
|
|
154
|
+
}
|
|
78
155
|
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
const { nextSibling } = node
|
|
82
|
-
let ref = child
|
|
156
|
+
if (buffer) yield buffer
|
|
157
|
+
},
|
|
83
158
|
|
|
84
|
-
|
|
85
|
-
const next = ref.nextSibling
|
|
86
|
-
host.insertBefore(ref, nextSibling)
|
|
87
|
-
ref = next
|
|
88
|
-
}
|
|
89
|
-
} else host.insertBefore(node, child)
|
|
90
|
-
}
|
|
159
|
+
update = (props, host) => {
|
|
91
160
|
|
|
92
|
-
|
|
93
|
-
const { nodeType, nextSibling } = child
|
|
94
|
-
if (nodeType === Node.ELEMENT_NODE) dispose(child)
|
|
95
|
-
host.removeChild(child)
|
|
96
|
-
child = nextSibling
|
|
97
|
-
}
|
|
98
|
-
}
|
|
161
|
+
const prev = cache.get(host) ?? (host.hasAttributes() ? reduce(host.attributes) : {})
|
|
99
162
|
|
|
100
|
-
const
|
|
101
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
102
|
-
if (node.data !== props.data) node.data = props.data
|
|
103
|
-
return
|
|
104
|
-
}
|
|
163
|
+
for (const name in { ...prev, ...props }) {
|
|
105
164
|
|
|
106
|
-
|
|
107
|
-
let value = props[name]
|
|
165
|
+
let value = props[name]
|
|
108
166
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
167
|
+
if (value === prev[name]) continue
|
|
168
|
+
|
|
169
|
+
if (name.startsWith('set:')) {
|
|
170
|
+
host[name.slice(4)] = value
|
|
112
171
|
continue
|
|
113
|
-
}
|
|
114
|
-
}
|
|
172
|
+
}
|
|
115
173
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
174
|
+
if (value === true) value = ''
|
|
175
|
+
else if (value == null || value === false) {
|
|
176
|
+
host.removeAttribute(name)
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
host.setAttribute(name, value)
|
|
120
181
|
}
|
|
121
182
|
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
}
|
|
183
|
+
cache.set(host, props)
|
|
184
|
+
},
|
|
125
185
|
|
|
126
|
-
|
|
186
|
+
before = (host, child, node) => {
|
|
187
|
+
if (node.contains?.(document.activeElement)) {
|
|
127
188
|
|
|
128
|
-
|
|
129
|
-
createElement(is ?? fn.is ?? 'div', {
|
|
130
|
-
...fn.host, ...host, key, ref: host => {
|
|
131
|
-
let cmp = components.get(host)
|
|
132
|
-
if (cmp == null) components.set(host, cmp = new Component(host, fn))
|
|
133
|
-
cmp.props = { ...fn.props, ...props }
|
|
134
|
-
cmp.update()
|
|
135
|
-
}
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
class Component {
|
|
139
|
-
constructor(host, fn) {
|
|
140
|
-
this.host = host
|
|
141
|
-
this.fn = fn
|
|
142
|
-
this.props = null
|
|
143
|
-
this.it = null
|
|
144
|
-
this.err = false
|
|
145
|
-
}
|
|
189
|
+
const ref = node.nextSibling
|
|
146
190
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
try {
|
|
152
|
-
if (this.it == null) {
|
|
153
|
-
const init = this.fn.call(this, this.props, this.host)
|
|
154
|
-
this.it = typeof init?.next === 'function' ? init : generate.call(this, init)
|
|
191
|
+
while (child && child !== node) {
|
|
192
|
+
const next = child.nextSibling
|
|
193
|
+
host.insertBefore(child, ref)
|
|
194
|
+
child = next
|
|
155
195
|
}
|
|
156
196
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
render(value, this.host)
|
|
160
|
-
if (done) this.host = null
|
|
161
|
-
} catch (err) {
|
|
162
|
-
this.err = true
|
|
163
|
-
propagate(this.host.parentNode, err)
|
|
164
|
-
}
|
|
165
|
-
}
|
|
197
|
+
} else host.insertBefore(node, child)
|
|
198
|
+
},
|
|
166
199
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function* generate(init) {
|
|
173
|
-
yield init
|
|
174
|
-
for (const props of this) yield this.fn.call(this, props, this.host)
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const propagate = (el, err) => {
|
|
178
|
-
if (el == null) throw err
|
|
179
|
-
const cmp = components.get(el)
|
|
180
|
-
typeof cmp?.it?.throw === 'function'
|
|
181
|
-
? cmp.update({ method: 'throw', arg: err })
|
|
182
|
-
: propagate(el.parentNode, err)
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const dispose = el => {
|
|
186
|
-
for (const child of el.children) dispose(child)
|
|
187
|
-
components.get(el)?.update({ method: 'return' })
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const provisions = new WeakMap
|
|
191
|
-
|
|
192
|
-
export const provide = (host, key, value) => {
|
|
193
|
-
let map = provisions.get(host)
|
|
194
|
-
if (map == null) provisions.set(host, map = new Map)
|
|
195
|
-
map.set(key, value)
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export const consume = (host, key) => {
|
|
199
|
-
for (let node = host; node != null; node = node.parentNode) {
|
|
200
|
-
if (provisions.has(node)) {
|
|
201
|
-
const map = provisions.get(node)
|
|
202
|
-
if (map.has(key)) return map.get(key)
|
|
203
|
-
}
|
|
200
|
+
dispose = host => {
|
|
201
|
+
for (const child of host.children) dispose(child)
|
|
202
|
+
for (const cleaner of cleaners.get(host) ?? []) cleaner(host)
|
|
204
203
|
}
|
|
205
|
-
}
|
package/index.test.js
CHANGED
|
@@ -1,28 +1,37 @@
|
|
|
1
|
-
import 'backdom/register
|
|
1
|
+
import 'backdom/register'
|
|
2
2
|
import { suite } from 'uvu'
|
|
3
3
|
import * as assert from 'uvu/assert'
|
|
4
|
-
import {
|
|
4
|
+
import { component, h, render } from './index.js'
|
|
5
5
|
|
|
6
6
|
let it
|
|
7
7
|
|
|
8
8
|
// ----------------------------------------------------------------------------
|
|
9
9
|
|
|
10
|
-
it = suite('
|
|
10
|
+
it = suite('h')
|
|
11
11
|
|
|
12
12
|
it('should create empty vnode', () => {
|
|
13
|
-
|
|
13
|
+
const vnode = h('div')
|
|
14
|
+
assert.equal(vnode, { tagName: 'div', children: null })
|
|
14
15
|
})
|
|
15
16
|
|
|
16
17
|
it('should create vnode with props', () => {
|
|
17
|
-
assert.equal(
|
|
18
|
+
assert.equal(h('div', { id: 'app' }), { tagName: 'div', children: null, id: 'app' })
|
|
18
19
|
})
|
|
19
20
|
|
|
20
21
|
it('should create vnode with one string child', () => {
|
|
21
|
-
assert.equal(
|
|
22
|
+
assert.equal(h('div', null, 'foo'), { tagName: 'div', children: 'foo' })
|
|
22
23
|
})
|
|
23
24
|
|
|
24
25
|
it('should create vnode with one vnode child', () => {
|
|
25
|
-
|
|
26
|
+
const child = h('span')
|
|
27
|
+
const vnode = h('div', null, child)
|
|
28
|
+
assert.equal(vnode.children, { tagName: 'span', children: null })
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should allow children as prop', () => {
|
|
32
|
+
const child = h('span')
|
|
33
|
+
const vnode = h('div', { children: child })
|
|
34
|
+
assert.equal(vnode.children, { tagName: 'span', children: null })
|
|
26
35
|
})
|
|
27
36
|
|
|
28
37
|
it.run()
|
|
@@ -33,16 +42,22 @@ it = suite('render')
|
|
|
33
42
|
|
|
34
43
|
it('should render a vnode', () => {
|
|
35
44
|
const host = document.createElement('div')
|
|
36
|
-
render(
|
|
45
|
+
render(h('div', { foo: 'bar' }, 'foobar'), host)
|
|
37
46
|
assert.snapshot(host.innerHTML, '<div foo="bar">foobar</div>')
|
|
38
47
|
})
|
|
39
48
|
|
|
40
|
-
it('should render a component', () => {
|
|
49
|
+
it('should render a stateless component', () => {
|
|
41
50
|
const host = document.createElement('div')
|
|
42
|
-
const Foo = ({ foo, children }) =>
|
|
43
|
-
|
|
44
|
-
render(createElement(Foo, { foo: 'bar' }, createElement('span', null, 'foobar')), host)
|
|
51
|
+
const Foo = ({ foo, children }) => h('div', { foo }, children)
|
|
52
|
+
render(h(Foo, { foo: 'bar' }, h('span', null, 'foobar')), host)
|
|
45
53
|
assert.snapshot(host.innerHTML, '<div foo="bar"><span>foobar</span></div>')
|
|
46
54
|
})
|
|
47
55
|
|
|
56
|
+
it('should render a stateful component', () => {
|
|
57
|
+
const host = document.createElement('div')
|
|
58
|
+
const Foo = component(() => ({ foo, children }) => h('div', { foo }, children))
|
|
59
|
+
render(h(Foo, { is: 'foo-component', foo: 'bar' }, h('span', null, 'foobar')), host)
|
|
60
|
+
assert.snapshot(host.innerHTML, '<foo-component><div foo="bar"><span>foobar</span></div></foo-component>')
|
|
61
|
+
})
|
|
62
|
+
|
|
48
63
|
it.run()
|
package/package.json
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ajo",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "ajo is a JavaScript view library for building user interfaces",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
"
|
|
6
|
+
"module": "index.js",
|
|
7
|
+
"main": "index.cjs",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./index.js",
|
|
11
|
+
"require": "./index.cjs"
|
|
12
|
+
}
|
|
10
13
|
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "esbuild --format=cjs --out-extension:.js=.cjs --outdir=. index.js",
|
|
16
|
+
"test": "uvu",
|
|
17
|
+
"release": "npm run build && npm t && git commit -am \"$npm_package_version\" && git tag $npm_package_version && git push && git push --tags && npm publish"
|
|
18
|
+
},
|
|
19
|
+
"repository": "cristianfalcone/ajo",
|
|
11
20
|
"author": "Cristian Falcone",
|
|
12
21
|
"license": "ISC",
|
|
13
|
-
"bugs":
|
|
14
|
-
"url": "https://github.com/cristianfalcone/ajo/issues"
|
|
15
|
-
},
|
|
22
|
+
"bugs": "https://github.com/cristianfalcone/ajo/issues",
|
|
16
23
|
"homepage": "https://github.com/cristianfalcone/ajo#readme",
|
|
17
24
|
"devDependencies": {
|
|
18
|
-
"backdom": "^0.0.
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
},
|
|
24
|
-
"readme": "# ajo\najo is a JavaScript view library for building user interfaces"
|
|
25
|
-
}
|
|
25
|
+
"backdom": "^0.0.4",
|
|
26
|
+
"esbuild": "^0.14.51",
|
|
27
|
+
"uvu": "^0.5.6"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/readme.md
CHANGED
|
@@ -1,2 +1,62 @@
|
|
|
1
1
|
# ajo
|
|
2
|
-
ajo is a JavaScript view library for building user interfaces
|
|
2
|
+
ajo is a JavaScript view library for building user interfaces
|
|
3
|
+
|
|
4
|
+
```sh
|
|
5
|
+
npm install ajo
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
## Incremental DOM
|
|
9
|
+
To keep the UI in sync, ajo uses a technique called incremental DOM.
|
|
10
|
+
It is a way to build UI components without the need to keep previous virtual DOM in memory.
|
|
11
|
+
Instead, generated virtual DOM is diffed against the actual DOM, and changes are applied along the way.
|
|
12
|
+
This reduces memory usage and makes ajo code more simple and concise. As a result, ajo is easy to read and maintain, but lacks perfomance oportunities that diffing two virtual DOM trees can provide.
|
|
13
|
+
|
|
14
|
+
```jsx
|
|
15
|
+
/** @jsx h */
|
|
16
|
+
import { h, render } from 'ajo'
|
|
17
|
+
|
|
18
|
+
document.body.innerHTML = '<div>Hello World</div>'
|
|
19
|
+
|
|
20
|
+
render(document.body, <div>Goodbye World</div>)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Stateless components
|
|
24
|
+
As a way to reuse markup snipets, ajo uses simple synchroneous functions that return virtual DOM.
|
|
25
|
+
This type of components are ment to be "consumers" of data.
|
|
26
|
+
No state is preserved between invocations, so generated virtual DOM should rely exclusively on function's arguments.
|
|
27
|
+
|
|
28
|
+
```jsx
|
|
29
|
+
/** @jsx h */
|
|
30
|
+
import { h, render } from 'ajo'
|
|
31
|
+
|
|
32
|
+
const Greet = ({ name }) => <div>Hello {name}</div>
|
|
33
|
+
|
|
34
|
+
render(document.body, <Greet name="World" />)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Stateful components
|
|
38
|
+
Since ajo does not store previous virtual DOM, stateful components rely on a DOM node to preserve its state between UI updates.
|
|
39
|
+
This DOM node is called a host node (similar to a Web Component host node).
|
|
40
|
+
State is declared in a generator function local scope.
|
|
41
|
+
Then ajo asociates the returned iterator with the host, and updates host children nodes each time, retrieving iterator's next value. Lifecycle of these components are closely related to its host nodes, and generator function provides a way to manage them.
|
|
42
|
+
|
|
43
|
+
```jsx
|
|
44
|
+
/** @jsx h */
|
|
45
|
+
import { h, component, render, refresh } from 'ajo'
|
|
46
|
+
|
|
47
|
+
const Counter = component(({ start = 0 }, host) => {
|
|
48
|
+
let count = start
|
|
49
|
+
|
|
50
|
+
const increment = () => {
|
|
51
|
+
count++
|
|
52
|
+
refresh(host)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return () =>
|
|
56
|
+
<button onclick={increment}>
|
|
57
|
+
Current: {count}
|
|
58
|
+
</button>
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
render(document.body, <Counter start={1} />)
|
|
62
|
+
```
|