ajo 0.1.29 → 0.1.31
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/LLMs.md +329 -0
- package/context.js +18 -0
- package/dist/html.cjs +1 -1
- package/dist/html.js +44 -65
- package/dist/index.cjs +1 -1
- package/dist/index.js +28 -27
- package/html.js +142 -0
- package/index.js +286 -0
- package/license +1 -1
- package/package.json +9 -10
- package/readme.md +34 -47
- package/types.ts +3 -21
- package/dist/stream.cjs +0 -1
- package/dist/stream.js +0 -24
package/html.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Context, current } from 'ajo/context'
|
|
2
|
+
|
|
3
|
+
const Void = new Set(['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'])
|
|
4
|
+
|
|
5
|
+
const Args = Symbol.for('ajo.args')
|
|
6
|
+
|
|
7
|
+
const escape = s => s.replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`)
|
|
8
|
+
|
|
9
|
+
const noop = () => { }
|
|
10
|
+
|
|
11
|
+
export const render = h => [...html(h)].join('')
|
|
12
|
+
|
|
13
|
+
export const html = function* (h) {
|
|
14
|
+
|
|
15
|
+
for (h of walk(h)) {
|
|
16
|
+
|
|
17
|
+
if (typeof h == 'string') yield escape(h)
|
|
18
|
+
|
|
19
|
+
else yield* element(h)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const element = function* ({ nodeName, children, ...h }) {
|
|
24
|
+
|
|
25
|
+
let attrs = ''
|
|
26
|
+
|
|
27
|
+
for (const key in h) {
|
|
28
|
+
|
|
29
|
+
if (key.startsWith('set:') || h[key] == null || h[key] === false) continue
|
|
30
|
+
|
|
31
|
+
if (h[key] === true) attrs += ` ${key}`
|
|
32
|
+
|
|
33
|
+
else attrs += ` ${key}="${escape(String(h[key]))}"`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (Void.has(nodeName)) yield `<${nodeName}${attrs}>`
|
|
37
|
+
|
|
38
|
+
else {
|
|
39
|
+
|
|
40
|
+
yield `<${nodeName}${attrs}>`
|
|
41
|
+
|
|
42
|
+
if (children != null) yield* html(children)
|
|
43
|
+
|
|
44
|
+
yield `</${nodeName}>`
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const walk = function* (h) {
|
|
49
|
+
|
|
50
|
+
if (h == null) return
|
|
51
|
+
|
|
52
|
+
const type = typeof h
|
|
53
|
+
|
|
54
|
+
if (type == 'boolean') return
|
|
55
|
+
|
|
56
|
+
if (type == 'string') yield h
|
|
57
|
+
|
|
58
|
+
else if (type == 'number' || type == 'bigint') yield String(h)
|
|
59
|
+
|
|
60
|
+
else if (Symbol.iterator in h) for (h of h) yield* walk(h)
|
|
61
|
+
|
|
62
|
+
else if ('nodeName' in h) typeof h.nodeName == 'function' ? yield* run(h) : yield vdom(h)
|
|
63
|
+
|
|
64
|
+
else yield String(h)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const run = function* ({ nodeName, fallback = nodeName.fallback, ...h }) {
|
|
68
|
+
|
|
69
|
+
if (nodeName.constructor.name == 'GeneratorFunction') yield runGenerator(nodeName, h)
|
|
70
|
+
|
|
71
|
+
else yield* walk(nodeName(h))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const runGenerator = (fn, h) => {
|
|
75
|
+
|
|
76
|
+
const attrs = { ...fn.attrs }, args = { ...fn.args }
|
|
77
|
+
|
|
78
|
+
for (const key in h) {
|
|
79
|
+
|
|
80
|
+
if (key.startsWith('attr:')) attrs[key.slice(5)] = h[key]
|
|
81
|
+
|
|
82
|
+
else if (key == 'key' || key == 'skip' || key == 'memo' || key == 'ref' || key.startsWith('set:')) attrs[key] = h[key]
|
|
83
|
+
|
|
84
|
+
else args[key] = h[key]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const controller = new AbortController()
|
|
88
|
+
|
|
89
|
+
const instance = {
|
|
90
|
+
|
|
91
|
+
[Context]: Object.create(current()?.[Context] ?? null),
|
|
92
|
+
|
|
93
|
+
[Args]: args,
|
|
94
|
+
|
|
95
|
+
signal: controller.signal,
|
|
96
|
+
|
|
97
|
+
next: noop,
|
|
98
|
+
|
|
99
|
+
return: noop,
|
|
100
|
+
|
|
101
|
+
throw: value => { throw value }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const iterator = fn.call(instance, args)
|
|
105
|
+
|
|
106
|
+
const parent = current()
|
|
107
|
+
|
|
108
|
+
current(instance)
|
|
109
|
+
|
|
110
|
+
const result = children => ({ ...attrs, nodeName: fn.is ?? 'div', ...vdom({ children }) })
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
|
|
114
|
+
return result(iterator.next().value)
|
|
115
|
+
|
|
116
|
+
} catch (error) {
|
|
117
|
+
|
|
118
|
+
return result(iterator.throw(error).value)
|
|
119
|
+
|
|
120
|
+
} finally {
|
|
121
|
+
|
|
122
|
+
iterator.return()
|
|
123
|
+
|
|
124
|
+
controller.abort()
|
|
125
|
+
|
|
126
|
+
current(parent)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const vdom = ({ key, skip, memo, ref, ...h }) => {
|
|
131
|
+
|
|
132
|
+
if ('children' in h) {
|
|
133
|
+
|
|
134
|
+
const children = [...walk(h.children)]
|
|
135
|
+
|
|
136
|
+
if (children.length) h.children = children.length == 1 ? children[0] : children
|
|
137
|
+
|
|
138
|
+
else delete h.children
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return h
|
|
142
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { Context, current } from 'ajo/context'
|
|
2
|
+
|
|
3
|
+
const Key = Symbol.for('ajo.key')
|
|
4
|
+
const Memo = Symbol.for('ajo.memo')
|
|
5
|
+
const Ref = Symbol.for('ajo.ref')
|
|
6
|
+
const Cache = Symbol.for('ajo.cache')
|
|
7
|
+
const Generator = Symbol.for('ajo.generator')
|
|
8
|
+
const Iterator = Symbol.for('ajo.iterator')
|
|
9
|
+
const Render = Symbol.for('ajo.render')
|
|
10
|
+
const Args = Symbol.for('ajo.args')
|
|
11
|
+
const Controller = Symbol.for('ajo.controller')
|
|
12
|
+
|
|
13
|
+
export const Fragment = props => props.children
|
|
14
|
+
|
|
15
|
+
export const h = (type, props, ...children) => {
|
|
16
|
+
|
|
17
|
+
(props ??= {}).nodeName = type
|
|
18
|
+
|
|
19
|
+
if (!('children' in props) && children.length) props.children = children.length == 1 ? children[0] : children
|
|
20
|
+
|
|
21
|
+
return props
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const render = (h, el, child = el.firstChild, ref = null) => {
|
|
25
|
+
|
|
26
|
+
for (h of walk(h)) {
|
|
27
|
+
|
|
28
|
+
const node = reconcile(h, el, child)
|
|
29
|
+
|
|
30
|
+
if (child == null) {
|
|
31
|
+
|
|
32
|
+
before(el, node, ref)
|
|
33
|
+
|
|
34
|
+
} else if (node == child) {
|
|
35
|
+
|
|
36
|
+
child = node.nextSibling
|
|
37
|
+
|
|
38
|
+
} else if (node == child.nextSibling) {
|
|
39
|
+
|
|
40
|
+
before(el, child, ref)
|
|
41
|
+
|
|
42
|
+
child = node.nextSibling
|
|
43
|
+
|
|
44
|
+
} else {
|
|
45
|
+
|
|
46
|
+
before(el, node, child)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
while (child != ref) {
|
|
51
|
+
|
|
52
|
+
const node = child.nextSibling
|
|
53
|
+
|
|
54
|
+
if (child.nodeType == 1) unref(child)
|
|
55
|
+
|
|
56
|
+
el.removeChild(child)
|
|
57
|
+
|
|
58
|
+
child = node
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const walk = function* (h) {
|
|
63
|
+
|
|
64
|
+
if (h == null) return
|
|
65
|
+
|
|
66
|
+
const type = typeof h
|
|
67
|
+
|
|
68
|
+
if (type == 'boolean') return
|
|
69
|
+
|
|
70
|
+
if (type == 'string') yield h
|
|
71
|
+
|
|
72
|
+
else if (type == 'number' || type == 'bigint') yield String(h)
|
|
73
|
+
|
|
74
|
+
else if (Symbol.iterator in h) for (h of h) yield* walk(h)
|
|
75
|
+
|
|
76
|
+
else if ('nodeName' in h) typeof h.nodeName == 'function' ? yield* run(h) : yield h
|
|
77
|
+
|
|
78
|
+
else yield String(h)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const run = function* ({ nodeName, ...h }) {
|
|
82
|
+
|
|
83
|
+
if (nodeName.constructor.name == 'GeneratorFunction') yield runGenerator(nodeName, h)
|
|
84
|
+
|
|
85
|
+
else yield* walk(nodeName(h))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const runGenerator = (fn, h) => {
|
|
89
|
+
|
|
90
|
+
const attrs = { ...fn.attrs }, args = { ...fn.args }
|
|
91
|
+
|
|
92
|
+
for (const key in h) {
|
|
93
|
+
|
|
94
|
+
if (key.startsWith('attr:')) attrs[key.slice(5)] = h[key]
|
|
95
|
+
|
|
96
|
+
else if (key == 'key' || key == 'skip' || key == 'memo' || key == 'ref' || key.startsWith('set:')) attrs[key] = h[key]
|
|
97
|
+
|
|
98
|
+
else args[key] = h[key]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { ...attrs, nodeName: fn.is ?? 'div', [Generator]: fn, [Args]: args }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const reconcile = (h, el, node) => typeof h == 'string' ? text(h, node) : element(h, el, node)
|
|
105
|
+
|
|
106
|
+
const text = (h, node) => {
|
|
107
|
+
|
|
108
|
+
while (node && node.nodeType != 3) node = node.nextSibling
|
|
109
|
+
|
|
110
|
+
node ? node.data != h && (node.data = h) : node = document.createTextNode(h)
|
|
111
|
+
|
|
112
|
+
return node
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const element = ({ nodeName, children, key, skip, memo, ref, [Generator]: gen, [Args]: args, ...h }, el, node) => {
|
|
116
|
+
|
|
117
|
+
while (node && (
|
|
118
|
+
|
|
119
|
+
(node.localName != nodeName) ||
|
|
120
|
+
|
|
121
|
+
(node[Key] != null && node[Key] != key) ||
|
|
122
|
+
|
|
123
|
+
(node[Generator] && node[Generator] != gen)
|
|
124
|
+
|
|
125
|
+
)) node = node.nextSibling
|
|
126
|
+
|
|
127
|
+
node ??= document.createElementNS(h.xmlns ?? el.namespaceURI, nodeName)
|
|
128
|
+
|
|
129
|
+
if (key != null) node[Key] = key
|
|
130
|
+
|
|
131
|
+
if (memo == null || some(node[Memo], node[Memo] = memo)) {
|
|
132
|
+
|
|
133
|
+
attrs(node[Cache] ?? extract(node), node[Cache] = h, node)
|
|
134
|
+
|
|
135
|
+
if (!skip) gen ? next(gen, args, node) : render(children, node)
|
|
136
|
+
|
|
137
|
+
if (typeof ref == 'function') (node[Ref] = ref)(node)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return node
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const attrs = (cache, h, node) => {
|
|
144
|
+
|
|
145
|
+
for (const key in { ...cache, ...h }) {
|
|
146
|
+
|
|
147
|
+
if (cache[key] === h[key]) continue
|
|
148
|
+
|
|
149
|
+
if (key.startsWith('set:')) node[key.slice(4)] = h[key]
|
|
150
|
+
|
|
151
|
+
else if (h[key] == null || h[key] === false) node.removeAttribute(key)
|
|
152
|
+
|
|
153
|
+
else node.setAttribute(key, h[key] === true ? '' : h[key])
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const some = (a, b) => Array.isArray(a) && Array.isArray(b) ? a.some((v, i) => v !== b[i]) : a !== b
|
|
158
|
+
|
|
159
|
+
const extract = el => Array.from(el.attributes).reduce((out, attr) => (out[attr.name] = attr.value, out), {})
|
|
160
|
+
|
|
161
|
+
const before = (el, node, child) => {
|
|
162
|
+
|
|
163
|
+
if (node.contains(document.activeElement)) {
|
|
164
|
+
|
|
165
|
+
const ref = node.nextSibling
|
|
166
|
+
|
|
167
|
+
while (child && child != node) {
|
|
168
|
+
|
|
169
|
+
const next = child.nextSibling
|
|
170
|
+
|
|
171
|
+
el.insertBefore(child, ref)
|
|
172
|
+
|
|
173
|
+
child = next
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
} else el.insertBefore(node, child)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const unref = node => {
|
|
180
|
+
|
|
181
|
+
for (const child of node.children) unref(child)
|
|
182
|
+
|
|
183
|
+
if (typeof node.return == 'function') node.return()
|
|
184
|
+
|
|
185
|
+
node[Ref]?.(null)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const next = (fn, args, el) => {
|
|
189
|
+
|
|
190
|
+
el[Generator] ??= (attach(el), fn)
|
|
191
|
+
|
|
192
|
+
Object.assign(el[Args] ??= {}, args)
|
|
193
|
+
|
|
194
|
+
el[Render]()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const attach = el => {
|
|
198
|
+
|
|
199
|
+
Object.assign(el, methods)
|
|
200
|
+
|
|
201
|
+
el[Context] = Object.create(current()?.[Context] ?? null)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const methods = {
|
|
205
|
+
|
|
206
|
+
[Render]() {
|
|
207
|
+
|
|
208
|
+
const parent = current()
|
|
209
|
+
|
|
210
|
+
current(this)
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
|
|
214
|
+
if (!this[Iterator]) {
|
|
215
|
+
|
|
216
|
+
this.signal = (this[Controller] = new AbortController()).signal
|
|
217
|
+
|
|
218
|
+
this[Iterator] = this[Generator].call(this, this[Args])
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const { value, done } = this[Iterator].next()
|
|
222
|
+
|
|
223
|
+
render(value, this)
|
|
224
|
+
|
|
225
|
+
this[Ref]?.(this)
|
|
226
|
+
|
|
227
|
+
if (done) this.return()
|
|
228
|
+
|
|
229
|
+
} catch (e) {
|
|
230
|
+
|
|
231
|
+
this.throw(e)
|
|
232
|
+
|
|
233
|
+
} finally {
|
|
234
|
+
|
|
235
|
+
current(parent)
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
next(fn, result) {
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
|
|
243
|
+
if (typeof fn == 'function') result = fn.call(this, this[Args])
|
|
244
|
+
|
|
245
|
+
} catch (e) {
|
|
246
|
+
|
|
247
|
+
return this.throw(e)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!current()?.contains(this)) this[Render]()
|
|
251
|
+
|
|
252
|
+
return result
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
throw(value) {
|
|
256
|
+
|
|
257
|
+
for (let el = this; el; el = el.parentNode) if (el[Iterator]?.throw) try {
|
|
258
|
+
|
|
259
|
+
return render(el[Iterator].throw(value).value, el)
|
|
260
|
+
|
|
261
|
+
} catch (e) {
|
|
262
|
+
|
|
263
|
+
value = new Error(e?.message ?? e, { cause: value })
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
throw value
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
return() {
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
|
|
273
|
+
this[Iterator]?.return()
|
|
274
|
+
|
|
275
|
+
} catch (e) {
|
|
276
|
+
|
|
277
|
+
this.throw(e)
|
|
278
|
+
|
|
279
|
+
} finally {
|
|
280
|
+
|
|
281
|
+
this[Iterator] = null
|
|
282
|
+
|
|
283
|
+
this[Controller]?.abort()
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
package/license
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ajo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"description": "ajo is a JavaScript view library for building user interfaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./types.ts",
|
|
@@ -21,23 +21,22 @@
|
|
|
21
21
|
"types": "./types.ts",
|
|
22
22
|
"import": "./dist/html.js",
|
|
23
23
|
"require": "./dist/html.cjs"
|
|
24
|
-
},
|
|
25
|
-
"./stream": {
|
|
26
|
-
"types": "./types.ts",
|
|
27
|
-
"import": "./dist/stream.js",
|
|
28
|
-
"require": "./dist/stream.cjs"
|
|
29
24
|
}
|
|
30
25
|
},
|
|
31
26
|
"files": [
|
|
32
27
|
"dist",
|
|
28
|
+
"context.js",
|
|
29
|
+
"html.js",
|
|
30
|
+
"index.js",
|
|
31
|
+
"LLMs.md",
|
|
33
32
|
"types.ts"
|
|
34
33
|
],
|
|
35
34
|
"devDependencies": {
|
|
36
|
-
"@types/node": "25.
|
|
37
|
-
"happy-dom": "20.
|
|
35
|
+
"@types/node": "25.2.3",
|
|
36
|
+
"happy-dom": "20.6.1",
|
|
38
37
|
"vite": "7.3.1",
|
|
39
|
-
"vite-tsconfig-paths": "6.
|
|
40
|
-
"vitest": "4.0.
|
|
38
|
+
"vite-tsconfig-paths": "6.1.1",
|
|
39
|
+
"vitest": "4.0.18"
|
|
41
40
|
},
|
|
42
41
|
"keywords": [
|
|
43
42
|
"ui",
|
package/readme.md
CHANGED
|
@@ -13,11 +13,10 @@
|
|
|
13
13
|
</a>
|
|
14
14
|
</div>
|
|
15
15
|
|
|
16
|
-
A modern JavaScript library for building user interfaces with generator-based state management
|
|
16
|
+
A modern JavaScript library for building user interfaces with generator-based state management and efficient DOM updates.
|
|
17
17
|
|
|
18
18
|
- **Generator-Based Components**: Use `function*` for stateful components with built-in lifecycle
|
|
19
19
|
- **Efficient DOM Updates**: In-place reconciliation minimizes DOM manipulation
|
|
20
|
-
- **Streaming SSR**: Progressive rendering with selective hydration (islands)
|
|
21
20
|
|
|
22
21
|
## Quick Start
|
|
23
22
|
|
|
@@ -29,6 +28,7 @@ npm install ajo
|
|
|
29
28
|
import { render } from 'ajo'
|
|
30
29
|
|
|
31
30
|
function* Counter() {
|
|
31
|
+
|
|
32
32
|
let count = 0
|
|
33
33
|
|
|
34
34
|
while (true) yield (
|
|
@@ -91,6 +91,7 @@ Generator functions with automatic wrapper elements. The structure provides a na
|
|
|
91
91
|
|
|
92
92
|
```javascript
|
|
93
93
|
function* TodoList() {
|
|
94
|
+
|
|
94
95
|
let todos = []
|
|
95
96
|
let text = ''
|
|
96
97
|
|
|
@@ -102,6 +103,7 @@ function* TodoList() {
|
|
|
102
103
|
})
|
|
103
104
|
|
|
104
105
|
while (true) {
|
|
106
|
+
|
|
105
107
|
const count = todos.length
|
|
106
108
|
|
|
107
109
|
yield (
|
|
@@ -123,10 +125,11 @@ function* TodoList() {
|
|
|
123
125
|
|
|
124
126
|
### Re-rendering with `this.next()`
|
|
125
127
|
|
|
126
|
-
Call `this.next()` to trigger a re-render. The optional callback receives current props
|
|
128
|
+
Call `this.next()` to trigger a re-render. The optional callback receives current props and its return value is passed through:
|
|
127
129
|
|
|
128
130
|
```javascript
|
|
129
131
|
function* Stepper(args) {
|
|
132
|
+
|
|
130
133
|
let count = 0
|
|
131
134
|
|
|
132
135
|
// Access current props in callback
|
|
@@ -155,11 +158,28 @@ function* Good(args) {
|
|
|
155
158
|
|
|
156
159
|
### Lifecycle and Cleanup
|
|
157
160
|
|
|
158
|
-
|
|
161
|
+
Every stateful component has a `this.signal` (AbortSignal) that aborts when the component unmounts. Use it with any API that accepts a signal:
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
function* MouseTracker() {
|
|
165
|
+
|
|
166
|
+
let pos = { x: 0, y: 0 }
|
|
167
|
+
|
|
168
|
+
document.addEventListener('mousemove', e => this.next(() => {
|
|
169
|
+
pos = { x: e.clientX, y: e.clientY }
|
|
170
|
+
}), { signal: this.signal }) // auto-removed on unmount
|
|
171
|
+
|
|
172
|
+
while (true) yield <p>{pos.x}, {pos.y}</p>
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
For APIs that don't accept a signal, use `try...finally`:
|
|
159
177
|
|
|
160
178
|
```javascript
|
|
161
179
|
function* Clock() {
|
|
180
|
+
|
|
162
181
|
let time = new Date()
|
|
182
|
+
|
|
163
183
|
const interval = setInterval(() => this.next(() => time = new Date()), 1000)
|
|
164
184
|
|
|
165
185
|
try {
|
|
@@ -223,6 +243,7 @@ function* ErrorBoundary(args) {
|
|
|
223
243
|
|
|
224
244
|
```javascript
|
|
225
245
|
function* AutoFocus() {
|
|
246
|
+
|
|
226
247
|
let input = null
|
|
227
248
|
|
|
228
249
|
while (true) yield (
|
|
@@ -251,6 +272,7 @@ timer?.next() // trigger re-render from outside
|
|
|
251
272
|
|
|
252
273
|
```javascript
|
|
253
274
|
function* Chart(args) {
|
|
275
|
+
|
|
254
276
|
let chart = null
|
|
255
277
|
|
|
256
278
|
while (true) yield (
|
|
@@ -292,6 +314,7 @@ const Card = ({ title }) => {
|
|
|
292
314
|
|
|
293
315
|
// Stateful - write inside loop (updates each render)
|
|
294
316
|
function* ThemeProvider(args) {
|
|
317
|
+
|
|
295
318
|
let theme = 'light'
|
|
296
319
|
|
|
297
320
|
while (true) {
|
|
@@ -318,9 +341,10 @@ function* FixedTheme(args) {
|
|
|
318
341
|
|
|
319
342
|
```javascript
|
|
320
343
|
function* UserProfile(args) {
|
|
344
|
+
|
|
321
345
|
let data = null, error = null, loading = true
|
|
322
346
|
|
|
323
|
-
fetch(`/api/users/${args.id}
|
|
347
|
+
fetch(`/api/users/${args.id}`, { signal: this.signal })
|
|
324
348
|
.then(r => r.json())
|
|
325
349
|
.then(d => this.next(() => { data = d; loading = false }))
|
|
326
350
|
.catch(e => this.next(() => { error = e; loading = false }))
|
|
@@ -336,41 +360,8 @@ function* UserProfile(args) {
|
|
|
336
360
|
## Server-Side Rendering
|
|
337
361
|
|
|
338
362
|
```javascript
|
|
339
|
-
// Static
|
|
340
363
|
import { render } from 'ajo/html'
|
|
341
364
|
const html = render(<App />)
|
|
342
|
-
|
|
343
|
-
// Streaming
|
|
344
|
-
import { stream } from 'ajo/stream'
|
|
345
|
-
for await (const chunk of stream(<App />)) res.write(chunk)
|
|
346
|
-
|
|
347
|
-
// Hydration (client-side)
|
|
348
|
-
import { hydrate } from 'ajo/stream'
|
|
349
|
-
window.$stream = { push: hydrate }
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
### Islands Architecture
|
|
353
|
-
|
|
354
|
-
```javascript
|
|
355
|
-
function* Interactive() {
|
|
356
|
-
let count = 0
|
|
357
|
-
while (true) yield (
|
|
358
|
-
<button set:onclick={() => this.next(() => count++)}>
|
|
359
|
-
{count}
|
|
360
|
-
</button>
|
|
361
|
-
)
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
Interactive.src = '/islands/interactive.js' // hydrate on client
|
|
365
|
-
|
|
366
|
-
const Page = () => (
|
|
367
|
-
<html>
|
|
368
|
-
<body>
|
|
369
|
-
<p>Static content</p>
|
|
370
|
-
<Interactive fallback={<button>0</button>} />
|
|
371
|
-
</body>
|
|
372
|
-
</html>
|
|
373
|
-
)
|
|
374
365
|
```
|
|
375
366
|
|
|
376
367
|
## TypeScript
|
|
@@ -388,6 +379,7 @@ const Card: Stateless<CardProps> = ({ title, children }) => (
|
|
|
388
379
|
type CounterProps = { initial: number; step?: number }
|
|
389
380
|
|
|
390
381
|
const Counter: Stateful<CounterProps, 'section'> = function* (args) {
|
|
382
|
+
|
|
391
383
|
let count = args.initial
|
|
392
384
|
|
|
393
385
|
while (true) {
|
|
@@ -423,16 +415,11 @@ let ref: ThisParameterType<typeof Counter> | null = null
|
|
|
423
415
|
|--------|-------------|
|
|
424
416
|
| `render(children)` | Render to HTML string |
|
|
425
417
|
|
|
426
|
-
### `ajo/stream`
|
|
427
|
-
| Export | Description |
|
|
428
|
-
|--------|-------------|
|
|
429
|
-
| `stream(children)` | Async iterator for streaming SSR |
|
|
430
|
-
| `hydrate(patch)` | Apply streamed patch on client |
|
|
431
|
-
|
|
432
418
|
### Stateful `this`
|
|
433
|
-
|
|
|
434
|
-
|
|
435
|
-
| `this.
|
|
419
|
+
| Property | Description |
|
|
420
|
+
|----------|-------------|
|
|
421
|
+
| `this.signal` | AbortSignal that aborts on unmount. Pass to `fetch()`, `addEventListener()`, etc. |
|
|
422
|
+
| `this.next(fn?)` | Re-render. Callback receives current args. Returns callback's result. |
|
|
436
423
|
| `this.throw(error)` | Throw to parent boundary |
|
|
437
424
|
| `this.return()` | Terminate generator |
|
|
438
425
|
|
package/types.ts
CHANGED
|
@@ -53,7 +53,8 @@ declare module 'ajo' {
|
|
|
53
53
|
TArguments
|
|
54
54
|
|
|
55
55
|
type StatefulElement<TArguments, TTag> = ElementType<TTag> & {
|
|
56
|
-
|
|
56
|
+
signal: AbortSignal,
|
|
57
|
+
next: <R>(fn?: (this: StatefulElement<TArguments, TTag>, args: StatefulArgs<TArguments, TTag>) => R) => R,
|
|
57
58
|
throw: (value?: unknown) => void,
|
|
58
59
|
return: () => void,
|
|
59
60
|
}
|
|
@@ -80,27 +81,8 @@ declare module 'ajo/context' {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
declare module 'ajo/html' {
|
|
83
|
-
|
|
84
|
-
type Patch = {
|
|
85
|
-
id: string,
|
|
86
|
-
h: import('ajo').Children,
|
|
87
|
-
src?: string,
|
|
88
|
-
done: boolean,
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
type Hooks = {
|
|
92
|
-
alloc?: (parentId: string) => string,
|
|
93
|
-
placeholder?: (id: string, children: import('ajo').Children) => unknown,
|
|
94
|
-
push?: (patch: Patch) => void,
|
|
95
|
-
}
|
|
96
|
-
|
|
97
84
|
function render(h: import('ajo').Children): string
|
|
98
|
-
function html(h: import('ajo').Children
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
declare module 'ajo/stream' {
|
|
102
|
-
function stream(h: import('ajo').Children): AsyncIterableIterator<string>
|
|
103
|
-
function hydrate(patch: import('ajo/html').Patch): Promise<void>
|
|
85
|
+
function html(h: import('ajo').Children): IterableIterator<string>
|
|
104
86
|
}
|
|
105
87
|
|
|
106
88
|
declare namespace JSX {
|
package/dist/stream.cjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const a=require("./index.cjs"),m=require("./html.cjs"),y=async function*(o,n=""){const s=new Map([[n,0]]),t=new Set,i=[],r=(e=n)=>(s.set(e,(s.get(e)??0)+1),e?`${e}:${s.get(e)-1}`:String(s.get(e)-1)),h=(e,c)=>({nodeName:"div","data-ssr":e,children:c}),f=e=>{const c=Promise.resolve(`<script>window.$stream?.push(${JSON.stringify(e)})<\/script>`);t.add(c),c.then(u=>i.push(u)).finally(()=>t.delete(c))};for(const e of m.html(o,{alloc:r,placeholder:h,push:f}))yield e;for(;t.size||i.length;){for(;i.length;)yield i.shift();t.size&&await Promise.race(t)}},d=new Set;async function l({id:o,src:n,h:s}){const t=document.querySelector(`[data-ssr="${o}"]`);if(!t)return d.add({id:o,src:n,h:s});n?a.render(a.h((await import(n)).default,s),t):a.render(s,t);const i=o+":";for(const r of d)r.id.startsWith(i)&&(d.delete(r),l(r))}exports.hydrate=l;exports.stream=y;
|