ajo 0.1.34 → 0.1.35
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 +111 -19
- package/dist/html.js +1 -1
- package/dist/index.js +1 -1
- package/html.js +5 -3
- package/index.js +363 -401
- package/jsx.js +5 -5
- package/package.json +1 -1
- package/readme.md +93 -34
- package/types.ts +12 -12
package/jsx.js
CHANGED
|
@@ -4,15 +4,15 @@ export const mark = v => (v[VNode] = v, v)
|
|
|
4
4
|
|
|
5
5
|
export const isVNode = v => v?.[VNode] === v
|
|
6
6
|
|
|
7
|
-
export const Fragment =
|
|
7
|
+
export const Fragment = args => args.children
|
|
8
8
|
|
|
9
|
-
export function jsx(type,
|
|
9
|
+
export function jsx(type, args, key) {
|
|
10
10
|
|
|
11
|
-
(
|
|
11
|
+
(args ??= {}).nodeName = type
|
|
12
12
|
|
|
13
|
-
if (key != null)
|
|
13
|
+
if (key != null) args.key = key
|
|
14
14
|
|
|
15
|
-
return mark(
|
|
15
|
+
return mark(args)
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export const jsxs = jsx
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -74,7 +74,7 @@ export default defineConfig({
|
|
|
74
74
|
|
|
75
75
|
### Stateless Components
|
|
76
76
|
|
|
77
|
-
Pure functions that receive
|
|
77
|
+
Pure functions that receive args and return JSX:
|
|
78
78
|
|
|
79
79
|
```javascript
|
|
80
80
|
const Greeting = ({ name }) => <p>Hello, {name}!</p>
|
|
@@ -82,7 +82,7 @@ const Greeting = ({ name }) => <p>Hello, {name}!</p>
|
|
|
82
82
|
|
|
83
83
|
### Stateful Components
|
|
84
84
|
|
|
85
|
-
Generator functions with automatic wrapper
|
|
85
|
+
Generator functions with an automatic **host** — a wrapper DOM element Ajo creates for each stateful component and uses to store its state and lifecycle. The structure provides a natural mental model:
|
|
86
86
|
|
|
87
87
|
- **Before the loop**: Persistent state and handlers (survives re-renders)
|
|
88
88
|
- **Inside the loop**: Derived values (computed fresh each render)
|
|
@@ -226,9 +226,9 @@ function* ErrorBoundary() {
|
|
|
226
226
|
| `key` | Unique identifier for list reconciliation |
|
|
227
227
|
| `ref` | Callback receiving DOM element (or `null` on unmount) |
|
|
228
228
|
| `memo` | Skip reconciliation: `memo={[deps]}`, `memo={value}`, or `memo` (render once) |
|
|
229
|
-
| `skip` | Exclude children from reconciliation; not a sanitizer |
|
|
229
|
+
| `skip` | Exclude children from reconciliation; not a sanitizer |
|
|
230
230
|
| `set:*` | Set DOM properties instead of HTML attributes |
|
|
231
|
-
| `attr:*` | Force HTML attributes on stateful component
|
|
231
|
+
| `attr:*` | Force HTML attributes on stateful component hosts |
|
|
232
232
|
|
|
233
233
|
### `set:` - DOM Properties vs HTML Attributes
|
|
234
234
|
|
|
@@ -243,8 +243,8 @@ function* ErrorBoundary() {
|
|
|
243
243
|
<input type="checkbox" set:checked={bool} />
|
|
244
244
|
<video set:currentTime={0} set:muted />
|
|
245
245
|
|
|
246
|
-
// innerHTML is only for trusted/sanitized HTML; skip preserves assigned children
|
|
247
|
-
<div set:innerHTML={trustedHtml} skip />
|
|
246
|
+
// innerHTML is only for trusted/sanitized HTML; skip preserves assigned children
|
|
247
|
+
<div set:innerHTML={trustedHtml} skip />
|
|
248
248
|
```
|
|
249
249
|
|
|
250
250
|
### `ref` - DOM Access
|
|
@@ -254,13 +254,13 @@ function* AutoFocus() {
|
|
|
254
254
|
|
|
255
255
|
let input = null
|
|
256
256
|
|
|
257
|
-
while (true) yield (
|
|
258
|
-
<>
|
|
259
|
-
<input ref={el => { input = el; el?.focus() }} />
|
|
260
|
-
<button set:onclick={() => input?.select()}>Select</button>
|
|
261
|
-
</>
|
|
262
|
-
)
|
|
263
|
-
}
|
|
257
|
+
while (true) yield (
|
|
258
|
+
<>
|
|
259
|
+
<input ref={el => { input = el; el?.focus() }} />
|
|
260
|
+
<button set:onclick={() => input?.select()}>Select</button>
|
|
261
|
+
</>
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
264
|
|
|
265
265
|
// Ref to stateful component includes control methods
|
|
266
266
|
let timer = null
|
|
@@ -289,20 +289,20 @@ function* Chart() {
|
|
|
289
289
|
}
|
|
290
290
|
```
|
|
291
291
|
|
|
292
|
-
### `attr:` -
|
|
292
|
+
### `attr:` - Host Attributes
|
|
293
293
|
|
|
294
294
|
```javascript
|
|
295
295
|
<Counter
|
|
296
296
|
initial={0} // → args
|
|
297
|
-
attr:id="main" // →
|
|
298
|
-
attr:class="widget" // →
|
|
299
|
-
set:onclick={fn} // →
|
|
297
|
+
attr:id="main" // → host HTML attribute
|
|
298
|
+
attr:class="widget" // → host HTML attribute
|
|
299
|
+
set:onclick={fn} // → host DOM property
|
|
300
300
|
/>
|
|
301
301
|
```
|
|
302
302
|
|
|
303
303
|
## Context API
|
|
304
304
|
|
|
305
|
-
Share data across component trees without
|
|
305
|
+
Share data across component trees without passing args down every level:
|
|
306
306
|
|
|
307
307
|
```javascript
|
|
308
308
|
import { context } from 'ajo/context'
|
|
@@ -355,7 +355,7 @@ function* UserProfile({ id }) {
|
|
|
355
355
|
fetch(`/api/users/${id}`, { signal: this.signal })
|
|
356
356
|
.then(r => r.json())
|
|
357
357
|
.then(d => this.next(() => { data = d; loading = false }))
|
|
358
|
-
.catch(e => this.next(() => { error = e; loading = false }))
|
|
358
|
+
.catch(e => e.name == 'AbortError' || this.next(() => { error = e; loading = false }))
|
|
359
359
|
|
|
360
360
|
while (true) {
|
|
361
361
|
if (loading) yield <p>Loading...</p>
|
|
@@ -365,6 +365,61 @@ function* UserProfile({ id }) {
|
|
|
365
365
|
}
|
|
366
366
|
```
|
|
367
367
|
|
|
368
|
+
## Cloves: Sharing Logic
|
|
369
|
+
|
|
370
|
+
Other frameworks need a dedicated mechanism to share component logic — React has hooks (with ordering rules, because state lives in positional slots), Vue has composables (wired into a reactivity graph). Ajo needs neither: a generator body runs once, so closures are real, and the component instance already exposes everything shared logic needs — `this.signal` for cleanup and `this.next()` for invalidation.
|
|
371
|
+
|
|
372
|
+
A **clove** (ajo is garlic; a bulb is made of cloves) is that idea as a convention: a plain function that takes the host and returns a live view of one concern.
|
|
373
|
+
|
|
374
|
+
```javascript
|
|
375
|
+
const pointer = host => {
|
|
376
|
+
|
|
377
|
+
const pos = { x: 0, y: 0 }
|
|
378
|
+
|
|
379
|
+
document.addEventListener('pointermove',
|
|
380
|
+
e => host.next(() => { pos.x = e.clientX; pos.y = e.clientY }),
|
|
381
|
+
{ signal: host.signal })
|
|
382
|
+
|
|
383
|
+
return pos
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function* Cursor() {
|
|
387
|
+
const pos = pointer(this)
|
|
388
|
+
while (true) yield <p>{pos.x}, {pos.y}</p>
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
The rules that make a clove reliable:
|
|
393
|
+
|
|
394
|
+
1. **`(host, options?) => view`** — the host (`this`) is always the first argument.
|
|
395
|
+
2. **Cleanup through `host.signal` only.** Listeners take `{ signal: host.signal }`; timers and observers register an `abort` listener. Never return a dispose function. Teardown then composes automatically with unmount *and* with reset (`el.return()` + `el.next()` restarts the generator with a fresh signal, and the clove re-subscribes by re-running setup).
|
|
396
|
+
3. **Invalidation through `host.next(fn)`**, mutating state inside the callback — safe by design: it no-ops after unmount and never re-renders reentrantly.
|
|
397
|
+
4. **The view is a stable reference whose fields mutate** — the same live-object contract as Ajo's `args`. Never reassign or spread it; use getters for derived values.
|
|
398
|
+
5. **Per-render input is an explicit call inside the render loop** (a no-op when unchanged), because in Ajo lifecycle is code position:
|
|
399
|
+
|
|
400
|
+
```javascript
|
|
401
|
+
function* User() {
|
|
402
|
+
const q = loader(this)
|
|
403
|
+
for (const { id } of this) {
|
|
404
|
+
q.load(`/api/users/${id}`)
|
|
405
|
+
yield q.loading ? <p>Loading...</p> : <h1>{q.data.name}</h1>
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
6. **SSR-inert:** guard DOM access (`typeof document != 'undefined' && host.nodeType == 1`) and always return the correctly shaped view, so universal components can call cloves unconditionally.
|
|
411
|
+
|
|
412
|
+
There are no ordering rules — cloves are ordinary function calls, so conditionals are legal and composition is just passing the host along. Don't name them `useX`: that prefix signals React constraints that don't exist here.
|
|
413
|
+
|
|
414
|
+
TypeScript ships a `Host` type for authoring cloves — the host's protocol plus its DOM surface, satisfied by any stateful component's `this`:
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
import type { Host } from 'ajo'
|
|
418
|
+
|
|
419
|
+
const focus = (host: Host) => { /* host.signal, host.next(), host.addEventListener()... */ }
|
|
420
|
+
const input = (host: Host<HTMLInputElement>) => host.value // narrow the element when needed
|
|
421
|
+
```
|
|
422
|
+
|
|
368
423
|
## Server-Side Rendering
|
|
369
424
|
|
|
370
425
|
```javascript
|
|
@@ -378,15 +433,15 @@ const html = render(<App />)
|
|
|
378
433
|
import type { Stateless, Stateful, WithChildren } from 'ajo'
|
|
379
434
|
|
|
380
435
|
// Stateless
|
|
381
|
-
type
|
|
382
|
-
const Card: Stateless<
|
|
436
|
+
type CardArgs = WithChildren<{ title: string }>
|
|
437
|
+
const Card: Stateless<CardArgs> = ({ title, children }) => (
|
|
383
438
|
<div class="card"><h3>{title}</h3>{children}</div>
|
|
384
439
|
)
|
|
385
440
|
|
|
386
|
-
// Stateful with custom
|
|
387
|
-
type
|
|
441
|
+
// Stateful with custom host element
|
|
442
|
+
type CounterArgs = { initial: number; step?: number }
|
|
388
443
|
|
|
389
|
-
const Counter: Stateful<
|
|
444
|
+
const Counter: Stateful<CounterArgs, 'section'> = function* ({ initial }) {
|
|
390
445
|
|
|
391
446
|
let count = initial
|
|
392
447
|
|
|
@@ -395,16 +450,20 @@ const Counter: Stateful<CounterProps, 'section'> = function* ({ initial }) {
|
|
|
395
450
|
}
|
|
396
451
|
}
|
|
397
452
|
|
|
398
|
-
Counter.is = 'section' //
|
|
399
|
-
Counter.attrs = { class: 'counter' } // default
|
|
453
|
+
Counter.is = 'section' // host element (default: 'div')
|
|
454
|
+
Counter.attrs = { class: 'counter' } // default host attributes
|
|
400
455
|
Counter.args = { step: 1 } // default args
|
|
401
456
|
|
|
402
457
|
// Or use stateful() to avoid duplicating 'section':
|
|
403
|
-
// const Counter = stateful(function* ({ initial }:
|
|
458
|
+
// const Counter = stateful(function* ({ initial }: CounterArgs) { ... }, 'section')
|
|
404
459
|
|
|
405
460
|
// Ref typing
|
|
406
461
|
let ref: ThisParameterType<typeof Counter> | null = null
|
|
407
462
|
<Counter ref={el => ref = el} initial={0} />
|
|
463
|
+
|
|
464
|
+
// Clove typing: Host is satisfied by any stateful component's `this`
|
|
465
|
+
import type { Host } from 'ajo'
|
|
466
|
+
const pointer = (host: Host) => { /* ... */ }
|
|
408
467
|
```
|
|
409
468
|
|
|
410
469
|
## API Reference
|
|
@@ -413,8 +472,8 @@ let ref: ThisParameterType<typeof Counter> | null = null
|
|
|
413
472
|
| Export | Description |
|
|
414
473
|
|--------|-------------|
|
|
415
474
|
| `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
|
|
417
|
-
| `defaults` | Default
|
|
475
|
+
| `stateful(fn, tag?)` | Create stateful component with type inference for a custom host. |
|
|
476
|
+
| `defaults` | Default host tag config (`defaults.tag`). |
|
|
418
477
|
|
|
419
478
|
### `ajo/context`
|
|
420
479
|
| Export | Description |
|
|
@@ -424,9 +483,9 @@ let ref: ThisParameterType<typeof Counter> | null = null
|
|
|
424
483
|
### `ajo/html`
|
|
425
484
|
| Export | Description |
|
|
426
485
|
|--------|-------------|
|
|
427
|
-
| `render(children)` | Render to HTML string. |
|
|
428
|
-
| `html(children, emit)` | Render to HTML chunks by calling `emit(chunk)`. |
|
|
429
|
-
| `defaults` | Default SSR
|
|
486
|
+
| `render(children)` | Render to HTML string. |
|
|
487
|
+
| `html(children, emit)` | Render to HTML chunks by calling `emit(chunk)`. |
|
|
488
|
+
| `defaults` | Default SSR host tag config (`defaults.tag`). |
|
|
430
489
|
|
|
431
490
|
### Stateful `this`
|
|
432
491
|
| Property | Description |
|
|
@@ -435,9 +494,9 @@ let ref: ThisParameterType<typeof Counter> | null = null
|
|
|
435
494
|
| `this.signal` | AbortSignal that aborts on unmount. Pass to `fetch()`, `addEventListener()`, etc. |
|
|
436
495
|
| `this.next(fn?)` | Re-render. Callback receives current args. Returns callback's result. |
|
|
437
496
|
| `this.throw(error)` | Throw to parent boundary. |
|
|
438
|
-
| `this.return(deep?)` | Terminate generator. Deep cleanup is the default; pass `false` to skip child generators. |
|
|
497
|
+
| `this.return(deep?)` | Terminate generator. Deep cleanup is the default; pass `false` to skip child generators. |
|
|
439
498
|
|
|
440
|
-
`this` is also the
|
|
499
|
+
`this` is also the host element itself (`this.addEventListener()`, etc).
|
|
441
500
|
|
|
442
501
|
## For AI Assistants
|
|
443
502
|
|
package/types.ts
CHANGED
|
@@ -27,7 +27,7 @@ declare module 'ajo' {
|
|
|
27
27
|
ref: (el: TElement | null) => void,
|
|
28
28
|
} & ElementChildrenAttribute
|
|
29
29
|
|
|
30
|
-
type
|
|
30
|
+
type PropertySetter<TTag> = {
|
|
31
31
|
[K in keyof ElementType<TTag> as `set:${Exclude<K, symbol>}`]: ElementType<TTag>[K]
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -50,32 +50,32 @@ declare module 'ajo' {
|
|
|
50
50
|
type Stateless<TArgs extends Args = {}> = (args: TArgs) => Children
|
|
51
51
|
|
|
52
52
|
type Stateful<TArgs extends Args = {}, TTag extends string = DefaultTag> = {
|
|
53
|
-
(this:
|
|
53
|
+
(this: Host<ElementType<TTag>, TArgs>, args: TArgs): Iterator<Children>
|
|
54
54
|
} & (TTag extends DefaultTag ? { is?: TTag } : { is: TTag }) & {
|
|
55
|
-
attrs?: Partial<
|
|
55
|
+
attrs?: Partial<PropertySetter<TTag> & CommonAttrs> & Args,
|
|
56
56
|
args?: Partial<TArgs>,
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
type Component<TArgs extends Args = {}> = Stateless<TArgs> | Stateful<TArgs>
|
|
60
60
|
|
|
61
61
|
type StatefulArgs<TArgs, TTag> =
|
|
62
|
-
Partial<SpecialAttrs<
|
|
62
|
+
Partial<SpecialAttrs<Host<ElementType<TTag>, TArgs>> & PropertySetter<TTag>> &
|
|
63
63
|
AttrSetter &
|
|
64
64
|
TArgs
|
|
65
65
|
|
|
66
|
-
type
|
|
66
|
+
type Host<TElement extends object = HTMLElement, TArgs = Args> = TElement & {
|
|
67
67
|
[Symbol.iterator](): Iterator<TArgs>,
|
|
68
68
|
signal: AbortSignal,
|
|
69
69
|
next: {
|
|
70
70
|
(): undefined,
|
|
71
|
-
<R>(fn: (this:
|
|
71
|
+
<R>(fn: (this: Host<TElement, TArgs>, args: TArgs) => R): R,
|
|
72
72
|
},
|
|
73
73
|
throw: (value?: unknown) => void,
|
|
74
74
|
return: (deep?: boolean) => void,
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
type HTMLIntrinsicElements = {
|
|
78
|
-
[TTag in Tag]: Partial<
|
|
78
|
+
[TTag in Tag]: Partial<PropertySetter<TTag> & SpecialAttrs<ElementType<TTag>> & CommonAttrs> & Args
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
interface IntrinsicElements extends HTMLIntrinsicElements { }
|
|
@@ -94,7 +94,7 @@ declare module 'ajo' {
|
|
|
94
94
|
function render(h: Children, el: ParentNode, child?: ChildNode | null, ref?: ChildNode | null): void
|
|
95
95
|
|
|
96
96
|
function stateful<TArgs extends Args, TTag extends string = DefaultTag>(
|
|
97
|
-
fn: (this:
|
|
97
|
+
fn: (this: Host<ElementType<TTag>, TArgs>, args: TArgs) => Iterator<Children>,
|
|
98
98
|
is?: TTag
|
|
99
99
|
): Stateful<TArgs, TTag>
|
|
100
100
|
}
|
|
@@ -104,7 +104,7 @@ declare module 'ajo/context' {
|
|
|
104
104
|
(): T
|
|
105
105
|
<V extends T>(value: V): V
|
|
106
106
|
}
|
|
107
|
-
function current(): import('ajo').
|
|
107
|
+
function current(): import('ajo').Host<object, any> | null
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
declare module 'ajo/html' {
|
|
@@ -116,9 +116,9 @@ declare module 'ajo/html' {
|
|
|
116
116
|
declare module 'ajo/jsx-runtime' {
|
|
117
117
|
import { Type, Args, VNode, ElementChildrenAttribute, IntrinsicElements as _IE } from 'ajo'
|
|
118
118
|
export function Fragment({ children }: ElementChildrenAttribute): typeof children
|
|
119
|
-
export function jsx(type: Type,
|
|
120
|
-
export function jsxs(type: Type,
|
|
121
|
-
export function jsxDEV(type: Type,
|
|
119
|
+
export function jsx(type: Type, args: Args | null, key?: string | number): VNode
|
|
120
|
+
export function jsxs(type: Type, args: Args | null, key?: string | number): VNode
|
|
121
|
+
export function jsxDEV(type: Type, args: Args | null, key?: string | number): VNode
|
|
122
122
|
export namespace JSX {
|
|
123
123
|
type ElementChildrenAttribute = import('ajo').ElementChildrenAttribute
|
|
124
124
|
type LibraryManagedAttributes<C, P> = import('ajo').ManagedAttributes<C, P>
|