tutuca 0.9.78 → 0.9.79

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tutuca",
3
- "version": "0.9.78",
3
+ "version": "0.9.79",
4
4
  "type": "module",
5
5
  "description": "Zero-dependency SPA framework with immutable state and virtual DOM",
6
6
  "main": "./dist/tutuca.js",
@@ -43,30 +43,31 @@ Touch is wired up too (drag fires after a small move threshold).
43
43
  ## Dynamic Bindings
44
44
 
45
45
  For passing values "context-style" through nested components without prop
46
- drilling. Define on the producer; alias on consumers; resolve as `*name`.
46
+ drilling. **`provide`** on the producer; **`lookup`** on consumers;
47
+ resolve as `*name`.
47
48
 
48
49
  ```js
49
50
  const Theme = component({
50
51
  name: "Theme",
51
52
  fields: { color: "blue" },
52
- dynamic: { color: ".color" },
53
- on: {
54
- stackEnter() {
55
- return ["color"];
56
- },
57
- },
53
+ provide: { color: ".color" },
58
54
  });
59
55
  const Child = component({
60
- dynamic: { color: { for: "Theme.color", default: "'gray'" } },
56
+ lookup: { color: { for: "Theme.color", default: "'gray'" } },
61
57
  view: html`<p :style="$'color: {*color}'"></p>`,
62
58
  });
63
59
  ```
64
60
 
65
- `on.stackEnter()` is required only on the **producer** (the component
66
- declaring `dynamic: { name: ".field" }` to expose a value). It returns
67
- the list of dynamic-binding names this component pushes onto the stack
68
- when entering its render. Consumers (which only alias via
69
- `{ for: "Producer.name", default: ... }`) don't need it.
61
+ A **`provide`** maps an exported name to a field expression. Every
62
+ provide is evaluated and pushed onto the dynamic stack automatically
63
+ when the producer is entered during render there is no hook to opt in.
64
+
65
+ A **`lookup`** reads a value the `*name` way: the key is the name used
66
+ in views (`*color`), the value is `"Producer.provideName"`, or
67
+ `{ for: "Producer.provideName", default: ".field" }` to supply a
68
+ fallback when no producer is in scope (default is optional — without it
69
+ a miss resolves to `null`). A `*name` that names the component's own
70
+ `provide` resolves to the nearest provided value (including its own).
70
71
 
71
72
  ### Dynamic vars as render targets
72
73
 
@@ -80,23 +81,26 @@ component-render target and an iteration source:
80
81
  <div @each="*items"><x render-it></x></div> <!-- iterate a dynamic seq -->
81
82
  ```
82
83
 
83
- The producer's `dynamic` value is an **expression**, not only a bare
84
- field it can be a sequence/map item access:
84
+ A `provide` value must be **addressable** a `.field` or a `.seq[.key]`
85
+ seq-access, nothing else. (It is both read as `*name` *and* used as a
86
+ render-target / teleport path, so a method or constant — which has no
87
+ path — is a lint error.) A `lookup` `default`, by contrast, is only a
88
+ value fallback and accepts the full grammar, including constants like
89
+ `'gray'`. A `provide` can be a sequence/map item access:
85
90
 
86
91
  ```js
