ajo 0.1.30 → 0.1.32
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 +349 -0
- package/context.js +18 -0
- package/dist/html.cjs +1 -1
- package/dist/html.js +47 -45
- package/dist/index.cjs +1 -1
- package/dist/index.js +87 -70
- package/dist/jsx.cjs +1 -0
- package/dist/jsx.js +12 -0
- package/html.js +137 -0
- package/index.js +309 -0
- package/jsx.js +21 -0
- package/license +15 -15
- package/package.json +74 -54
- package/readme.md +447 -418
- package/types.ts +135 -90
package/readme.md
CHANGED
|
@@ -1,418 +1,447 @@
|
|
|
1
|
-
# Ajo
|
|
2
|
-
|
|
3
|
-
<div align="center">
|
|
4
|
-
<img src="https://github.com/cristianfalcone/ajo/raw/main/ajo.png" alt="ajo" width="372" />
|
|
5
|
-
</div>
|
|
6
|
-
|
|
7
|
-
<div align="center">
|
|
8
|
-
<a href="https://npmjs.org/package/ajo">
|
|
9
|
-
<img src="https://badgen.now.sh/npm/v/ajo" alt="version" />
|
|
10
|
-
</a>
|
|
11
|
-
<a href="https://npmjs.org/package/ajo">
|
|
12
|
-
<img src="https://badgen.now.sh/npm/dm/ajo" alt="downloads" />
|
|
13
|
-
</a>
|
|
14
|
-
</div>
|
|
15
|
-
|
|
16
|
-
A modern JavaScript library for building user interfaces with generator-based state management and efficient DOM updates.
|
|
17
|
-
|
|
18
|
-
- **Generator-Based Components**: Use `function*` for stateful components with built-in lifecycle
|
|
19
|
-
- **Efficient DOM Updates**: In-place reconciliation minimizes DOM manipulation
|
|
20
|
-
|
|
21
|
-
## Quick Start
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
npm install ajo
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
```javascript
|
|
28
|
-
import { render } from 'ajo'
|
|
29
|
-
|
|
30
|
-
function* Counter() {
|
|
31
|
-
|
|
32
|
-
let count = 0
|
|
33
|
-
|
|
34
|
-
while (true) yield (
|
|
35
|
-
<button set:onclick={() => this.next(() => count++)}>
|
|
36
|
-
Count: {count}
|
|
37
|
-
</button>
|
|
38
|
-
)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
render(<Counter />, document.body)
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
### Build Configuration
|
|
45
|
-
|
|
46
|
-
Configure your build tool to use Ajo's JSX
|
|
47
|
-
|
|
48
|
-
**Vite:**
|
|
49
|
-
```typescript
|
|
50
|
-
// vite.config.ts
|
|
51
|
-
import { defineConfig } from 'vite'
|
|
52
|
-
|
|
53
|
-
export default defineConfig({
|
|
54
|
-
esbuild: {
|
|
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
|
-
set:
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
```javascript
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
<button set:onclick={
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
<
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
)
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
<
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
1
|
+
# Ajo
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
<img src="https://github.com/cristianfalcone/ajo/raw/main/ajo.png" alt="ajo" width="372" />
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div align="center">
|
|
8
|
+
<a href="https://npmjs.org/package/ajo">
|
|
9
|
+
<img src="https://badgen.now.sh/npm/v/ajo" alt="version" />
|
|
10
|
+
</a>
|
|
11
|
+
<a href="https://npmjs.org/package/ajo">
|
|
12
|
+
<img src="https://badgen.now.sh/npm/dm/ajo" alt="downloads" />
|
|
13
|
+
</a>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
A modern JavaScript library for building user interfaces with generator-based state management and efficient DOM updates.
|
|
17
|
+
|
|
18
|
+
- **Generator-Based Components**: Use `function*` for stateful components with built-in lifecycle
|
|
19
|
+
- **Efficient DOM Updates**: In-place reconciliation minimizes DOM manipulation
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install ajo
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
import { render } from 'ajo'
|
|
29
|
+
|
|
30
|
+
function* Counter() {
|
|
31
|
+
|
|
32
|
+
let count = 0
|
|
33
|
+
|
|
34
|
+
while (true) yield (
|
|
35
|
+
<button set:onclick={() => this.next(() => count++)}>
|
|
36
|
+
Count: {count}
|
|
37
|
+
</button>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
render(<Counter />, document.body)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Build Configuration
|
|
45
|
+
|
|
46
|
+
Configure your build tool to use Ajo's automatic JSX runtime:
|
|
47
|
+
|
|
48
|
+
**Vite:**
|
|
49
|
+
```typescript
|
|
50
|
+
// vite.config.ts
|
|
51
|
+
import { defineConfig } from 'vite'
|
|
52
|
+
|
|
53
|
+
export default defineConfig({
|
|
54
|
+
esbuild: {
|
|
55
|
+
jsx: 'automatic',
|
|
56
|
+
jsxImportSource: 'ajo',
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**TypeScript:**
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"compilerOptions": {
|
|
65
|
+
"jsx": "react-jsx",
|
|
66
|
+
"jsxImportSource": "ajo"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Other build tools:** Set `jsx: 'react-jsx'` (or `'automatic'`), `jsxImportSource: 'ajo'`. No manual imports needed — the build tool auto-imports from `ajo/jsx-runtime`.
|
|
72
|
+
|
|
73
|
+
## Core Concepts
|
|
74
|
+
|
|
75
|
+
### Stateless Components
|
|
76
|
+
|
|
77
|
+
Pure functions that receive props and return JSX:
|
|
78
|
+
|
|
79
|
+
```javascript
|
|
80
|
+
const Greeting = ({ name }) => <p>Hello, {name}!</p>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Stateful Components
|
|
84
|
+
|
|
85
|
+
Generator functions with automatic wrapper elements. The structure provides a natural mental model:
|
|
86
|
+
|
|
87
|
+
- **Before the loop**: Persistent state and handlers (survives re-renders)
|
|
88
|
+
- **Inside the loop**: Derived values (computed fresh each render)
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
function* TodoList() {
|
|
92
|
+
|
|
93
|
+
let todos = []
|
|
94
|
+
let text = ''
|
|
95
|
+
|
|
96
|
+
const add = () => this.next(() => {
|
|
97
|
+
if (text.trim()) {
|
|
98
|
+
todos.push({ id: Date.now(), text })
|
|
99
|
+
text = ''
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
while (true) {
|
|
104
|
+
|
|
105
|
+
const count = todos.length
|
|
106
|
+
|
|
107
|
+
yield (
|
|
108
|
+
<>
|
|
109
|
+
<input
|
|
110
|
+
set:value={text}
|
|
111
|
+
set:oninput={e => text = e.target.value}
|
|
112
|
+
set:onkeydown={e => e.key === 'Enter' && add()}
|
|
113
|
+
/>
|
|
114
|
+
<button set:onclick={add}>Add ({count})</button>
|
|
115
|
+
<ul>
|
|
116
|
+
{todos.map(t => <li key={t.id}>{t.text}</li>)}
|
|
117
|
+
</ul>
|
|
118
|
+
</>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Re-rendering with `this.next()`
|
|
125
|
+
|
|
126
|
+
Call `this.next()` to trigger a re-render. The optional callback receives current args and its return value is passed through:
|
|
127
|
+
|
|
128
|
+
```javascript
|
|
129
|
+
function* Stepper() {
|
|
130
|
+
|
|
131
|
+
let count = 0
|
|
132
|
+
|
|
133
|
+
// Access current args in callback
|
|
134
|
+
const inc = () => this.next(({ step = 1 }) => count += step)
|
|
135
|
+
|
|
136
|
+
for (const { step = 1 } of this) yield (
|
|
137
|
+
<button set:onclick={inc}>Count: {count} (+{step})</button>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Args in the Render Loop
|
|
143
|
+
|
|
144
|
+
Use `for...of this` to receive fresh args each render cycle. Destructure in the parameter for values needed in init code:
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
function* Counter({ initial }) {
|
|
148
|
+
|
|
149
|
+
let count = initial // parameter destructuring for init code
|
|
150
|
+
|
|
151
|
+
for (const { step = 1 } of this) { // fresh args each cycle
|
|
152
|
+
yield <button set:onclick={() => this.next(() => count += step)}>+{step}</button>
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Use `while (true)` when you don't need args in the loop:
|
|
158
|
+
|
|
159
|
+
```javascript
|
|
160
|
+
function* Timer() {
|
|
161
|
+
let seconds = 0
|
|
162
|
+
setInterval(() => this.next(() => seconds++), 1000)
|
|
163
|
+
while (true) yield <p>{seconds}s</p>
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Lifecycle and Cleanup
|
|
168
|
+
|
|
169
|
+
Every stateful component has a `this.signal` (AbortSignal) that aborts when the component unmounts. Use it with any API that accepts a signal:
|
|
170
|
+
|
|
171
|
+
```javascript
|
|
172
|
+
function* MouseTracker() {
|
|
173
|
+
|
|
174
|
+
let pos = { x: 0, y: 0 }
|
|
175
|
+
|
|
176
|
+
document.addEventListener('mousemove', e => this.next(() => {
|
|
177
|
+
pos = { x: e.clientX, y: e.clientY }
|
|
178
|
+
}), { signal: this.signal }) // auto-removed on unmount
|
|
179
|
+
|
|
180
|
+
while (true) yield <p>{pos.x}, {pos.y}</p>
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
For APIs that don't accept a signal, use `try...finally`:
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
function* Clock() {
|
|
188
|
+
|
|
189
|
+
let time = new Date()
|
|
190
|
+
|
|
191
|
+
const interval = setInterval(() => this.next(() => time = new Date()), 1000)
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
while (true) yield <p>{time.toLocaleTimeString()}</p>
|
|
195
|
+
} finally {
|
|
196
|
+
clearInterval(interval)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Error Boundaries
|
|
202
|
+
|
|
203
|
+
Use `try...catch` **inside** the loop to catch errors and recover:
|
|
204
|
+
|
|
205
|
+
```javascript
|
|
206
|
+
function* ErrorBoundary() {
|
|
207
|
+
for (const { children } of this) {
|
|
208
|
+
try {
|
|
209
|
+
yield children
|
|
210
|
+
} catch (error) {
|
|
211
|
+
yield (
|
|
212
|
+
<>
|
|
213
|
+
<p>Error: {error.message}</p>
|
|
214
|
+
<button set:onclick={() => this.next()}>Retry</button>
|
|
215
|
+
</>
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Special Attributes
|
|
223
|
+
|
|
224
|
+
| Attribute | Description |
|
|
225
|
+
|-----------|-------------|
|
|
226
|
+
| `key` | Unique identifier for list reconciliation |
|
|
227
|
+
| `ref` | Callback receiving DOM element (or `null` on unmount) |
|
|
228
|
+
| `memo` | Skip reconciliation: `memo={[deps]}`, `memo={value}`, or `memo` (render once) |
|
|
229
|
+
| `skip` | Exclude children from reconciliation (required with `set:innerHTML`) |
|
|
230
|
+
| `set:*` | Set DOM properties instead of HTML attributes |
|
|
231
|
+
| `attr:*` | Force HTML attributes on stateful component wrappers |
|
|
232
|
+
|
|
233
|
+
### `set:` - DOM Properties vs HTML Attributes
|
|
234
|
+
|
|
235
|
+
```javascript
|
|
236
|
+
// Events (always use set:)
|
|
237
|
+
<button set:onclick={handleClick}>Click</button>
|
|
238
|
+
|
|
239
|
+
// Dynamic values that need to sync with state
|
|
240
|
+
<input set:value={text} /> // DOM property (syncs)
|
|
241
|
+
<input value="initial" /> // HTML attribute (initial only)
|
|
242
|
+
|
|
243
|
+
<input type="checkbox" set:checked={bool} />
|
|
244
|
+
<video set:currentTime={0} set:muted />
|
|
245
|
+
|
|
246
|
+
// innerHTML requires skip
|
|
247
|
+
<div set:innerHTML={html} skip />
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### `ref` - DOM Access
|
|
251
|
+
|
|
252
|
+
```javascript
|
|
253
|
+
function* AutoFocus() {
|
|
254
|
+
|
|
255
|
+
let input = null
|
|
256
|
+
|
|
257
|
+
while (true) yield (
|
|
258
|
+
<>
|
|
259
|
+
<input ref={el => el?.focus()} />
|
|
260
|
+
<button set:onclick={() => input?.select()}>Select</button>
|
|
261
|
+
</>
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Ref to stateful component includes control methods
|
|
266
|
+
let timer = null
|
|
267
|
+
<Clock ref={el => timer = el} />
|
|
268
|
+
timer?.next() // trigger re-render from outside
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### `memo` - Performance Optimization
|
|
272
|
+
|
|
273
|
+
```javascript
|
|
274
|
+
<div memo={[user.id]}>...</div> // re-render when user.id changes
|
|
275
|
+
<div memo={count}>...</div> // re-render when count changes
|
|
276
|
+
<footer memo>Static content</footer> // render once, never update
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### `skip` - Third-Party DOM
|
|
280
|
+
|
|
281
|
+
```javascript
|
|
282
|
+
function* Chart() {
|
|
283
|
+
|
|
284
|
+
let chart = null
|
|
285
|
+
|
|
286
|
+
for (const { data } of this) yield (
|
|
287
|
+
<div skip ref={el => el && (chart ??= new ChartLib(el, data))} />
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### `attr:` - Wrapper Attributes
|
|
293
|
+
|
|
294
|
+
```javascript
|
|
295
|
+
<Counter
|
|
296
|
+
initial={0} // → args
|
|
297
|
+
attr:id="main" // → wrapper HTML attribute
|
|
298
|
+
attr:class="widget" // → wrapper HTML attribute
|
|
299
|
+
set:onclick={fn} // → wrapper DOM property
|
|
300
|
+
/>
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Context API
|
|
304
|
+
|
|
305
|
+
Share data across component trees without prop drilling:
|
|
306
|
+
|
|
307
|
+
```javascript
|
|
308
|
+
import { context } from 'ajo/context'
|
|
309
|
+
|
|
310
|
+
const ThemeContext = context('light')
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Stateless**: read only.
|
|
314
|
+
**Stateful**: read/write. Write inside the loop when the value depends on state, or outside for a constant value.
|
|
315
|
+
|
|
316
|
+
```javascript
|
|
317
|
+
// Stateless - read only
|
|
318
|
+
const Card = ({ title }) => {
|
|
319
|
+
const theme = ThemeContext()
|
|
320
|
+
return <div class={`card theme-${theme}`}>{title}</div>
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Stateful - write inside loop (value depends on state)
|
|
324
|
+
function* ThemeProvider() {
|
|
325
|
+
|
|
326
|
+
let theme = 'light'
|
|
327
|
+
|
|
328
|
+
for (const { children } of this) {
|
|
329
|
+
ThemeContext(theme)
|
|
330
|
+
yield (
|
|
331
|
+
<>
|
|
332
|
+
<button set:onclick={() => this.next(() => theme = theme === 'light' ? 'dark' : 'light')}>
|
|
333
|
+
{theme}
|
|
334
|
+
</button>
|
|
335
|
+
{children}
|
|
336
|
+
</>
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Stateful - write outside loop (constant value)
|
|
342
|
+
function* FixedTheme() {
|
|
343
|
+
ThemeContext('dark') // set once at mount
|
|
344
|
+
for (const { children } of this) yield children
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Async Operations
|
|
349
|
+
|
|
350
|
+
```javascript
|
|
351
|
+
function* UserProfile({ id }) {
|
|
352
|
+
|
|
353
|
+
let data = null, error = null, loading = true
|
|
354
|
+
|
|
355
|
+
fetch(`/api/users/${id}`, { signal: this.signal })
|
|
356
|
+
.then(r => r.json())
|
|
357
|
+
.then(d => this.next(() => { data = d; loading = false }))
|
|
358
|
+
.catch(e => this.next(() => { error = e; loading = false }))
|
|
359
|
+
|
|
360
|
+
while (true) {
|
|
361
|
+
if (loading) yield <p>Loading...</p>
|
|
362
|
+
else if (error) yield <p>Error: {error.message}</p>
|
|
363
|
+
else yield <h1>{data.name}</h1>
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## Server-Side Rendering
|
|
369
|
+
|
|
370
|
+
```javascript
|
|
371
|
+
import { render } from 'ajo/html'
|
|
372
|
+
const html = render(<App />)
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## TypeScript
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
import type { Stateless, Stateful, WithChildren } from 'ajo'
|
|
379
|
+
|
|
380
|
+
// Stateless
|
|
381
|
+
type CardProps = WithChildren<{ title: string }>
|
|
382
|
+
const Card: Stateless<CardProps> = ({ title, children }) => (
|
|
383
|
+
<div class="card"><h3>{title}</h3>{children}</div>
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
// Stateful with custom wrapper element
|
|
387
|
+
type CounterProps = { initial: number; step?: number }
|
|
388
|
+
|
|
389
|
+
const Counter: Stateful<CounterProps, 'section'> = function* ({ initial }) {
|
|
390
|
+
|
|
391
|
+
let count = initial
|
|
392
|
+
|
|
393
|
+
for (const { step = 1 } of this) {
|
|
394
|
+
yield <button set:onclick={() => this.next(() => count += step)}>+{step}</button>
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
Counter.is = 'section' // wrapper element (default: 'div')
|
|
399
|
+
Counter.attrs = { class: 'counter' } // default wrapper attributes
|
|
400
|
+
Counter.args = { step: 1 } // default args
|
|
401
|
+
|
|
402
|
+
// Or use stateful() to avoid duplicating 'section':
|
|
403
|
+
// const Counter = stateful(function* ({ initial }: CounterProps) { ... }, 'section')
|
|
404
|
+
|
|
405
|
+
// Ref typing
|
|
406
|
+
let ref: ThisParameterType<typeof Counter> | null = null
|
|
407
|
+
<Counter ref={el => ref = el} initial={0} />
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## API Reference
|
|
411
|
+
|
|
412
|
+
### `ajo`
|
|
413
|
+
| Export | Description |
|
|
414
|
+
|--------|-------------|
|
|
415
|
+
| `render(children, container, start?, end?)` | Render to DOM. Optional `start`/`end` for targeted updates. |
|
|
416
|
+
| `stateful(fn, tag?)` | Create stateful component with type inference for custom wrapper. |
|
|
417
|
+
| `defaults` | Default wrapper tag config (`defaults.tag`). |
|
|
418
|
+
|
|
419
|
+
### `ajo/context`
|
|
420
|
+
| Export | Description |
|
|
421
|
+
|--------|-------------|
|
|
422
|
+
| `context<T>(fallback?)` | Create context. Call with value to write, without to read. |
|
|
423
|
+
|
|
424
|
+
### `ajo/html`
|
|
425
|
+
| Export | Description |
|
|
426
|
+
|--------|-------------|
|
|
427
|
+
| `render(children)` | Render to HTML string. |
|
|
428
|
+
| `html(children)` | Render to HTML generator (yields strings). |
|
|
429
|
+
|
|
430
|
+
### Stateful `this`
|
|
431
|
+
| Property | Description |
|
|
432
|
+
|----------|-------------|
|
|
433
|
+
| `for...of this` | Iterable: yields fresh args each render cycle. |
|
|
434
|
+
| `this.signal` | AbortSignal that aborts on unmount. Pass to `fetch()`, `addEventListener()`, etc. |
|
|
435
|
+
| `this.next(fn?)` | Re-render. Callback receives current args. Returns callback's result. |
|
|
436
|
+
| `this.throw(error)` | Throw to parent boundary. |
|
|
437
|
+
| `this.return(deep?)` | Terminate generator. Pass `true` to also terminate child generators. |
|
|
438
|
+
|
|
439
|
+
`this` is also the wrapper element (`this.addEventListener()`, etc).
|
|
440
|
+
|
|
441
|
+
## For AI Assistants
|
|
442
|
+
|
|
443
|
+
See [LLMs.md](./LLMs.md) for a condensed reference.
|
|
444
|
+
|
|
445
|
+
## License
|
|
446
|
+
|
|
447
|
+
ISC © [Cristian Falcone](https://cristianfalcone.com)
|