ajo 0.1.33 → 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 CHANGED
@@ -74,8 +74,8 @@ const Counter: Stateful<Args, 'section'> = function* ({ initial }) {
74
74
  }
75
75
  }
76
76
 
77
- Counter.is = 'section' // wrapper element (default: div)
78
- Counter.attrs = { class: 'counter-wrap' } // default wrapper attributes
77
+ Counter.is = 'section' // host element (default: div)
78
+ Counter.attrs = { class: 'counter-wrap' } // default host attributes
79
79
  Counter.args = { step: 1 } // default args
80
80
 
81
81
  // Or use stateful() to avoid duplicating 'section':
@@ -83,19 +83,19 @@ Counter.args = { step: 1 } // default args
83
83
  // Counter.attrs = { class: 'counter-wrap' }
84
84
  // Counter.args = { step: 1 }
85
85
 
86
- // Example usage - special attrs apply to wrapper, rest goes to args:
86
+ // Example usage - special attrs apply to the host, rest goes to args:
87
87
 
88
88
  let ref: ThisParameterType<typeof Counter> | null = null
89
89
 
90
90
  <Counter
91
91
  initial={0} step={5} // → args
92
- attr:id="main" attr:class="my-counter" // → wrapper attributes (HTML attrs)
93
- set:onclick={fn} // → wrapper properties (DOM props)
94
- key={id} // → wrapper key
95
- memo={[id]} // → wrapper memo (array)
96
- memo={id} // → wrapper memo (single value)
97
- memo // → wrapper memo (render once)
98
- ref={el => ref = el} // → wrapper ref (el is <section> + .next()/.throw()/.return())
92
+ attr:id="main" attr:class="my-counter" // → host attributes (HTML attrs)
93
+ set:onclick={fn} // → host properties (DOM properties)
94
+ key={id} // → host key
95
+ memo={[id]} // → host memo (array)
96
+ memo={id} // → host memo (single value)
97
+ memo // → host memo (render once)
98
+ ref={el => ref = el} // → host ref (el is <section> + .next()/.throw()/.return())
99
99
  />
100
100
 
101
101
  ref?.next() // trigger re-render from outside
@@ -105,27 +105,29 @@ ref?.next() // trigger re-render from outside
105
105
 
106
106
  | Topic | Rule |
107
107
  |-------|------|
108
- | **Elements** | Everything becomes HTML attributes. `set:prop` assigns DOM properties instead (`node[prop] = value`) |
108
+ | **Elements** | Everything becomes HTML attributes. `set:property` assigns DOM properties instead (`node[property] = value`) |
109
109
  | **Stateless** | Everything goes to `args`. Special attrs like `memo` must be applied to elements inside |
110
- | **Stateful** | `key`, `memo`, `skip`, `ref`, `set:*` apply to implicit wrapper element. `attr:*` sets wrapper attributes. Rest goes to `args` |
110
+ | **Stateful** | `key`, `memo`, `skip`, `ref`, `set:*` apply to the host (the implicit wrapper DOM element every stateful component gets). `attr:*` sets host attributes. Rest goes to `args` |
111
111
  | **Events** | `set:onclick`, `set:oninput`, etc. Never `onClick` |
112
112
  | **Classes** | `class`, never `className`. Must be string, no object/array syntax. Use `clsx()` or template literals |
113
113
  | **Styles** | `style` must be string (`style="color: red"`), not object. No special handling |
114
- | **Args** | Destructure in parameter for init code. Render loops: `for (const { prop } of this)` for fresh args each cycle (new bindings), or `for ({ prop } of this)` without `const` to reassign variables from the outer scope. `while (true)` when you don't need fresh args |
114
+ | **Args** | Destructure in parameter for init code. Render loops: `for (const { step } of this)` for fresh args each cycle (new bindings), or `for ({ step } of this)` without `const` to reassign variables from the outer scope. `while (true)` when you don't need fresh args |
115
115
  | **Generators** | Stateful components are JS generators with a main render loop (`while`/`for...of`). Since they're generators, yields can also appear outside the loop — sequential phases, conditional blocks, etc. are all valid |
116
- | **Root JSX** | Use `<>...</>` in stateful to avoid double wrapper |
116
+ | **Root JSX** | Use `<>...</>` in stateful to avoid an extra element inside the host |
117
117
  | **Re-render** | `this.next(fn?)` — returns `fn`'s result. Safe after unmount (no-op). Use `this.throw(e)` for explicit error routing |
118
118
  | **Context** | `context<T>(fallback)` creates context. Stateless: read only. Stateful: read/write. Reads go inside loop (fresh each render). Constant writes can go before loop; dynamic writes go inside loop |
119
119
  | **Lists** | Always provide unique `key` on elements |
120
+ | **SVG** | Set `xmlns="http://www.w3.org/2000/svg"` on the root `<svg>` element — children inherit the namespace |
120
121
  | **Refs** | `ref={el => ...}` on elements. Receives `null` on unmount. Stateful ref type: `ThisParameterType<typeof Component>` |
121
122
  | **Memo** | `memo={[deps]}` array, `memo={value}` single, or just `memo` (never re-render). Skips subtree if unchanged |
122
123
  | **Skip** | `skip` excludes children from reconciliation; it does not sanitize `set:innerHTML` |
123
- | **Custom wrapper** | `stateful(fn, 'tagname')` sets `.is` and infers `this` type. Or manually: `Stateful<Args, 'tagname'>` + `.is = 'tagname'`. Default is `div` (no need to set) |
124
+ | **Custom host** | `stateful(fn, 'tagname')` sets `.is` and infers `this` type. Or manually: `Stateful<Args, 'tagname'>` + `.is = 'tagname'`. Default is `div` (no need to set) |
124
125
  | **Default attrs** | `.attrs = { class: '...' }` on stateful component generator function |
125
- | **Default args** | `.args = { prop: value }` on stateful component generator function |
126
+ | **Default args** | `.args = { step: 1 }` on stateful component generator function |
126
127
  | **Cleanup** | `this.signal` aborts on lifecycle end (unmount, `this.return()`, generator done). Use for fetch, addEventListener, etc. `try/finally` for the rest |
127
128
  | **Error recovery** | `try { ... } catch { yield error UI }` inside loop |
