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 +148 -22
- package/dist/html.js +1 -1
- package/dist/index.js +1 -1
- package/dist/jsx.js +1 -1
- package/html.js +76 -68
- package/index.js +81 -93
- package/jsx.js +6 -6
- package/package.json +1 -1
- package/readme.md +93 -34
- package/types.ts +28 -19
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' //
|
|
78
|
-
Counter.attrs = { class: 'counter-wrap' } // default
|
|
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
|
|
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" // →
|
|
93
|
-
set:onclick={fn} // →
|
|
94
|
-
key={id} // →
|
|
95
|
-
memo={[id]} // →
|
|
96
|
-
memo={id} // →
|
|
97
|
-
memo // →
|
|
98
|
-
ref={el => ref = el} // →
|
|
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:
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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 = {
|
|
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** |
|
|
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))
|
|
148
|
-
.catch(e => this.next(() => error = e)) //
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
40
|
-
|
|
41
|
-
else
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
62
|
+
el.removeChild(child)
|
|
63
63
|
|
|
64
|
-
|
|
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
|
-
|
|
74
|
+
if (isVNode(h)) {
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
if (typeof h.nodeName != 'function') return fn(h)
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
const { nodeName, ...rest } = h
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
if (nodeName.prototype?.next) fn(runGenerator(nodeName, rest))
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
else walk(nodeName(rest), fn)
|
|
83
83
|
|
|
84
|
-
else if (Symbol.iterator in
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
128
|
+
while (node && node != ref) {
|
|
132
129
|
|
|
133
|
-
|
|
130
|
+
const k = node[Key], g = node[Generator]
|
|
134
131
|
|
|
135
|
-
|
|
132
|
+
if (node.localName == nodeName && (k == null || k == key) && (!g || g == gen)) break
|
|
136
133
|
|
|
137
|
-
|
|
134
|
+
node = k != null ? null : node.nextElementSibling
|
|
135
|
+
}
|
|
138
136
|
|
|
139
|
-
node
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
226
|
+
const clean = node => {
|
|
243
227
|
|
|
244
|
-
|
|
228
|
+
for (let child = node.firstElementChild, next; child; child = next) {
|
|
245
229
|
|
|
246
|
-
|
|
230
|
+
next = child.nextElementSibling
|
|
247
231
|
|
|
248
|
-
|
|
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
|
-
|
|
249
|
+
if (!el[Generator]) {
|
|
267
250
|
|
|
268
|
-
|
|
251
|
+
if (!observer) (observer = new MutationObserver(records => {
|
|
269
252
|
|
|
270
|
-
|
|
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
|
-
|
|
273
|
-
}
|
|
255
|
+
})).observe(document, { childList: true, subtree: true })
|
|
274
256
|
|
|
275
|
-
|
|
257
|
+
assign(el, methods)[Context] = Object.create(current()?.[Context] ?? null)
|
|
276
258
|
|
|
277
|
-
|
|
259
|
+
el[Generator] = fn
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const prev = el[Args] ??= args
|
|
278
263
|
|
|
279
|
-
for (const
|
|
264
|
+
for (const key of keys(prev)) if (!hasOwn(args, key)) delete prev[key]
|
|
280
265
|
|
|
281
|
-
|
|
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
|
|
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 =
|
|
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
|
@@ -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
|
|
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:
|
|
53
|
+
(this: Host<ElementType<TTag>, TArgs>, args: TArgs): Iterator<Children>
|
|
48
54
|
} & (TTag extends DefaultTag ? { is?: TTag } : { is: TTag }) & {
|
|
49
|
-
attrs?: Partial<
|
|
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<
|
|
62
|
+
Partial<SpecialAttrs<Host<ElementType<TTag>, TArgs>> & PropertySetter<TTag>> &
|
|
57
63
|
AttrSetter &
|
|
58
64
|
TArgs
|
|
59
65
|
|
|
60
|
-
type
|
|
66
|
+
type Host<TElement extends object = HTMLElement, TArgs = Args> = TElement & {
|
|
61
67
|
[Symbol.iterator](): Iterator<TArgs>,
|
|
62
68
|
signal: AbortSignal,
|
|
63
|
-
next:
|
|
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<
|
|
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:
|
|
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').
|
|
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,
|
|
111
|
-
export function jsxs(type: Type,
|
|
112
|
-
export function jsxDEV(type: Type,
|
|
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>
|