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/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 factory:
47
-
48
- **Vite:**
49
- ```typescript
50
- // vite.config.ts
51
- import { defineConfig } from 'vite'
52
-
53
- export default defineConfig({
54
- esbuild: {
55
- jsxFactory: 'h',
56
- jsxFragment: 'Fragment',
57
- jsxInject: `import { h, Fragment } from 'ajo'`,
58
- },
59
- })
60
- ```
61
-
62
- **TypeScript:**
63
- ```json
64
- {
65
- "compilerOptions": {
66
- "jsx": "react",
67
- "jsxFactory": "h",
68
- "jsxFragmentFactory": "Fragment"
69
- }
70
- }
71
- ```
72
-
73
- **Other build tools:** Set `jsxFactory: 'h'`, `jsxFragment: 'Fragment'`, and auto-import `{ h, Fragment }` from `'ajo'`.
74
-
75
- ## Core Concepts
76
-
77
- ### Stateless Components
78
-
79
- Pure functions that receive props and return JSX:
80
-
81
- ```javascript
82
- const Greeting = ({ name }) => <p>Hello, {name}!</p>
83
- ```
84
-
85
- ### Stateful Components
86
-
87
- Generator functions with automatic wrapper elements. The structure provides a natural mental model:
88
-
89
- - **Before the loop**: Persistent state and handlers (survives re-renders)
90
- - **Inside the loop**: Derived values (computed fresh each render)
91
-
92
- ```javascript
93
- function* TodoList() {
94
-
95
- let todos = []
96
- let text = ''
97
-
98
- const add = () => this.next(() => {
99
- if (text.trim()) {
100
- todos.push({ id: Date.now(), text })
101
- text = ''
102
- }
103
- })
104
-
105
- while (true) {
106
-
107
- const count = todos.length
108
-
109
- yield (
110
- <>
111
- <input
112
- set:value={text}
113
- set:oninput={e => text = e.target.value}
114
- set:onkeydown={e => e.key === 'Enter' && add()}
115
- />
116
- <button set:onclick={add}>Add ({count})</button>
117
- <ul>
118
- {todos.map(t => <li key={t.id}>{t.text}</li>)}
119
- </ul>
120
- </>
121
- )
122
- }
123
- }
124
- ```
125
-
126
- ### Re-rendering with `this.next()`
127
-
128
- Call `this.next()` to trigger a re-render. The optional callback receives current props, useful when props may have changed:
129
-
130
- ```javascript
131
- function* Stepper(args) {
132
-
133
- let count = 0
134
-
135
- // Access current props in callback
136
- const inc = () => this.next(({ step = 1 }) => count += step)
137
-
138
- while (true) yield (
139
- <button set:onclick={inc}>Count: {count} (+{args.step})</button>
140
- )
141
- }
142
- ```
143
-
144
- **Never destructure args in the generator signature** - it locks values to initial props:
145
-
146
- ```javascript
147
- // DON'T - values frozen at mount
148
- function* Bad({ step }) { let count = step }
149
-
150
- // DO - use args directly, or destructure inside the loop
151
- function* Good(args) {
152
- while (true) {
153
- const { step } = args // fresh each render
154
- yield ...
155
- }
156
- }
157
- ```
158
-
159
- ### Lifecycle and Cleanup
160
-
161
- Use `try...finally` for cleanup when the component unmounts:
162
-
163
- ```javascript
164
- function* Clock() {
165
-
166
- let time = new Date()
167
-
168
- const interval = setInterval(() => this.next(() => time = new Date()), 1000)
169
-
170
- try {
171
- while (true) yield <p>{time.toLocaleTimeString()}</p>
172
- } finally {
173
- clearInterval(interval)
174
- }
175
- }
176
- ```
177
-
178
- ### Error Boundaries
179
-
180
- Use `try...catch` **inside** the loop to catch errors and recover:
181
-
182
- ```javascript
183
- function* ErrorBoundary(args) {
184
- while (true) {
185
- try {
186
- yield args.children
187
- } catch (error) {
188
- yield (
189
- <>
190
- <p>Error: {error.message}</p>
191
- <button set:onclick={() => this.next()}>Retry</button>
192
- </>
193
- )
194
- }
195
- }
196
- }
197
- ```
198
-
199
- ## Special Attributes
200
-
201
- | Attribute | Description |
202
- |-----------|-------------|
203
- | `key` | Unique identifier for list reconciliation |
204
- | `ref` | Callback receiving DOM element (or `null` on unmount) |
205
- | `memo` | Skip reconciliation: `memo={[deps]}`, `memo={value}`, or `memo` (render once) |
206
- | `skip` | Exclude children from reconciliation (required with `set:innerHTML`) |
207
- | `set:*` | Set DOM properties instead of HTML attributes |
208
- | `attr:*` | Force HTML attributes on stateful component wrappers |
209
-
210
- ### `set:` - DOM Properties vs HTML Attributes
211
-
212
- ```javascript
213
- // Events (always use set:)
214
- <button set:onclick={handleClick}>Click</button>
215
-
216
- // Dynamic values that need to sync with state
217
- <input set:value={text} /> // DOM property (syncs)
218
- <input value="initial" /> // HTML attribute (initial only)
219
-
220
- <input type="checkbox" set:checked={bool} />
221
- <video set:currentTime={0} set:muted />
222
-
223
- // innerHTML requires skip
224
- <div set:innerHTML={html} skip />
225
- ```
226
-
227
- ### `ref` - DOM Access
228
-
229
- ```javascript
230
- function* AutoFocus() {
231
-
232
- let input = null
233
-
234
- while (true) yield (
235
- <>
236
- <input ref={el => el?.focus()} />
237
- <button set:onclick={() => input?.select()}>Select</button>
238
- </>
239
- )
240
- }
241
-
242
- // Ref to stateful component includes control methods
243
- let timer = null
244
- <Clock ref={el => timer = el} />
245
- timer?.next() // trigger re-render from outside
246
- ```
247
-
248
- ### `memo` - Performance Optimization
249
-
250
- ```javascript
251
- <div memo={[user.id]}>...</div> // re-render when user.id changes
252
- <div memo={count}>...</div> // re-render when count changes
253
- <footer memo>Static content</footer> // render once, never update
254
- ```
255
-
256
- ### `skip` - Third-Party DOM
257
-
258
- ```javascript
259
- function* Chart(args) {
260
-
261
- let chart = null
262
-
263
- while (true) yield (
264
- <div skip ref={el => el && (chart ??= new ChartLib(el, args.data))} />
265
- )
266
- }
267
- ```
268
-
269
- ### `attr:` - Wrapper Attributes
270
-
271
- ```javascript
272
- <Counter
273
- initial={0} // → args
274
- attr:id="main" // wrapper HTML attribute
275
- attr:class="widget" // wrapper HTML attribute
276
- set:onclick={fn} // wrapper DOM property
277
- />
278
- ```
279
-
280
- ## Context API
281
-
282
- Share data across component trees without prop drilling:
283
-
284
- ```javascript
285
- import { context } from 'ajo/context'
286
-
287
- const ThemeContext = context('light')
288
- ```
289
-
290
- **Stateless**: read only.
291
- **Stateful**: read/write. Write inside the loop to update each render, or outside for a one-time set.
292
-
293
- ```javascript
294
- // Stateless - read only
295
- const Card = ({ title }) => {
296
- const theme = ThemeContext()
297
- return <div class={`card theme-${theme}`}>{title}</div>
298
- }
299
-
300
- // Stateful - write inside loop (updates each render)
301
- function* ThemeProvider(args) {
302
-
303
- let theme = 'light'
304
-
305
- while (true) {
306
- ThemeContext(theme)
307
- yield (
308
- <>
309
- <button set:onclick={() => this.next(() => theme = theme === 'light' ? 'dark' : 'light')}>
310
- {theme}
311
- </button>
312
- {args.children}
313
- </>
314
- )
315
- }
316
- }
317
-
318
- // Stateful - write outside loop (one-time set)
319
- function* FixedTheme(args) {
320
- ThemeContext('dark') // set once at mount
321
- while (true) yield args.children
322
- }
323
- ```
324
-
325
- ## Async Operations
326
-
327
- ```javascript
328
- function* UserProfile(args) {
329
-
330
- let data = null, error = null, loading = true
331
-
332
- fetch(`/api/users/${args.id}`)
333
- .then(r => r.json())
334
- .then(d => this.next(() => { data = d; loading = false }))
335
- .catch(e => this.next(() => { error = e; loading = false }))
336
-
337
- while (true) {
338
- if (loading) yield <p>Loading...</p>
339
- else if (error) yield <p>Error: {error.message}</p>
340
- else yield <h1>{data.name}</h1>
341
- }
342
- }
343
- ```
344
-
345
- ## Server-Side Rendering
346
-
347
- ```javascript
348
- import { render } from 'ajo/html'
349
- const html = render(<App />)
350
- ```
351
-
352
- ## TypeScript
353
-
354
- ```typescript
355
- import type { Stateless, Stateful, WithChildren } from 'ajo'
356
-
357
- // Stateless
358
- type CardProps = WithChildren<{ title: string }>
359
- const Card: Stateless<CardProps> = ({ title, children }) => (
360
- <div class="card"><h3>{title}</h3>{children}</div>
361
- )
362
-
363
- // Stateful with custom wrapper element
364
- type CounterProps = { initial: number; step?: number }
365
-
366
- const Counter: Stateful<CounterProps, 'section'> = function* (args) {
367
-
368
- let count = args.initial
369
-
370
- while (true) {
371
- const { step = 1 } = args
372
- yield <button set:onclick={() => this.next(() => count += step)}>+{step}</button>
373
- }
374
- }
375
-
376
- Counter.is = 'section' // wrapper element (default: 'div')
377
- Counter.attrs = { class: 'counter' } // default wrapper attributes
378
- Counter.args = { step: 1 } // default args
379
-
380
- // Ref typing
381
- let ref: ThisParameterType<typeof Counter> | null = null
382
- <Counter ref={el => ref = el} initial={0} />
383
- ```
384
-
385
- ## API Reference
386
-
387
- ### `ajo`
388
- | Export | Description |
389
- |--------|-------------|
390
- | `render(children, container, start?, end?)` | Render to DOM. Optional `start`/`end` for targeted updates. |
391
- | `h`, `Fragment` | JSX factory and fragment |
392
-
393
- ### `ajo/context`
394
- | Export | Description |
395
- |--------|-------------|
396
- | `context<T>(fallback?)` | Create context. Call with value to write, without to read. |
397
-
398
- ### `ajo/html`
399
- | Export | Description |
400
- |--------|-------------|
401
- | `render(children)` | Render to HTML string |
402
-
403
- ### Stateful `this`
404
- | Method | Description |
405
- |--------|-------------|
406
- | `this.next(fn?)` | Re-render. Callback receives current args. |
407
- | `this.throw(error)` | Throw to parent boundary |
408
- | `this.return()` | Terminate generator |
409
-
410
- `this` is also the wrapper element (`this.addEventListener()`, etc).
411
-
412
- ## For AI Assistants
413
-
414
- See [LLMs.md](./LLMs.md) for a condensed reference.
415
-
416
- ## License
417
-
418
- ISC © [Cristian Falcone](https://cristianfalcone.com)
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)