128
- | **this** | Stateful wrapper element with `.signal`, `.next(fn?)` (returns `fn`'s result), `.throw()`, `.return(deep?)`. Iterable: `for...of this` yields args. Type: `ThisParameterType<typeof Component>` |
129
+ | **this** | The host element with `.signal`, `.next(fn?)` (returns `fn`'s result), `.throw()`, `.return(deep?)`. Iterable: `for...of this` yields args. Type: `ThisParameterType<typeof Component>` or `Host` |
130
+ | **Cloves** | Shared logic = plain function `(host: Host, options?) => live view`. Pass `this` as host. See Cloves section |
129
131
 
130
132
  ## Common Patterns
131
133
 
@@ -144,8 +146,8 @@ const DataLoader: Stateful<LoaderArgs> = function* ({ url }) {
144
146
 
145
147
  fetch(url, { signal: this.signal }) // signal auto-aborts on lifecycle end
146
148
  .then(r => r.json())
147
- .then(d => this.next(() => data = d)) // no-op if unmounted
148
- .catch(e => this.next(() => error = e)) // no-op if unmounted
149
+ .then(d => this.next(() => data = d)) // no-op if unmounted
150
+ .catch(e => e.name == 'AbortError' || this.next(() => error = e)) // skip AbortError: the signal already fired because the component ended
149
151
 
150
152
  while (true) yield (
151
153
  <>
@@ -252,6 +254,97 @@ let map: MapLibrary | null = null
252
254
  <div skip ref={el => el ? (map ??= new MapLibrary(el)) : map?.destroy()} /> // skip lets library control children
253
255
  ```
254
256
 
257
+ ## Cloves — Reusable Logic
258
+
259
+ A clove is Ajo's unit of shared component logic (what hooks/composables are in other frameworks): a plain function that takes the component host (`this`) and returns a live view of one stateful concern. No runtime, no registry, no ordering rules — generators already provide persistent closures, so the instance protocol (`signal` + `next`) is the whole mechanism.
260
+
261
+ ```tsx
262
+ import type { Host } from 'ajo'
263
+
264
+ /** Tracks the pointer position. */
265
+ const pointer = (host: Host) => {
266
+
267
+ const pos = { x: 0, y: 0 }
268
+
269
+ if (typeof document != 'undefined' && host.nodeType == 1) // SSR guard: inert view on the server
270
+ document.addEventListener('pointermove',
271
+ e => host.next(() => { pos.x = e.clientX; pos.y = e.clientY }),
272
+ { signal: host.signal }) // teardown on unmount AND on reset, automatically
273
+
274
+ return pos
275
+ }
276
+
277
+ function* Cursor() {
278
+ const pos = pointer(this) // setup phase: call before the render loop
279
+ while (true) yield <p>{pos.x}, {pos.y}</p>
280
+ }
281
+ ```
282
+
283
+ **The contract — all six rules are mandatory when writing a clove:**
284
+
285
+ 1. **Signature**: `(host, options?) => view`. Host first. Options in a single object with defaults, never positional flags.
286
+ 2. **Cleanup goes through `host.signal`, exclusively.** Pass `{ signal: host.signal }` to `addEventListener`; use `host.signal.addEventListener('abort', () => ...)` for timers, observers, sockets. Never return a dispose function. This makes teardown compose with unmount and with explicit reset (`el.return()` + `el.next()` restarts the generator with a fresh signal, so setup re-runs and the clove re-subscribes with zero extra code).
287
+ 3. **Invalidation goes through `host.next(fn)`**, mutating state inside `fn`. Already safe: no-op after unmount, no reentrant render while rendering.
288
+ 4. **The view is a stable reference.** Mutate its fields; never reassign or spread it (that breaks liveness — same contract as Ajo's own live `args`). Use getters for derived values. Name state as nouns (`x`, `open`, `data`), methods as verbs (`load`, `start`).
289
+ 5. **Per-render input is an explicit method call inside the render loop**, internally a no-op when input is unchanged. No hidden dependency tracking.
290
+ 6. **SSR-inert by shape.** During `ajo/html` SSR the host is protocol-only (`next` is a noop, no DOM). Guard DOM access (`typeof document != 'undefined' && host.nodeType == 1`) and always return the correctly shaped view so universal components can call cloves unconditionally.
291
+
292
+ Per-render input + async, the canonical shape:
293
+
294
+ ```tsx
295
+ /** Keyed async loader: chains host.signal, re-loads only when the key changes. */
296
+ const loader = <T,>(host: Host) => {
297
+
298
+ let key: string | undefined
299
+
300
+ const q = {
301
+ data: null as T | null, error: null as Error | null, loading: false,
302
+ load(url: string) {
303
+ if (url == key) return // per-render no-op: rule 5
304
+ key = url
305
+ q.loading = true
306
+ q.data = q.error = null
307
+ fetch(url, { signal: host.signal })
308
+ .then(r => r.json())
309
+ .then(d => host.next(() => { q.data = d; q.loading = false }))
310
+ .catch(e => e.name == 'AbortError' || host.next(() => { q.error = e; q.loading = false }))
311
+ },
312
+ }
313
+
314
+ return q
315
+ }
316
+
317
+ function* User() {
318
+ const q = loader<{ name: string }>(this)
319
+ for (const { id } of this) {
320
+ q.load(`/api/users/${id}`) // reacts to args because it runs every render
321
+ yield q.loading ? <p>Loading...</p> : q.error ? <p>{q.error.message}</p> : <h1>{q.data!.name}</h1>
322
+ }
323
+ }
324
+ ```
325
+
326
+ Composition is function calls sharing the host — a clove may call other cloves, and conditional calls are legal (there are no hook rules):
327
+
328
+ ```tsx
329
+ const idle = (host: Host, ms = 60000) => { /* uses timer(host) + pointer(host) internally */ }
330
+
331
+ function* App(args: { track?: boolean }) {
332
+ const t = args.track ? idle(this) : { idle: false } // conditional: fine
333
+ while (true) yield <p>{t.idle ? 'away' : 'here'}</p>
334
+ }
335
+ ```
336
+
337
+ Shared state across components is a clove factory: module-level state + a `Set` of hosts, `next()` on all hosts when mutating, each host auto-unsubscribed via its signal.
338
+
339
+ ```tsx
340
+ // ❌ Clove anti-patterns
341
+ const bad1 = (host: Host) => { /* ... */ return () => cleanup() } // dispose fn: use host.signal
342
+ const bad2 = (host: Host) => ({ ...state }) // copy: breaks liveness, return the live object
343
+ const useBad3 = (host: Host) => { } // use* prefix: signals React rules that don't exist
344
+ setInterval(() => host.next(), 1000) // leak: no abort wiring
345
+ fetch(url).catch(e => host.next(() => error = e)) // missing host.signal + AbortError filter
346
+ ```
347
+
255
348
  ## Anti-patterns
256
349
 
257
350
  ```tsx
@@ -261,7 +354,7 @@ className="..."
261
354
  onClick={...}
262
355
  useState, useEffect, useCallback
263
356
 
264
- // ❌ class/style as object or array: no special handling in Ajo
357
+ // ❌ class/style as object or array: no special handling in Ajo (compile error)
265
358
  <div class={{ active: isActive }} /> // won't work
266
359
  <div class={['btn', 'primary']} /> // won't work
267
360
  <div style={{ color: 'red' }} /> // won't work
@@ -337,10 +430,17 @@ pnpm add ajo
337
430
  yarn add ajo
338
431
  ```
339
432
 
340
- Configure JSX automatic runtime (Vite example):
433
+ Configure the JSX automatic runtime in the build tool:
341
434
 
342
435
  ```ts
343
- // vite.config.ts
436
+ // vite.config.ts (Vite 8+, rolldown/oxc)
437
+ export default defineConfig({
438
+ oxc: {
439
+ jsx: { importSource: 'ajo' },
440
+ },
441
+ })
442
+
443
+ // vite.config.ts (Vite 7 and below, esbuild)
344
444
  export default defineConfig({
345
445
  esbuild: {
346
446
  jsx: 'automatic',
@@ -349,4 +449,30 @@ export default defineConfig({
349
449
  })
350
450
  ```
351
451
 
452
+ And in tsconfig, so JSX types resolve in the editor:
453
+
454
+ ```jsonc
455
+ // tsconfig.json
456
+ {
457
+ "compilerOptions": {
458
+ "jsx": "react-jsx",
459
+ "jsxImportSource": "ajo"
460
+ }
461
+ }
462
+ ```
463
+
352
464
  For other build systems: `jsx: 'react-jsx'` (or `'automatic'`), `jsxImportSource: 'ajo'`. No manual imports needed — the build tool auto-imports from `ajo/jsx-runtime`.
465
+
466
+ Mount and server-render:
467
+
468
+ ```tsx
469
+ import { render } from 'ajo'
470
+
471
+ render(<App />, document.getElementById('root')!)
472
+ // render(h, el, child?, ref?) — optional child/ref bound the reconciled
473
+ // range to [child, ref) for hydration or partial updates inside el
474
+
475
+ import { render as toString } from 'ajo/html'
476
+
477
+ const html = toString(<App />) // SSR: stateful components run one iteration, then finalize
478
+ ```
package/dist/html.js CHANGED
@@ -1 +1 @@
1
- import{isVNode as t,mark as n}from"./jsx.js";import{Context as e,current as o}from"./context.js";var{keys:r}=Object,a=new Set(["area","base","br","col","command","embed","hr","img","input","keygen","link","meta","param","source","track","wbr"]),l=Symbol.for("ajo.args"),s=e=>e.replace(/[&<>"']/g,e=>`&#${e.charCodeAt(0)};`),i=/^[A-Za-z][\w:.-]*$/,m=/^[^\s"'/>=\0-\x1F\x7F]+$/,f=()=>{},c={tag:"div"},u=e=>{let t="";return d(e,e=>t+=e),t},d=(e,r)=>{if(null==e)return;let a=typeof e;if("boolean"!=a)if("string"==a)r(s(e));else if("number"==a||"bigint"==a)r(s(String(e)));else if(t(e))"function"==typeof e.nodeName?b(e,r):g(e,r);else if(Symbol.iterator in Object(e))for(e of e)d(e,r);else r(s(String(e)))},g=(e,t)=>{let{nodeName:o,children:n}=e;o="string"==typeof o&&i.test(o)?o:c.tag;let l="";for(let t of r(e))"nodeName"==t||"children"==t||"key"==t||"skip"==t||"memo"==t||"ref"==t||t.startsWith("set:")||!m.test(t)||null==e[t]||!1===e[t]||(!0===e[t]?l+=` ${t}`:l+=` ${t}="${s(String(e[t]))}"`);t(`<${o}${l}>`),a.has(o)||(null!=n&&d(n,t),t(`</${o}>`))},b=({nodeName:e,...t},r)=>{"GeneratorFunction"==e.constructor.name?h(e,t,r):d(e(t),r)},h=(t,a,s)=>{let i={...t.attrs},m={...t.args};for(let e of r(a))e.startsWith("attr:")?i[e.slice(5)]=a[e]:"key"==e||"skip"==e||"memo"==e||"ref"==e||e.startsWith("set:")?i[e]=a[e]:m[e]=a[e];let u=new AbortController,d={*[Symbol.iterator](){for(;;)yield this[l]},[e]:Object.create(o()?.[e]??null),[l]:m,signal:u.signal,next:f,return:f,throw:e=>{throw e}},b=t.call(d,m),h=o();o(d);let y=e=>n({...i,nodeName:t.is??c.tag,children:e}),p="";try{g(y(b.next().value),e=>p+=e)}catch(t){p="",g(y(b.throw(t).value),e=>p+=e)}finally{b.return(),u.abort(),o(h)}s(p)};export{c as defaults,d as html,u as render};
1
+ import{isVNode as t}from"./jsx.js";import{Context as n,current as e}from"./context.js";var{keys:o}=Object,r=new Set(["area","base","br","col","command","embed","hr","img","input","keygen","link","meta","param","source","track","wbr"]),a=Symbol.for("ajo.args"),l=e=>e.replace(/[&<>"']/g,e=>`&#${e.charCodeAt(0)};`),i=/^[A-Za-z][\w:.-]*$/,s=/^[^\s"'/>=\0-\x1F\x7F]+$/,f=()=>{},m=e=>"key"==e||"skip"==e||"memo"==e||"ref"==e||e.startsWith("set:"),c={tag:"div"},d=e=>{let t="";return g(e,e=>t+=e),t},g=(e,r)=>{if(null==e)return;let a=typeof e;if("boolean"!=a)if("string"==a)r(l(e));else if("number"==a||"bigint"==a)r(l(String(e)));else if(t(e))"function"==typeof e.nodeName?b(e,r):u(e,r);else if(Symbol.iterator in Object(e))for(e of e)g(e,r);else r(l(String(e)))},u=(e,t)=>{let{nodeName:a,children:n}=e;a="string"==typeof a&&i.test(a)?a:c.tag;let f="";for(let t of o(e))"nodeName"==t||"children"==t||m(t)||!s.test(t)||null==e[t]||!1===e[t]||(!0===e[t]?f+=` ${t}`:f+=` ${t}="${l(String(e[t]))}"`);t(`<${a}${f}>`),r.has(a)||(null!=n&&g(n,t),t(`</${a}>`))},b=({nodeName:e,...t},r)=>{e.prototype?.next?h(e,t,r):g(e(t),r)},h=(t,r,l)=>{let i={...t.attrs},s={...t.args};for(let e of o(r))e.startsWith("attr:")?i[e.slice(5)]=r[e]:m(e)?i[e]=r[e]:s[e]=r[e];i.nodeName=t.is??c.tag;let d=new AbortController,g={*[Symbol.iterator](){for(;;)yield this[a]},[n]:Object.create(e()?.[n]??null),[a]:s,signal:d.signal,next:f,return:f,throw:e=>{throw e}},b=t.call(g,s),h=e();e(g);let y="",p=e=>y+=e;try{i.children=b.next().value,u(i,p)}catch(t){y="",i.children=b.throw(t).value,u(i,p)}finally{b.return(),d.abort(),e(h)}l(y)};export{c as defaults,g as html,d as render};
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{isVNode as t,mark as n}from"./jsx.js";import{Context as e,current as o}from"./context.js";var y,{isArray:r}=Array,{assign:a,hasOwn:l,keys:s}=Object,i=Symbol.for("ajo.key"),m=Symbol.for("ajo.keyed"),f=Symbol.for("ajo.memo"),c=Symbol.for("ajo.cache"),u=Symbol.for("ajo.generator"),d=Symbol.for("ajo.iterator"),g=Symbol.for("ajo.render"),b=Symbol.for("ajo.args"),h=Symbol.for("ajo.controller"),p={tag:"div"},S=(e,t)=>(t&&(e.is=t),e),x=(e,t,r=t.firstChild,n=null)=>{if(C(e,e=>{let i="string"==typeof e?E(e,r,n):k(e,t,r,n);null==r?A(t,i,n):i==r?r=i.nextSibling:i==r.nextSibling?(A(t,r,n),r=i.nextSibling):A(t,i,r)}),r&&!n&&r==t.firstChild){let e=[];for(t[m]?.clear();r;)e.push(r),r=r.nextSibling;for(r of(t.textContent="",e))1==r.nodeType&&O(r)}else for(;r&&r!=n;){let e=r.nextSibling,n=t.removeChild(r);1==n.nodeType&&O(n,t),r=e}},C=(e,r)=>{if(null==e)return;let n=typeof e;if("boolean"!=n)if("string"==n)r(e);else if("number"==n||"bigint"==n)r(String(e));else if(t(e))"function"==typeof e.nodeName?N(e,r):r(e);else if(Symbol.iterator in Object(e))for(e of e)C(e,r);else r(String(e))},N=({nodeName:e,...t},r)=>{"GeneratorFunction"==e.constructor.name?r(j(e,t)):C(e(t),r)},j=(e,t)=>{let r={...e.attrs},i={...e.args};for(let e of s(t))e.startsWith("attr:")?r[e.slice(5)]=t[e]:"key"==e||"skip"==e||"memo"==e||"ref"==e||e.startsWith("set:")?r[e]=t[e]:i[e]=t[e];return n({...r,nodeName:e.is??p.tag,[u]:e,[b]:i})},E=(e,t,r)=>{for(;t&&t!=r&&3!=t.nodeType;)t=t.nextSibling;return t&&t!=r?t.data!=e&&(t.data=e):t=document.createTextNode(e),t},k=(e,t,r,n)=>{let o=e.nodeName,l=e.key,s=e[u];for(r&&null!=l&&null==n&&(r=(t[m]??=new Map).get(l)??r);r&&r!=n&&(r.localName!=o||null!=r[i]&&r[i]!=l||r[u]&&r[u]!=s);)r=null==r[i]?r.nextElementSibling:null;return r==n&&(r=null),r??=document.createElementNS(e.xmlns??t.namespaceURI,o),null!=l&&(t[m]??=new Map).set(r[i]=l,r),(!r[c]||null==e.memo||w(r[f],e.memo))&&(v(r[c],e,r),e.skip||(s?T(s,e[b],r):r[c]||"string"!=typeof e.children&&"number"!=typeof e.children?x(e.children,r):r.textContent=e.children+""),r[f]=e.memo,r[c]=e),r},v=(e,t,r)=>{if(!e)for(let e=r.attributes.length;e--;)l(t,r.attributes[e].name)||r.removeAttribute(r.attributes[e].name);for(let n in t)if(l(t,n)){let i=t[n];if(e&&e[n]===i||"nodeName"==n||"children"==n||"key"==n||"skip"==n||"memo"==n)continue;"ref"==n&&"function"==typeof i?i(r):n.startsWith("set:")?r[n.slice(4)]=i:null==i||!1===i?r.removeAttribute(n):"class"==n&&"string"==typeof r.className?r.className=!0===i?"":i:r.setAttribute(n,!0===i?"":i)}if(e)for(let n in e)if(l(e,n)){if(l(t,n)||"nodeName"==n||"children"==n||"key"==n||"skip"==n||"memo"==n)continue;n.startsWith("set:")?r[n.slice(4)]=void 0:r.removeAttribute(n)}},w=(e,t)=>{if(r(e)&&r(t)){if(e.length!=t.length)return!0;for(let r=e.length;r--;)if(e[r]!==t[r])return!0;return!1}return e!==t},A=(e,t,r)=>{if(t.isConnected&&t.contains(document.activeElement)){let n=t.nextSibling;for(;r&&r!=t;){let t=r.nextSibling;e.insertBefore(r,n),r=t}}else e.insertBefore(t,r)},O=(e,t)=>{if(e[f]==O)return;e[f]=O;let r=e;for(;r.firstElementChild;)r=r.firstElementChild;for(;;){let{nextElementSibling:n,parentNode:o}=r;if(null!=r[i]&&(o??t)?.[m]?.delete(r[i]),"function"==typeof r.return&&r.return(!1),"function"==typeof r[c]?.ref&&r[c].ref(null),r===e)break;if(r=n??o??e,n)for(;r.firstElementChild;)r=r.firstElementChild}},T=(t,r,n)=>{n[u]??=(W(),a(n,M)[e]=Object.create(o()?.[e]??null),t);for(let e of s(n[b]??=r))l(r,e)||delete n[b][e];a(n[b],r),n[g]()},W=()=>y||(y=new MutationObserver(e=>{for(let t of e)for(let e of t.removedNodes)1==e.nodeType&&!e.isConnected&&O(e,t.target)})).observe(document,{childList:!0,subtree:!0}),M={*[Symbol.iterator](){for(;;)yield this[b]},[g](){let e=o();o(this);try{this[d]||(this.signal=(this[h]=new AbortController).signal,this[d]=this[u].call(this,this[b]));let{value:e,done:t}=this[d].next();x(e,this),t&&this.return()}catch(e){this.throw(e)}finally{o(e)}},next(e,t){if(!1===this[d]||!this.isConnected)return t;try{"function"==typeof e&&(t=e.call(this,this[b]))}catch(e){return this.throw(e)}return o()?.contains(this)||this[g](),t},throw(e){for(let t=this;t;t=t.parentNode)if(t[d]?.throw)try{return x(t[d].throw(e).value,t)}catch(t){e=Error(t?.message??t,{cause:e})}throw e},return(e=!0){if(!1===this[d])return;let t=this[d];this[d]=!1,e&&((e,t)=>{let r=t.firstElementChild;for(;r;)if(e(r)&&r.firstElementChild)r=r.firstElementChild;else{for(;r!=t&&!r.nextElementSibling;)r=r.parentNode??t;r=r!=t&&r.nextElementSibling}})(e=>"function"!=typeof e.return||e.return(),this);try{t?.return()}catch(e){this.throw(e)}finally{this[d]=null,this[h]?.abort()}}};export{p as defaults,x as render,S as stateful};
1
+ import{isVNode as t}from"./jsx.js";import{Context as n,current as e}from"./context.js";var y,{isArray:o}=Array,{assign:r,hasOwn:a,keys:l}=Object,i=Symbol.for("ajo.key"),s=Symbol.for("ajo.keyed"),f=Symbol.for("ajo.memo"),m=Symbol.for("ajo.cache"),c=Symbol.for("ajo.generator"),d=Symbol.for("ajo.iterator"),g=Symbol.for("ajo.render"),u=Symbol.for("ajo.args"),b=Symbol.for("ajo.controller"),h={tag:"div"},p=(e,t)=>(t&&(e.is=t),e),S=(e,t,r=t.firstChild,n=null)=>{if(x(e,e=>{let o="string"==typeof e?N(e,r,n):v(e,t,r,n);null==r?A(t,o,n):o==r?r=o.nextSibling:o==r.nextSibling?(r.contains(document.activeElement)?t.insertBefore(o,r):t.insertBefore(r,n),r=o.nextSibling):A(t,o,r)}),r&&!n&&r==t.firstChild){let e=[...t.children];t[s]?.clear(),t.textContent="";for(let t of e)B(t)}else for(;r&&r!=n;){let e=r.nextSibling;t.removeChild(r),B(r,t),r=e}},x=(e,r)=>{if(null!=e&&"boolean"!=typeof e)if(t(e)){if("function"!=typeof e.nodeName)return r(e);let{nodeName:t,...n}=e;t.prototype?.next?r(j(t,n)):x(t(n),r)}else if("object"==typeof e&&Symbol.iterator in e)for(e of e)x(e,r);else r(String(e))},j=(e,t)=>{let r={...e.attrs},n={...e.args};for(let e of l(t))e.startsWith("attr:")?r[e.slice(5)]=t[e]:"key"==e||"skip"==e||"memo"==e||"ref"==e||e.startsWith("set:")?r[e]=t[e]:n[e]=t[e];return r.nodeName=e.is??h.tag,r[c]=e,r[u]=n,r},N=(e,t,r)=>{for(;t&&t!=r&&3!=t.nodeType;)t=t.nextSibling;return null==t||t==r?t=document.createTextNode(e):t.data!=e&&(t.data=e),t},v=(e,t,r,n)=>{let o=e.nodeName,l=e.key,a=e[c];for(r&&null!=l&&null==n&&(r=t[s]?.get(l)??r);r&&r!=n;){let e=r[i],t=r[c];if(!(r.localName!=o||null!=e&&e!=l||t&&t!=a))break;r=null==e?r.nextElementSibling:null}return(null==r||r==n)&&(r=document.createElementNS(e.xmlns??t.namespaceURI,o)),null!=l&&(t[s]??=new Map).set(r[i]=l,r),(!r[m]||null==e.memo||k(r[f],e.memo))&&(w(r[m],e,r),e.skip||(a?O(a,e[u],r):r[m]||"string"!=typeof e.children&&"number"!=typeof e.children?S(e.children,r):r.textContent=e.children+""),r[f]=e.memo,r[m]=e),r},C=e=>"nodeName"==e||"children"==e||"key"==e||"skip"==e||"memo"==e,w=(e,t,r)=>{for(let n of e?l(e):r.getAttributeNames())a(t,n)||C(n)||(n.startsWith("set:")?r[n.slice(4)]=void 0:r.removeAttribute(n));for(let n of l(t)){let o=t[n];e&&e[n]===o||C(n)||("ref"==n&&"function"==typeof o?o(r):n.startsWith("set:")?r[n.slice(4)]=o:null==o||!1===o?r.removeAttribute(n):"class"==n&&"string"==typeof r.className?r.className=!0===o?"":o:r.setAttribute(n,!0===o?"":o))}},k=(e,t)=>o(e)&&o(t)?e.length!=t.length||e.some((e,r)=>e!==t[r]):e!==t,E=(e,t)=>{for(let r=t.firstElementChild;r;r=r.nextElementSibling)e(r)&&E(e,r)},A=(e,t,r)=>{if(t.isConnected&&t.contains(document.activeElement)){let n=t.nextSibling;for(;r&&r!=t;){let t=r.nextSibling;e.insertBefore(r,n),r=t}}else e.insertBefore(t,r)},B=(e,t)=>{if(e[f]==B)return;e[f]=B;let r=e=>{for(let t,n=e.firstElementChild;n;n=t)t=n.nextElementSibling,r(n);null!=e[i]&&(e.parentNode??t)?.[s]?.delete(e[i]),"function"==typeof e.return&&e.return(!1),"function"==typeof e[m]?.ref&&e[m].ref(null)};r(e)},O=(t,o,i)=>{i[c]||(y||(y=new MutationObserver(e=>{for(let t of e)for(let e of t.removedNodes)1==e.nodeType&&!e.isConnected&&B(e,t.target)})).observe(document,{childList:!0,subtree:!0}),r(i,W)[n]=Object.create(e()?.[n]??null),i[c]=t);let s=i[u]??=o;for(let e of l(s))a(o,e)||delete s[e];r(s,o),i[g]()},W={*[Symbol.iterator](){for(;;)yield this[u]},[g](){let t=e();e(this);try{this[d]||(this.signal=(this[b]=new AbortController).signal,this[d]=this[c].call(this,this[u]));let{value:e,done:t}=this[d].next();S(e,this),t&&this.return()}catch(t){this.throw(t)}finally{e(t)}},next(t,r){if(!1===this[d]||!this.isConnected)return r;try{"function"==typeof t&&(r=t.call(this,this[u]))}catch(t){return this.throw(t)}return e()?.contains(this)||this[g](),r},throw(e){for(let t=this;t;t=t.parentNode)if(t[d]?.throw)try{return S(t[d].throw(e).value,t)}catch(t){e=Error(t?.message??t,{cause:e})}throw e},return(e=!0){let t=this[d];if(!1!==t){this[d]=!1,e&&E(e=>"function"!=typeof e.return||e.return(),this);try{t?.return()}catch(e){this.throw(e)}finally{this[d]=null,this[b]?.abort()}}}};export{h as defaults,S as render,p as stateful};
package/dist/jsx.js CHANGED
@@ -1 +1 @@
1
- var t=Symbol.for("ajo.vnode"),n=a=>(a[t]=a,a),e=a=>a?.[t]===a,o=a=>a.children;function r(a,s,e){return(s??={}).nodeName=a,null!=e&&(s.key=e),n(s)}var a=r,l=r;export{o as Fragment,e as isVNode,r as jsx,l as jsxDEV,a as jsxs,n as mark};
1
+ var t=Symbol(),n=a=>(a[t]=a,a),e=a=>a?.[t]===a,o=a=>a.children;function r(a,s,e){return(s??={}).nodeName=a,null!=e&&(s.key=e),n(s)}var a=r,l=r;export{o as Fragment,e as isVNode,r as jsx,l as jsxDEV,a as jsxs,n as mark};
package/html.js CHANGED
@@ -1,17 +1,19 @@
1
- import { Context, current } from './context.js'
2
- import { isVNode, mark } from './jsx.js'
3
-
4
- const { keys } = Object
5
-
6
- const Void = new Set(['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'])
7
-
8
- const Args = Symbol.for('ajo.args')
9
-
10
- const escape = s => s.replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`)
11
-
12
- const Tag = /^[A-Za-z][\w:.-]*$/, Attr = /^[^\s"'/>=\0-\x1F\x7F]+$/
13
-
14
- const noop = () => { }
1
+ import { Context, current } from './context.js'
2
+ import { isVNode } from './jsx.js'
3
+
4
+ const { keys } = Object
5
+
6
+ const Void = new Set(['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'])
7
+
8
+ const Args = Symbol.for('ajo.args')
9
+
10
+ const escape = s => s.replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`)
11
+
12
+ const Tag = /^[A-Za-z][\w:.-]*$/, Attr = /^[^\s"'/>=\0-\x1F\x7F]+$/
13
+
14
+ const noop = () => { }
15
+
16
+ const special = key => key == 'key' || key == 'skip' || key == 'memo' || key == 'ref' || key.startsWith('set:')
15
17
 
16
18
  export const defaults = { tag: 'div' }
17
19
 
@@ -24,55 +26,55 @@ export const render = h => {
24
26
  return out
25
27
  }
26
28
 
27
- export const html = (h, emit) => {
28
-
29
- if (h == null) return
30
-
31
- const type = typeof h
32
-
33
- if (type == 'boolean') return
29
+ export const html = (h, emit) => {
30
+
31
+ if (h == null) return
32
+
33
+ const type = typeof h
34
+
35
+ if (type == 'boolean') return
34
36
 
35
37
  if (type == 'string') emit(escape(h))
36
38
 
37
- else if (type == 'number' || type == 'bigint') emit(escape(String(h)))
39
+ else if (type == 'number' || type == 'bigint') emit(escape(String(h)))
40
+
41
+ else if (isVNode(h)) typeof h.nodeName == 'function' ? run(h, emit) : element(h, emit)
38
42
 
39
- else if (isVNode(h)) typeof h.nodeName == 'function' ? run(h, emit) : element(h, emit)
40
-
41
- else if (Symbol.iterator in Object(h)) for (h of h) html(h, emit)
42
-
43
- else emit(escape(String(h)))
44
- }
43
+ else if (Symbol.iterator in Object(h)) for (h of h) html(h, emit)
44
+
45
+ else emit(escape(String(h)))
46
+ }
45
47
 
46
48
  const element = (h, emit) => {
47
49
 
48
- let { nodeName, children } = h
49
-
50
- nodeName = typeof nodeName == 'string' && Tag.test(nodeName) ? nodeName : defaults.tag
51
-
52
- let a = ''
53
-
54
- for (const key of keys(h)) {
55
-
56
- if (key == 'nodeName' || key == 'children' || key == 'key' || key == 'skip' || key == 'memo' || key == 'ref' || key.startsWith('set:') || !Attr.test(key) || h[key] == null || h[key] === false) continue
57
-
58
- if (h[key] === true) a += ` ${key}`
59
-
60
- else a += ` ${key}="${escape(String(h[key]))}"`
61
- }
62
-
63
- emit(`<${nodeName}${a}>`)
64
-
65
- if (!Void.has(nodeName)) {
66
-
67
- if (children != null) html(children, emit)
68
-
69
- emit(`</${nodeName}>`)
50
+ let { nodeName, children } = h
51
+
52
+ nodeName = typeof nodeName == 'string' && Tag.test(nodeName) ? nodeName : defaults.tag
53
+
54
+ let a = ''
55
+
56
+ for (const key of keys(h)) {
57
+
58
+ if (key == 'nodeName' || key == 'children' || special(key) || !Attr.test(key) || h[key] == null || h[key] === false) continue
59
+
60
+ if (h[key] === true) a += ` ${key}`
61
+
62
+ else a += ` ${key}="${escape(String(h[key]))}"`
63
+ }
64
+
65
+ emit(`<${nodeName}${a}>`)
66
+
67
+ if (!Void.has(nodeName)) {
68
+
69
+ if (children != null) html(children, emit)
70
+
71
+ emit(`</${nodeName}>`)
70
72
  }
71
73
  }
72
74
 
73
- const run = ({ nodeName, ...h }, emit) => {
75
+ const run = ({ nodeName, ...h }, emit) => {
74
76
 
75
- if (nodeName.constructor.name == 'GeneratorFunction') runGenerator(nodeName, h, emit)
77
+ if (nodeName.prototype?.next) runGenerator(nodeName, h, emit)
76
78
 
77
79
  else html(nodeName(h), emit)
78
80
  }
@@ -81,20 +83,22 @@ const runGenerator = (fn, h, emit) => {
81
83
 
82
84
  const attrs = { ...fn.attrs }, args = { ...fn.args }
83
85
 
84
- for (const key of keys(h)) {
86
+ for (const key of keys(h)) {
85
87
 
86
88
  if (key.startsWith('attr:')) attrs[key.slice(5)] = h[key]
87
89
 
88
- else if (key == 'key' || key == 'skip' || key == 'memo' || key == 'ref' || key.startsWith('set:')) attrs[key] = h[key]
90
+ else if (special(key)) attrs[key] = h[key]
89
91
 
90
92
  else args[key] = h[key]
91
93
  }
92
94
 
95
+ attrs.nodeName = fn.is ?? defaults.tag
96
+
93
97
  const controller = new AbortController()
94
98
 
95
99
  const instance = {
96
100
 
97
- *[Symbol.iterator]() { while (true) yield this[Args] },
101
+ *[Symbol.iterator]() { while (true) yield this[Args] },
98
102
 
99
103
  [Context]: Object.create(current()?.[Context] ?? null),
100
104
 
@@ -115,19 +119,23 @@ const runGenerator = (fn, h, emit) => {
115
119
 
116
120
  current(instance)
117
121
 
118
- const vnode = children => mark({ ...attrs, nodeName: fn.is ?? defaults.tag, children })
119
-
120
- let out = ''
121
-
122
- try {
123
-
124
- element(vnode(iterator.next().value), chunk => out += chunk)
125
-
126
- } catch (error) {
127
-
128
- out = ''
129
-
130
- element(vnode(iterator.throw(error).value), chunk => out += chunk)
122
+ let out = ''
123
+
124
+ const write = chunk => out += chunk
125
+
126
+ try {
127
+
128
+ attrs.children = iterator.next().value
129
+
130
+ element(attrs, write)
131
+
132
+ } catch (error) {
133
+
134
+ out = ''
135
+
136
+ attrs.children = iterator.throw(error).value
137
+
138
+ element(attrs, write)
131
139
 
132
140
  } finally {
133
141
 
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Context, current } from './context.js'
2
- import { isVNode, mark } from './jsx.js'
2
+ import { isVNode } from './jsx.js'
3
3
 
4
4
  const { isArray } = Array, { assign, hasOwn, keys } = Object
5
5
 
@@ -33,7 +33,9 @@ export const render = (h, el, child = el.firstChild, ref = null) => {
33
33
 
34
34
  } else if (node == child.nextSibling) {
35
35
 
36
- before(el, child, ref)
36
+ if (child.contains(document.activeElement)) el.insertBefore(node, child)
37
+
38
+ else el.insertBefore(child, ref)
37
39
 
38
40
  child = node.nextSibling
39
41
 
@@ -45,23 +47,21 @@ export const render = (h, el, child = el.firstChild, ref = null) => {
45
47
 
46
48
  if (child && !ref && child == el.firstChild) {
47
49
 
48
- const gone = []
50
+ const gone = [...el.children]
49
51
 
50
52
  el[Keyed]?.clear()
51
53
 
52
- while (child) gone.push(child), child = child.nextSibling
53
-
54
54
  el.textContent = ''
55
55
 
56
- for (child of gone) if (child.nodeType == 1) unref(child)
56
+ for (const node of gone) unref(node)
57
57
 
58
58
  } else while (child && child != ref) {
59
59
 
60
60
  const node = child.nextSibling
61
61
 
62
- const gone = el.removeChild(child)
62
+ el.removeChild(child)
63
63
 
64
- if (gone.nodeType == 1) unref(gone, el)
64
+ unref(child, el)
65
65
 
66
66
  child = node
67
67
  }
@@ -69,30 +69,23 @@ export const render = (h, el, child = el.firstChild, ref = null) => {
69
69
 
70
70
  const walk = (h, fn) => {
71
71
 
72
- if (h == null) return
72
+ if (h == null || typeof h == 'boolean') return
73
73
 
74
- const type = typeof h
74
+ if (isVNode(h)) {
75
75
 
76
- if (type == 'boolean') return
76
+ if (typeof h.nodeName != 'function') return fn(h)
77
77
 
78
- if (type == 'string') fn(h)
78
+ const { nodeName, ...rest } = h
79
79
 
80
- else if (type == 'number' || type == 'bigint') fn(String(h))
80
+ if (nodeName.prototype?.next) fn(runGenerator(nodeName, rest))
81
81
 
82
- else if (isVNode(h)) typeof h.nodeName == 'function' ? run(h, fn) : fn(h)
82
+ else walk(nodeName(rest), fn)
83
83
 
84
- else if (Symbol.iterator in Object(h)) for (h of h) walk(h, fn)
84
+ } else if (typeof h == 'object' && Symbol.iterator in h) for (h of h) walk(h, fn)
85
85
 
86
86
  else fn(String(h))
87
87
  }
88
88
 
89
- const run = ({ nodeName, ...h }, fn) => {
90
-
91
- if (nodeName.constructor.name == 'GeneratorFunction') fn(runGenerator(nodeName, h))
92
-
93
- else walk(nodeName(h), fn)
94
- }
95
-
96
89
  const runGenerator = (fn, h) => {
97
90
 
98
91
  const attrs = { ...fn.attrs }, args = { ...fn.args }
@@ -106,14 +99,22 @@ const runGenerator = (fn, h) => {
106
99
  else args[key] = h[key]
107
100
  }
108
101
 
109
- return mark({ ...attrs, nodeName: fn.is ?? defaults.tag, [Generator]: fn, [Args]: args })
102
+ attrs.nodeName = fn.is ?? defaults.tag
103
+
104
+ attrs[Generator] = fn
105
+
106
+ attrs[Args] = args
107
+
108
+ return attrs
110
109
  }
111
110
 
112
111
  const text = (h, node, ref) => {
113
112
 
114
113
  while (node && node != ref && node.nodeType != 3) node = node.nextSibling
115
114
 
116
- node && node != ref ? node.data != h && (node.data = h) : node = document.createTextNode(h)
115
+ if (node == null || node == ref) node = document.createTextNode(h)
116
+
117
+ else if (node.data != h) node.data = h
117
118
 
118
119
  return node
119
120
  }
@@ -122,21 +123,18 @@ const element = (h, el, node, ref) => {
122
123
 
123
124
  const nodeName = h.nodeName, key = h.key, gen = h[Generator]
124
125
 
125
- if (node && key != null && ref == null) node = (el[Keyed] ??= new Map()).get(key) ?? node
126
-
127
- while (node && node != ref && (
128
-
129
- (node.localName != nodeName) ||
126
+ if (node && key != null && ref == null) node = el[Keyed]?.get(key) ?? node
130
127
 
131
- (node[Key] != null && node[Key] != key) ||
128
+ while (node && node != ref) {
132
129
 
133
- (node[Generator] && node[Generator] != gen)
130
+ const k = node[Key], g = node[Generator]
134
131
 
135
- )) node = node[Key] != null ? null : node.nextElementSibling
132
+ if (node.localName == nodeName && (k == null || k == key) && (!g || g == gen)) break
136
133
 
137
- if (node == ref) node = null
134
+ node = k != null ? null : node.nextElementSibling
135
+ }
138
136
 
139
- node ??= document.createElementNS(h.xmlns ?? el.namespaceURI, nodeName)
137
+ if (node == null || node == ref) node = document.createElementNS(h.xmlns ?? el.namespaceURI, nodeName)
140
138
 
141
139
  if (key != null) (el[Keyed] ??= new Map()).set(node[Key] = key, node)
142
140
 
@@ -144,7 +142,14 @@ const element = (h, el, node, ref) => {
144
142
 
145
143
  attrs(node[Cache], h, node)
146
144
 
147
- if (!h.skip) gen ? next(gen, h[Args], node) : !node[Cache] && (typeof h.children == 'string' || typeof h.children == 'number') ? node.textContent = h.children + '' : render(h.children, node)
145
+ if (!h.skip) {
146
+
147
+ if (gen) next(gen, h[Args], node)
148
+
149
+ else if (!node[Cache] && (typeof h.children == 'string' || typeof h.children == 'number')) node.textContent = h.children + ''
150
+
151
+ else render(h.children, node)
152
+ }
148
153
 
149
154
  node[Memo] = h.memo
150
155
 
@@ -154,15 +159,24 @@ const element = (h, el, node, ref) => {
154
159
  return node
155
160
  }
156
161
 
162
+ const special = key => key == 'nodeName' || key == 'children' || key == 'key' || key == 'skip' || key == 'memo'
163
+
157
164
  const attrs = (cache, h, node) => {
158
165
 
159
- if (!cache) for (let i = node.attributes.length; i--;) hasOwn(h, node.attributes[i].name) || node.removeAttribute(node.attributes[i].name)
166
+ for (const key of cache ? keys(cache) : node.getAttributeNames()) {
167
+
168
+ if (hasOwn(h, key) || special(key)) continue
169
+
170
+ if (key.startsWith('set:')) node[key.slice(4)] = undefined
160
171
 
161
- for (const key in h) if (hasOwn(h, key)) {
172
+ else node.removeAttribute(key)
173
+ }
174
+
175
+ for (const key of keys(h)) {
162
176
 
163
177
  const value = h[key]
164
178
 
165
- if (cache && cache[key] === value || key == 'nodeName' || key == 'children' || key == 'key' || key == 'skip' || key == 'memo') continue
179
+ if (cache && cache[key] === value || special(key)) continue
166
180
 
167
181
  if (key == 'ref' && typeof value == 'function') value(node)
168
182
 
@@ -174,45 +188,15 @@ const attrs = (cache, h, node) => {
174
188
 
175
189
  else node.setAttribute(key, value === true ? '' : value)
176
190
  }
177
-
178
- if (cache) for (const key in cache) if (hasOwn(cache, key)) {
179
-
180
- if (hasOwn(h, key) || key == 'nodeName' || key == 'children' || key == 'key' || key == 'skip' || key == 'memo') continue
181
-
182
- if (key.startsWith('set:')) node[key.slice(4)] = undefined
183
-
184
- else node.removeAttribute(key)
185
- }
186
191
  }
187
192
 
188
- const some = (a, b) => {
189
-
190
- if (isArray(a) && isArray(b)) {
191
-
192
- if (a.length != b.length) return true
193
-
194
- for (let i = a.length; i--;) if (a[i] !== b[i]) return true
193
+ const some = (a, b) =>
195
194
 
196
- return false
197
- }
198
-
199
- return a !== b
200
- }
195
+ isArray(a) && isArray(b) ? a.length != b.length || a.some((v, i) => v !== b[i]) : a !== b
201
196
 
202
197
  const each = (fn, node) => {
203
198
 
204
- let child = node.firstElementChild
205
-
206
- while (child)
207
-
208
- if (fn(child) && child.firstElementChild) child = child.firstElementChild
209
-
210
- else {
211
-
212
- while (child != node && !child.nextElementSibling) child = child.parentNode ?? node
213
-
214
- child = child != node && child.nextElementSibling
215
- }
199
+ for (let child = node.firstElementChild; child; child = child.nextElementSibling) if (fn(child)) each(fn, child)
216
200
  }
217
201
 
218
202
  const before = (el, node, child) => {
@@ -239,46 +223,50 @@ const unref = (root, parent) => {
239
223
 
240
224
  root[Memo] = unref
241
225
 
242
- let node = root
226
+ const clean = node => {
243
227
 
244
- while (node.firstElementChild) node = node.firstElementChild
228
+ for (let child = node.firstElementChild, next; child; child = next) {
245
229
 
246
- while (true) {
230
+ next = child.nextElementSibling
247
231
 
248
- const { nextElementSibling, parentNode } = node
232
+ clean(child)
233
+ }
249
234
 
250
- if (node[Key] != null) (parentNode ?? parent)?.[Keyed]?.delete(node[Key])
235
+ if (node[Key] != null) (node.parentNode ?? parent)?.[Keyed]?.delete(node[Key])
251
236
 
252
237
  if (typeof node.return == 'function') node.return(false)
253
238
 
254
239
  if (typeof node[Cache]?.ref == 'function') node[Cache].ref(null)
255
-
256
- if (node === root) break
257
-
258
- node = nextElementSibling ?? parentNode ?? root
259
-
260
- if (nextElementSibling) while (node.firstElementChild) node = node.firstElementChild
261
240
  }
241
+
242
+ clean(root)
262
243
  }
263
244
 
245
+ let observer
246
+
264
247
  const next = (fn, args, el) => {
265
248
 
266
- el[Generator] ??= (watch(), assign(el, methods)[Context] = Object.create(current()?.[Context] ?? null), fn)
249
+ if (!el[Generator]) {
267
250
 
268
- for (const key of keys(el[Args] ??= args)) hasOwn(args, key) || delete el[Args][key]
251
+ if (!observer) (observer = new MutationObserver(records => {
269
252
 
270
- assign(el[Args], args)
253
+ for (const record of records) for (const node of record.removedNodes) if (node.nodeType == 1 && !node.isConnected) unref(node, record.target)
271
254
 
272
- el[Render]()
273
- }
255
+ })).observe(document, { childList: true, subtree: true })
274
256
 
275
- let observer
257
+ assign(el, methods)[Context] = Object.create(current()?.[Context] ?? null)
276
258
 
277
- const watch = () => observer || (observer = new MutationObserver(records => {
259
+ el[Generator] = fn
260
+ }
261
+
262
+ const prev = el[Args] ??= args
278
263
 
279
- for (const record of records) for (const node of record.removedNodes) if (node.nodeType == 1 && !node.isConnected) unref(node, record.target)
264
+ for (const key of keys(prev)) if (!hasOwn(args, key)) delete prev[key]
280
265
 
281
- })).observe(document, { childList: true, subtree: true })
266
+ assign(prev, args)
267
+
268
+ el[Render]()
269
+ }
282
270
 
283
271
  const methods = {
284
272
 
@@ -349,10 +337,10 @@ const methods = {
349
337
 
350
338
  return(deep = true) {
351
339
 
352
- if (this[Iterator] === false) return
353
-
354
340
  const iterator = this[Iterator]
355
341
 
342
+ if (iterator === false) return
343
+
356
344
  this[Iterator] = false
357
345
 
358
346
  if (deep) each(el => typeof el.return == 'function' ? el.return() : true, this)
package/jsx.js CHANGED
@@ -1,18 +1,18 @@
1
- const VNode = Symbol.for('ajo.vnode')
1
+ const VNode = Symbol()
2
2
 
3
3
  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.33",
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
@@ -2,7 +2,7 @@ declare module 'ajo' {
2
2
 
3
3
  type Tag = keyof (HTMLElementTagNameMap & SVGElementTagNameMap)
4
4
 
5
- type Type = Tag | (string & {}) | Stateless | Stateful
5
+ type Type = Tag | (string & {}) | Stateless<any> | Stateful<any, any>
6
6
 
7
7
  type Children = unknown
8
8
 
@@ -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
 
@@ -35,6 +35,12 @@ declare module 'ajo' {
35
35
  [key: `attr:${string}`]: unknown
36
36
  }
37
37
 
38
+ type CommonAttrs = {
39
+ class: string | boolean | null,
40
+ style: string | boolean | null,
41
+ xmlns: string,
42
+ }
43
+
38
44
  interface Defaults { }
39
45
 
40
46
  type DefaultTag = Defaults extends { tag: infer T extends string } ? T : 'div'
@@ -44,29 +50,32 @@ declare module 'ajo' {
44
50
  type Stateless<TArgs extends Args = {}> = (args: TArgs) => Children
45
51
 
46
52
  type Stateful<TArgs extends Args = {}, TTag extends string = DefaultTag> = {
47
- (this: StatefulElement<TArgs, TTag>, args: TArgs): Iterator<Children>
53
+ (this: Host<ElementType<TTag>, TArgs>, args: TArgs): Iterator<Children>
48
54
  } & (TTag extends DefaultTag ? { is?: TTag } : { is: TTag }) & {
49
- attrs?: Partial<PropSetter<TTag>> & Args,
55
+ attrs?: Partial<PropertySetter<TTag> & CommonAttrs> & Args,
50
56
  args?: Partial<TArgs>,
51
57
  }
52
58
 
53
59
  type Component<TArgs extends Args = {}> = Stateless<TArgs> | Stateful<TArgs>
54
60
 
55
61
  type StatefulArgs<TArgs, TTag> =
56
- Partial<SpecialAttrs<StatefulElement<TArgs, TTag>> & PropSetter<TTag>> &
62
+ Partial<SpecialAttrs<Host<ElementType<TTag>, TArgs>> & PropertySetter<TTag>> &
57
63
  AttrSetter &
58
64
  TArgs
59
65
 
60
- type StatefulElement<TArgs, TTag> = ElementType<TTag> & {
66
+ type Host<TElement extends object = HTMLElement, TArgs = Args> = TElement & {
61
67
  [Symbol.iterator](): Iterator<TArgs>,
62
68
  signal: AbortSignal,
63
- next: <R>(fn?: (this: StatefulElement<TArgs, TTag>, args: TArgs) => R) => R,
69
+ next: {
70
+ (): undefined,
71
+ <R>(fn: (this: Host<TElement, TArgs>, args: TArgs) => R): R,
72
+ },
64
73
  throw: (value?: unknown) => void,
65
74
  return: (deep?: boolean) => void,
66
75
  }
67
76
 
68
77
  type HTMLIntrinsicElements = {
69
- [TTag in Tag]: Partial<PropSetter<TTag> & SpecialAttrs<ElementType<TTag>>> & Args
78
+ [TTag in Tag]: Partial<PropertySetter<TTag> & SpecialAttrs<ElementType<TTag>> & CommonAttrs> & Args
70
79
  }
71
80
 
72
81
  interface IntrinsicElements extends HTMLIntrinsicElements { }
@@ -82,10 +91,10 @@ declare module 'ajo' {
82
91
  : P
83
92
  : P
84
93
 
85
- function render(h: Children, el: ParentNode, child?: ChildNode, ref?: ChildNode): void
94
+ function render(h: Children, el: ParentNode, child?: ChildNode | null, ref?: ChildNode | null): void
86
95
 
87
96
  function stateful<TArgs extends Args, TTag extends string = DefaultTag>(
88
- fn: (this: StatefulElement<TArgs, TTag>, args: TArgs) => Iterator<Children>,
97
+ fn: (this: Host<ElementType<TTag>, TArgs>, args: TArgs) => Iterator<Children>,
89
98
  is?: TTag
90
99
  ): Stateful<TArgs, TTag>
91
100
  }
@@ -95,7 +104,7 @@ declare module 'ajo/context' {
95
104
  (): T
96
105
  <V extends T>(value: V): V
97
106
  }
98
- function current(): import('ajo').StatefulElement<any, any> | null
107
+ function current(): import('ajo').Host<object, any> | null
99
108
  }
100
109
 
101
110
  declare module 'ajo/html' {
@@ -105,11 +114,11 @@ declare module 'ajo/html' {
105
114
  }
106
115
 
107
116
  declare module 'ajo/jsx-runtime' {
108
- import { Type, Args, VNode, ElementChildrenAttribute, IntrinsicElements as _IE } from 'ajo'
109
- export function Fragment({ children }: ElementChildrenAttribute): typeof children
110
- export function jsx(type: Type, props: Args | null, key?: string | number): VNode
111
- export function jsxs(type: Type, props: Args | null, key?: string | number): VNode
112
- export function jsxDEV(type: Type, props: Args | null, key?: string | number): VNode
117
+ import { Type, Args, VNode, ElementChildrenAttribute, IntrinsicElements as _IE } from 'ajo'
118
+ export function Fragment({ children }: ElementChildrenAttribute): typeof children
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
113
122
  export namespace JSX {
114
123
  type ElementChildrenAttribute = import('ajo').ElementChildrenAttribute
115
124
  type LibraryManagedAttributes<C, P> = import('ajo').ManagedAttributes<C, P>
@@ -117,9 +126,9 @@ declare module 'ajo/jsx-runtime' {
117
126
  }
118
127
  }
119
128
 
120
- declare module 'ajo/jsx-dev-runtime' {
121
- import { IntrinsicElements as _IE } from 'ajo'
122
- export { Fragment, jsx, jsxs, jsxDEV } from 'ajo/jsx-runtime'
129
+ declare module 'ajo/jsx-dev-runtime' {
130
+ import { IntrinsicElements as _IE } from 'ajo'
131
+ export { Fragment, jsx, jsxs, jsxDEV } from 'ajo/jsx-runtime'
123
132
  export namespace JSX {
124
133
  type ElementChildrenAttribute = import('ajo').ElementChildrenAttribute
125
134
  type LibraryManagedAttributes<C, P> = import('ajo').ManagedAttributes<C, P>