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/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 = props => props.children
7
+ export const Fragment = args => args.children
8
8
 
9
- export function jsx(type, props, key) {
9
+ export function jsx(type, args, key) {
10
10
 
11
- (props ??= {}).nodeName = type
11
+ (args ??= {}).nodeName = type
12
12
 
13
- if (key != null) props.key = key
13
+ if (key != null) args.key = key
14
14
 
15
- return mark(props)
15
+ return mark(args)
16
16
  }
17
17
 
18
18
  export const jsxs = jsx
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ajo",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
4
4
  "description": "ajo is a JavaScript view library for building user interfaces",
5
5
  "type": "module",
6
6
  "types": "./types.ts",
package/readme.md CHANGED
@@ -74,7 +74,7 @@ export default defineConfig({
74
74
 
75
75
  ### Stateless Components
76
76
 
77
- Pure functions that receive props and return JSX:
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 elements. The structure provides a natural mental model:
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 wrappers |
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:` - Wrapper Attributes
292
+ ### `attr:` - Host Attributes
293
293
 
294
294
  ```javascript
295
295
  <Counter
296
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
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 prop drilling:
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 CardProps = WithChildren<{ title: string }>
382
- const Card: Stateless<CardProps> = ({ title, children }) => (
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 wrapper element
387
- type CounterProps = { initial: number; step?: number }
441
+ // Stateful with custom host element
442
+ type CounterArgs = { initial: number; step?: number }
388
443
 
389
- const Counter: Stateful<CounterProps, 'section'> = function* ({ initial }) {
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' // wrapper element (default: 'div')
399
- Counter.attrs = { class: 'counter' } // default wrapper attributes
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 }: CounterProps) { ... }, 'section')
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 wrapper. |
417
- | `defaults` | Default wrapper tag config (`defaults.tag`). |
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 wrapper tag config (`defaults.tag`). |
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 wrapper element (`this.addEventListener()`, etc).
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 PropSetter<TTag> = {
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: StatefulElement<TArgs, TTag>, args: TArgs): Iterator<Children>
53
+ (this: Host<ElementType<TTag>, TArgs>, args: TArgs): Iterator<Children>
54
54
  } & (TTag extends DefaultTag ? { is?: TTag } : { is: TTag }) & {
55
- attrs?: Partial<PropSetter<TTag> & CommonAttrs> & Args,
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<StatefulElement<TArgs, TTag>> & PropSetter<TTag>> &
62
+ Partial<SpecialAttrs<Host<ElementType<TTag>, TArgs>> & PropertySetter<TTag>> &
63
63
  AttrSetter &
64
64
  TArgs
65
65
 
66
- type StatefulElement<TArgs, TTag> = ElementType<TTag> & {
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: StatefulElement<TArgs, TTag>, args: TArgs) => R): R,
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<PropSetter<TTag> & SpecialAttrs<ElementType<TTag>> & CommonAttrs> & Args
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: StatefulElement<TArgs, TTag>, args: TArgs) => Iterator<Children>,
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').StatefulElement<any, any> | null
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, props: Args | null, key?: string | number): VNode
120
- export function jsxs(type: Type, props: Args | null, key?: string | number): VNode
121
- export function jsxDEV(type: Type, props: Args | null, key?: string | number): VNode
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>