87
92
  const Root = component({
88
93
  name: "Root",
89
94
  fields: { items: IMap(), selectedKey: "" },
90
- dynamic: {
95
+ provide: {
91
96
  items: ".items", // the whole sequence
92
97
  selected: ".items[.selectedKey]", // seq-access to one entry
93
98
  },
94
- on: { stackEnter() { return ["items", "selected"]; } },
95
99
  });
96
100
  ```
97
101
 
98
102
  There is **no `*name[.key]` form** — a consumer never indexes a dynamic
99
- var. The seq-access lives in the producer's `dynamic` declaration; the
103
+ var. The seq-access lives in the producer's `provide` declaration; the
100
104
  consumer just reads the resolved value as `*name`.
101
105
 
102
106
  **Teleporting.** The component rendered via `<x render="*selected">`
@@ -305,7 +305,7 @@ component({
305
305
  bubble: { itemPicked(item, ctx) { return this.setSelected(item); } },
306
306
  response:{ loadData(res, err, ctx) { return this.setItems(res); } },
307
307
  statics: { fromData(d) { return this.make({ count: d.n ?? 0 }); } },
308
- // dynamic: { ... }, on: { stackEnter() {...} } // see advanced.md
308
+ // provide: { ... }, lookup: { ... } // see advanced.md
309
309
  });
310
310
  ```
311
311
 
@@ -416,6 +416,48 @@ statics: {
416
416
  // usage: TreeRoot.Class.fromData([...])
417
417
  ```
418
418
 
419
+ ### One definition, multiple scopes (`clone`)
420
+
421
+ A component is built once and bound to a scope at `registerComponents`
422
+ time: that scope owns the component's `Class`, the per-instance
423
+ component tag, and the scope-bound `make`/statics. Re-registering the
424
+ *same* component object into another scope rebinds it (last wins) —
425
+ fine for a fresh re-setup, but it means a single definition can't be
426
+ live in two scopes at once.
427
+
428
+ To register the same definition into a second scope simultaneously, use
429
+ `Comp.clone()` — it returns a fresh, fully independent `Component` (new
430
+ id, its own `Class`) built from the same spec:
431
+
432
+ ```js
433
+ scopeA.registerComponents([Widget]);
434
+ scopeB.registerComponents([Widget.clone()]); // independent Class + scope
435
+ ```
436
+
437
+ Each clone has its own `Class`, so `getCompFor(instance)` and a static's
438
+ `this.scope` / `this.make` resolve unambiguously to the scope that
439
+ instance belongs to — even after immutable `.set()` updates, since the
440
+ component tag lives on the (per-scope) prototype.
441
+
442
+ **Caveat — statics that reach a child by its module-level const.** A
443
+ static like `fromData` that builds a *different* child type by naming the
444
+ imported const directly (`Item.Class.fromData(v)` above) hardcodes the
445
+ child's *original* scope. In a single-scope app that's the only scope, so
446
+ it's fine. But once you `clone()` either component into another scope, the
447
+ parent clone still deserializes children through the original `Item` —
448
+ wrong scope. For multi-scope safety, resolve the child through the
449
+ caller's scope instead of the module-level const:
450
+
451
+ ```js
452
+ fromData({ items = [] }) {
453
+ const Item = this.scope.lookupComponent("Item");
454
+ return this.make({ items: items.map((v) => Item.Class.fromData(v)) });
455
+ }
456
+ ```
457
+
458
+ Recursion into the *same* type needs no lookup — use `this.fromData(v)`
459
+ (or `this.make`), which already targets the caller's scope.
460
+
419
461
  ## Text Rendering
420
462
 
421
463
  ```html
@@ -459,6 +501,14 @@ consequences:
459
501
  - Macro registry keys are lowercased on insert for the same reason
460
502
  (see *Macros* below).
461
503
 
504
+ Tutuca auto-namespaces by subtree: elements inside `<svg>` get the SVG
505
+ namespace and elements inside `<math>` get MathML, with spec-cased local
506
+ names preserved (`linearGradient`, `viewBox`). A `<foreignObject>` switches
507
+ its children back to the HTML namespace. Customised built-in elements work
508
+ via `is="..."` (e.g. `<button is="x-fancy">`); `is` is applied when the
509
+ element is created, so it must be a static attribute — setting it later
510
+ does not upgrade the element.
511
+
462
512
  ## Event Handling
463
513
 
464
514
  ```html
@@ -106,7 +106,7 @@ bubble handler can reply to the originator via `ctx.sendAtPath(ctx.targetPath,
106
106
  ## Dynamic-var teleporting
107
107
 
108
108
  A component rendered through `<x render="*sel">` *physically lives* at the
109
- producer that declared `dynamic: { sel: … }`, not under the consumer that
109
+ producer that declared `provide: { sel: … }`, not under the consumer that
110
110
  wrote the render. The reconstructed dispatch path keeps every intermediate
111
111
  component (so bubbling visits them), but `toTransactionPath()` teleports
112
112
  the `DynStep`: it pops the steps tagged with the marker's `interiorCids`
@@ -115,7 +115,7 @@ mutation therefore lands on the producer's data, and the consumer's view
115
115
  of it updates in lock-step. Authoring view: *Teleporting* in
116
116
  [advanced.md](./advanced.md).
117
117
 
118
- When the producer's `dynamic` value is a seq-access (`.sheets[.selId]`),
118
+ When the producer's `provide` value is a seq-access (`.sheets[.selId]`),
119
119
  the teleported steps include a `SeqAccessStep` — which is where async key
120
120
  races come from.
121
121
 
@@ -474,6 +474,7 @@ var G_BOOL = K_FIELD | K_METHOD | K_BIND | K_DYN | K_CONST;
474
474
  var G_TEXT = G_BOOL | K_STRTPL;
475
475
  var G_COMPONENT = K_FIELD | K_SEQ | K_DYN;
476
476
  var G_SEQUENCE = K_FIELD | K_DYN;
477
+ var G_PROVIDE = K_FIELD | K_SEQ;
477
478
  var G_FIELD = K_FIELD | K_METHOD | K_CONST | K_STR | K_SEQ;
478
479
  var G_VALUE = K_FIELD | K_METHOD | K_BIND | K_DYN | K_NAME | K_TYPE | K_REQUEST | K_CONST;
479
480
  var G_PRED_ARG = G_BOOL | K_STR;
@@ -592,6 +593,9 @@ class ValParser {
592
593
  parseField(s, px) {
593
594
  return this._parseSingle(s, px, G_FIELD);
594
595
  }
596
+ parseProvide(s, px) {
597
+ return this._parseSingle(s, px, G_PROVIDE);
598
+ }
595
599
  parseHandlerArg(s, px) {
596
600
  return this._parseSingle(s, px, G_HANDLER_ARG);
597
601
  }
@@ -1578,21 +1582,22 @@ function h(tagName, properties, children, namespace) {
1578
1582
  }
1579
1583
 
1580
1584
  // src/anode.js
1581
- function resolveDynProducer(comp, dynName) {
1582
- const dyn = comp?.dynamic?.[dynName];
1583
- if (dyn == null)
1584
- return null;
1585
- let producerComp, producerDyn;
1586
- if (dyn.compName != null) {
1587
- producerComp = comp.scope?.lookupComponent(dyn.compName);
1588
- producerDyn = producerComp?.dynamic?.[dyn.dynName];
1585
+ function resolveDynProducer(comp, name) {
1586
+ let producerComp, producerProvide;
1587
+ const lk = comp?.lookup?.[name];
1588
+ if (lk != null) {
1589
+ producerComp = comp.scope?.lookupComponent(lk.compName);
1590
+ producerProvide = producerComp?.provide?.[lk.provideName];
1589
1591
  } else {
1592
+ const p = comp?.provide?.[name];
1593
+ if (p == null)
1594
+ return null;
1590
1595
  producerComp = comp;
1591
- producerDyn = dyn;
1596
+ producerProvide = p;
1592
1597
  }
1593
- if (producerComp == null || producerDyn == null)
1598
+ if (producerComp == null || producerProvide == null)
1594
1599
  return null;
1595
- const pi = producerDyn.val?.toPathItem?.() ?? null;
1600
+ const pi = producerProvide.val?.toPathItem?.() ?? null;
1596
1601
  return { producerCompId: producerComp.id, producerSteps: pi ? [pi] : [] };
1597
1602
  }
1598
1603
 
@@ -2335,9 +2340,6 @@ class Components {
2335
2340
  getCompFor(v) {
2336
2341
  return v?.[this.getComponentSymbol]?.() ?? null;
2337
2342
  }
2338
- getOnEnterFor(v) {
2339
- return this.getCompFor(v)?.on.stackEnter ?? defaultOnStackEnter;
2340
- }
2341
2343
  getHandlerFor(v, name, key) {
2342
2344
  return this.getCompFor(v)?.[key][name] ?? null;
2343
2345
  }
@@ -2407,36 +2409,30 @@ class ComponentStack {
2407
2409
  }
2408
2410
  }
2409
2411
 
2410
- class Dynamic {
2412
+ class ProvideInfo {
2411
2413
  constructor(name, val, symbol) {
2412
2414
  this.name = name;
2413
2415
  this.val = val;
2414
2416
  this.symbol = symbol;
2415
2417
  }
2416
- getSymbol(_stack) {
2417
- return this.symbol;
2418
- }
2419
- evalAndBind(stack, binds) {
2420
- binds[this.getSymbol(stack)] = this.val.eval(stack);
2421
- }
2422
2418
  }
2423
2419
 
2424
- class DynamicAlias extends Dynamic {
2425
- constructor(name, val, compName, dynName) {
2426
- super(name, val, null);
2420
+ class LookupInfo {
2421
+ constructor(name, compName, provideName, val) {
2422
+ this.name = name;
2427
2423
  this.compName = compName;
2428
- this.dynName = dynName;
2429
- }
2430
- _resolveSymbol(stack) {
2431
- return stack.lookupType(this.compName)?.dynamic[this.dynName]?.symbol ?? null;
2424
+ this.provideName = provideName;
2425
+ this.val = val;
2426
+ this._sym = undefined;
2432
2427
  }
2433
- getSymbol(stack) {
2434
- this.symbol ??= this._resolveSymbol(stack);
2435
- return this.symbol;
2428
+ getProducerSymbol(stack) {
2429
+ if (this._sym === undefined)
2430
+ this._sym = stack.lookupType(this.compName)?.provide?.[this.provideName]?.symbol ?? null;
2431
+ return this._sym;
2436
2432
  }
2437
2433
  }
2438
2434
  var isString = (v) => typeof v === "string";
2439
- var _rawSpecKeys = "name view style commonStyle globalStyle input receive bubble response alter on views dynamic fields methods statics";
2435
+ var _rawSpecKeys = "name view style commonStyle globalStyle input receive bubble response alter views provide lookup fields methods statics";
2440
2436
  var KNOWN_SPEC_KEYS = new Set(_rawSpecKeys.split(" "));
2441
2437
  var _compId = 0;
2442
2438
 
@@ -2453,35 +2449,47 @@ class Component {
2453
2449
  this.bubble = o.bubble ?? {};
2454
2450
  this.response = o.response ?? {};
2455
2451
  this.alter = o.alter ?? {};
2456
- this.on = { stackEnter: o.on?.stackEnter ?? defaultOnStackEnter };
2457
2452
  for (const name in o.views ?? {}) {
2458
2453
  const v = o.views[name];
2459
2454
  const { view, style } = isString(v) ? { view: v } : v;
2460
2455
  this.views[name] = new View(name, view, style);
2461
2456
  }
2462
- this._rawDynamic = o.dynamic ?? {};
2463
- this.dynamic = {};
2457
+ this._rawProvide = o.provide ?? {};
2458
+ this._rawLookup = o.lookup ?? {};
2459
+ this.provide = {};
2460
+ this.lookup = {};
2464
2461
  this.scope = null;
2462
+ this.spec = o;
2465
2463
  this.extra = {};
2466
2464
  for (const key of Object.keys(o))
2467
2465
  if (!KNOWN_SPEC_KEYS.has(key))
2468
2466
  this.extra[key] = o[key];
2469
2467
  }
2468
+ clone() {
2469
+ return Component.fromSpec(this.spec);
2470
+ }
2470
2471
  compile(ParseContext2) {
2471
2472
  for (const name in this.views)
2472
2473
  this.views[name].compile(new ParseContext2, this.scope, this.id);
2473
- for (const key in this._rawDynamic) {
2474
- const dinfo = this._rawDynamic[key];
2475
- if (isString(dinfo)) {
2476
- const val = vp.parseField(dinfo, this.views.main.ctx);
2477
- this.dynamic[key] = new Dynamic(key, val, Symbol(key));
2478
- } else if (isString(dinfo?.default) && isString(dinfo?.for)) {
2479
- const val = vp.parseField(dinfo.default, this.views.main.ctx);
2480
- const [compName, dynName] = dinfo.for.split(".");
2481
- if (isString(compName) && isString(dynName))
2482
- this.dynamic[key] = new DynamicAlias(key, val, compName, dynName);
2483
- }
2484
- }
2474
+ const ctx = this.views.main.ctx;
2475
+ for (const key in this._rawProvide) {
2476
+ const val = vp.parseProvide(this._rawProvide[key], ctx);
2477
+ if (val)
2478
+ this.provide[key] = new ProvideInfo(key, val, Symbol(key));
2479
+ }
2480
+ for (const key in this._rawLookup) {
2481
+ const linfo = this._rawLookup[key];
2482
+ const forStr = isString(linfo) ? linfo : isString(linfo?.for) ? linfo.for : null;
2483
+ const [compName, provideName] = forStr === null ? [] : forStr.split(".");
2484
+ if (!isString(compName) || !isString(provideName))
2485
+ continue;
2486
+ const defStr = isString(linfo?.default) ? linfo.default : null;
2487
+ const val = defStr === null ? null : vp.parseField(defStr, ctx);
2488
+ this.lookup[key] = new LookupInfo(key, compName, provideName, val);
2489
+ }
2490
+ for (const key in this.lookup)
2491
+ if (this.provide[key] !== undefined)
2492
+ console.warn("name declared in both provide and lookup", this.name, key);
2485
2493
  }
2486
2494
  make(args, opts) {
2487
2495
  return this.Class.make(args, opts ?? { scope: this.scope });
@@ -2509,9 +2517,6 @@ class Component {
2509
2517
  `);
2510
2518
  }
2511
2519
  }
2512
- function defaultOnStackEnter() {
2513
- return null;
2514
- }
2515
2520
 
2516
2521
  // src/stack.js
2517
2522
  var STOP = Symbol("STOP");
@@ -2570,47 +2575,61 @@ class Stack {
2570
2575
  this.viewsId = viewsId;
2571
2576
  this.ctx = ctx;
2572
2577
  }
2573
- _enrichOnEnter() {
2574
- return this.withDynamicBinds(this.comps.getOnEnterFor(this.it).call(this.it));
2578
+ _pushProvides() {
2579
+ const provide = this.comps.getCompFor(this.it)?.provide;
2580
+ if (provide == null)
2581
+ return this;
2582
+ const dynObj = {};
2583
+ let has = false;
2584
+ for (const k in provide) {
2585
+ dynObj[provide[k].symbol] = provide[k].val.eval(this);
2586
+ has = true;
2587
+ }
2588
+ if (!has)
2589
+ return this;
2590
+ const newDynBinds = [new ObjectFrame(dynObj), this.dynBinds];
2591
+ const { comps, it, binds, views, viewsId, ctx } = this;
2592
+ return new Stack(comps, it, binds, newDynBinds, views, viewsId, ctx);
2575
2593
  }
2576
2594
  static root(comps, it, ctx) {
2577
2595
  const binds = [new BindFrame(it, { it }, true), null];
2578
2596
  const dynBinds = [new ObjectFrame({}), null];
2579
2597
  const views = ["main", null];
2580
- return new Stack(comps, it, binds, dynBinds, views, "", ctx)._enrichOnEnter();
2598
+ return new Stack(comps, it, binds, dynBinds, views, "", ctx)._pushProvides();
2581
2599
  }
2582
2600
  enter(it, bindings = {}, isFrame = true) {
2583
2601
  const { comps, binds, dynBinds, views, viewsId, ctx } = this;
2584
2602
  const newBinds = [new BindFrame(it, bindings, isFrame), binds];
2585
2603
  const stack = new Stack(comps, it, newBinds, dynBinds, views, viewsId, ctx);
2586
- return isFrame ? stack._enrichOnEnter() : stack;
2604
+ return isFrame ? stack._pushProvides() : stack;
2587
2605
  }
2588
2606
  pushViewName(name) {
2589
2607
  const { comps, it, binds, dynBinds, views, ctx } = this;
2590
2608
  const newViews = [name, views];
2591
2609
  return new Stack(comps, it, binds, dynBinds, newViews, computeViewsId(newViews), ctx);
2592
2610
  }
2593
- withDynamicBinds(dynamics) {
2594
- if (dynamics == null || dynamics.length === 0)
2595
- return this;
2596
- const dynObj = {};
2597
- const comp = this.comps.getCompFor(this.it);
2598
- for (const dynName of dynamics)
2599
- comp.dynamic[dynName].evalAndBind(this, dynObj);
2600
- const newDynBinds = [new ObjectFrame(dynObj), this.dynBinds];
2601
- const { comps, it, binds, views, viewsId, ctx } = this;
2602
- return new Stack(comps, it, binds, newDynBinds, views, viewsId, ctx);
2611
+ _pushDynBindValuesToArray(arr, comp) {
2612
+ for (const k in comp.provide)
2613
+ arr.push(this._lookupProvide(comp.provide[k]));
2614
+ for (const k in comp.lookup)
2615
+ arr.push(this._lookupAlias(comp.lookup[k]));
2603
2616
  }
2604
- _pushDynBindValuesToArray(arr, dyns) {
2605
- for (const k in dyns)
2606
- arr.push(this._lookupDynamicWithDynVal(dyns[k]));
2617
+ _lookupProvide(p) {
2618
+ return lookup(this.dynBinds, p.symbol) ?? p.val.eval(this) ?? null;
2607
2619
  }
2608
- _lookupDynamicWithDynVal(d) {
2609
- return lookup(this.dynBinds, d.getSymbol(this)) ?? d.val?.eval(this) ?? null;
2620
+ _lookupAlias(lk) {
2621
+ const sym = lk.getProducerSymbol(this);
2622
+ return (sym != null ? lookup(this.dynBinds, sym) : null) ?? lk.val?.eval(this) ?? null;
2610
2623
  }
2611
2624
  lookupDynamic(name) {
2612
- const d = this.comps.getCompFor(this.it)?.dynamic[name];
2613
- return d ? this._lookupDynamicWithDynVal(d) : null;
2625
+ const comp = this.comps.getCompFor(this.it);
2626
+ if (comp == null)
2627
+ return null;
2628
+ const lk = comp.lookup[name];
2629
+ if (lk !== undefined)
2630
+ return this._lookupAlias(lk);
2631
+ const p = comp.provide[name];
2632
+ return p !== undefined ? this._lookupProvide(p) : null;
2614
2633
  }
2615
2634
  lookupBind(name) {
2616
2635
  return lookup(this.binds, name);
@@ -3380,7 +3399,7 @@ class Renderer {
3380
3399
  _rValComp(stack, val, comp, node, key, viewName) {
3381
3400
  const cacheKey = `${viewName ?? stack.viewsId ?? ""}-${key}`;
3382
3401
  const cachePath = [node, val];
3383
- stack._pushDynBindValuesToArray(cachePath, comp.dynamic);
3402
+ stack._pushDynBindValuesToArray(cachePath, comp);
3384
3403
  const cachedNode = this.cache.get(cachePath, cacheKey);
3385
3404
  if (cachedNode)
3386
3405
  return cachedNode;
@@ -3920,7 +3939,8 @@ function classFromData(name, { fields = {}, methods, statics }) {
3920
3939
  b.statics(statics);
3921
3940
  return b.build();
3922
3941
  }
3923
- var component = (opts) => new Component(classFromData(opts.name, opts), opts);
3942
+ Component.fromSpec = (opts) => new Component(classFromData(opts.name, opts), opts);
3943
+ var component = (opts) => Component.fromSpec(opts);
3924
3944
 
3925
3945
  // index.js
3926
3946
  var css = String.raw;