bitwrench 2.0.18 → 2.0.20

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.
Files changed (58) hide show
  1. package/README.md +86 -81
  2. package/dist/bitwrench-bccl.cjs.js +221 -48
  3. package/dist/bitwrench-bccl.cjs.min.js +3 -3
  4. package/dist/bitwrench-bccl.esm.js +221 -48
  5. package/dist/bitwrench-bccl.esm.min.js +3 -3
  6. package/dist/bitwrench-bccl.umd.js +221 -48
  7. package/dist/bitwrench-bccl.umd.min.js +3 -3
  8. package/dist/bitwrench-code-edit.cjs.js +7 -9
  9. package/dist/bitwrench-code-edit.cjs.min.js +5 -7
  10. package/dist/bitwrench-code-edit.es5.js +6 -8
  11. package/dist/bitwrench-code-edit.es5.min.js +5 -7
  12. package/dist/bitwrench-code-edit.esm.js +7 -9
  13. package/dist/bitwrench-code-edit.esm.min.js +5 -7
  14. package/dist/bitwrench-code-edit.umd.js +7 -9
  15. package/dist/bitwrench-code-edit.umd.min.js +5 -7
  16. package/dist/bitwrench-debug.js +268 -0
  17. package/dist/bitwrench-debug.min.js +3 -0
  18. package/dist/bitwrench-lean.cjs.js +250 -1574
  19. package/dist/bitwrench-lean.cjs.min.js +6 -6
  20. package/dist/bitwrench-lean.es5.js +344 -1661
  21. package/dist/bitwrench-lean.es5.min.js +4 -4
  22. package/dist/bitwrench-lean.esm.js +250 -1574
  23. package/dist/bitwrench-lean.esm.min.js +6 -6
  24. package/dist/bitwrench-lean.umd.js +250 -1574
  25. package/dist/bitwrench-lean.umd.min.js +6 -6
  26. package/dist/bitwrench-util-css.cjs.js +1 -1
  27. package/dist/bitwrench-util-css.cjs.min.js +1 -1
  28. package/dist/bitwrench-util-css.es5.js +1 -1
  29. package/dist/bitwrench-util-css.es5.min.js +1 -1
  30. package/dist/bitwrench-util-css.esm.js +1 -1
  31. package/dist/bitwrench-util-css.esm.min.js +1 -1
  32. package/dist/bitwrench-util-css.umd.js +1 -1
  33. package/dist/bitwrench-util-css.umd.min.js +1 -1
  34. package/dist/bitwrench.cjs.js +510 -1660
  35. package/dist/bitwrench.cjs.min.js +7 -7
  36. package/dist/bitwrench.css +80 -33
  37. package/dist/bitwrench.es5.js +569 -1694
  38. package/dist/bitwrench.es5.min.js +5 -5
  39. package/dist/bitwrench.esm.js +510 -1660
  40. package/dist/bitwrench.esm.min.js +7 -7
  41. package/dist/bitwrench.min.css +1 -1
  42. package/dist/bitwrench.umd.js +510 -1660
  43. package/dist/bitwrench.umd.min.js +7 -7
  44. package/dist/builds.json +133 -111
  45. package/dist/bwserve.cjs.js +2 -2
  46. package/dist/bwserve.esm.js +2 -2
  47. package/dist/sri.json +46 -44
  48. package/package.json +5 -3
  49. package/readme.html +86 -75
  50. package/src/bitwrench-bccl-entry.js +3 -4
  51. package/src/bitwrench-bccl.js +217 -43
  52. package/src/bitwrench-code-edit.js +6 -8
  53. package/src/bitwrench-debug.js +245 -0
  54. package/src/bitwrench-styles.js +35 -8
  55. package/src/bitwrench.js +212 -1563
  56. package/src/cli/attach.js +53 -21
  57. package/src/cli/serve.js +179 -3
  58. package/src/version.js +3 -3
package/src/bitwrench.js CHANGED
@@ -56,12 +56,11 @@ const bw = {
56
56
  _subIdCounter: 0, // monotonic ID for subscriptions
57
57
 
58
58
  // ── Node reference cache ──────────────────────────────────────────────
59
- // Fast O(1) lookup for elements by bw_id, id attribute, or bw_uuid.
59
+ // Fast O(1) lookup for elements by id attribute or bw_uuid_* class.
60
60
  //
61
61
  // Populated by bw.createDOM() when elements have:
62
- // - data-bw_id attribute (user-declared addressable elements)
63
62
  // - id attribute (standard HTML id)
64
- // - bw_uuid (internal, for lifecycle-managed elements)
63
+ // - bw_uuid_* class (lifecycle-managed or explicitly addressed elements)
65
64
  //
66
65
  // Cleaned up by bw.cleanup() when elements are destroyed via bitwrench APIs.
67
66
  // On cache miss, falls back to querySelector/getElementById — never fails,
@@ -69,7 +68,7 @@ const bw = {
69
68
  // via parentNode === null check (IE11-safe, unlike el.isConnected).
70
69
  //
71
70
  // Elements created via bw.createDOM() also get el._bw_refs — a local map of
72
- // child bw_id DOM node ref for fast parentchild access in o.render.
71
+ // child id/UUID -> DOM node ref for fast parent->child access in o.render.
73
72
  // This is the bitwrench equivalent of React's compiled template "holes".
74
73
  //
75
74
  // Contract: if you remove elements outside of bitwrench APIs (raw el.remove()),
@@ -149,7 +148,6 @@ Object.defineProperty(bw, '_isBrowser', {
149
148
  // _cw console.warn 8
150
149
  // _cl console.log 11
151
150
  // _ce console.error 4
152
- // _chp ComponentHandle.prototype 28 (defined after constructor)
153
151
  //
154
152
  // Note: document.createElement etc. are NOT aliased because they require
155
153
  // `this === document` and .bind() would add overhead on every call.
@@ -322,15 +320,15 @@ bw.uuid = function(prefix) {
322
320
  * 1. Check `bw._nodeMap[id]` — if found and still attached (parentNode !== null), return it
323
321
  * 2. If cached ref is detached (parentNode === null), remove stale entry
324
322
  * 3. Fall back to `document.getElementById(id)` then `document.querySelector(...)`
325
- * 4. If fallback finds the element, cache it for next time
326
- * 5. If not found anywhere, return null
323
+ * 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
324
+ * 5. Cache the result for next time
327
325
  *
328
326
  * Accepts a DOM element directly (pass-through) or a string identifier.
329
327
  * String identifiers are tried as: direct map key, getElementById,
330
328
  * querySelector (for CSS selectors starting with . or #), and
331
- * data-bw_id attribute selector.
329
+ * bw_uuid_* class selector.
332
330
  *
333
- * @param {string|Element} id - Element ID, CSS selector, data-bw_id value, or DOM element
331
+ * @param {string|Element} id - Element ID, CSS selector, bw_uuid_* class, or DOM element
334
332
  * @returns {Element|null} The DOM element, or null if not found
335
333
  * @category Internal
336
334
  */
@@ -359,17 +357,12 @@ bw._el = function(id) {
359
357
  el = document.querySelector(id);
360
358
  }
361
359
 
362
- // 4. Try data-bw_id attribute (for bw.uuid-generated IDs)
363
- if (!el) {
364
- el = document.querySelector('[data-bw_id="' + id + '"]');
365
- }
366
-
367
- // 5. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
360
+ // 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
368
361
  if (!el && id.indexOf('bw_uuid_') === 0) {
369
362
  el = document.querySelector('.' + id);
370
363
  }
371
364
 
372
- // 6. Cache the result for next time
365
+ // 5. Cache the result for next time
373
366
  if (el) {
374
367
  bw._nodeMap[id] = el;
375
368
  }
@@ -381,17 +374,17 @@ bw._el = function(id) {
381
374
  * Register a DOM element in the node cache under one or more keys.
382
375
  *
383
376
  * Called internally by `bw.createDOM()`. Registers elements that have
384
- * id attributes, data-bw_id attributes, or both.
377
+ * id attributes, UUID classes, or both.
385
378
  *
386
379
  * @param {Element} el - DOM element to register
387
- * @param {string} [bwId] - data-bw_id value to register under
380
+ * @param {string} [uuid] - bw_uuid_* class token to register under
388
381
  * @category Internal
389
382
  */
390
- bw._registerNode = function(el, bwId) {
383
+ bw._registerNode = function(el, uuid) {
391
384
  if (!el) return;
392
- // Register under data-bw_id
393
- if (bwId) {
394
- bw._nodeMap[bwId] = el;
385
+ // Register under UUID class token
386
+ if (uuid) {
387
+ bw._nodeMap[uuid] = el;
395
388
  }
396
389
  // Register under id attribute
397
390
  var htmlId = el.getAttribute ? el.getAttribute('id') : null;
@@ -407,13 +400,13 @@ bw._registerNode = function(el, bwId) {
407
400
  * through bitwrench APIs.
408
401
  *
409
402
  * @param {Element} el - DOM element to deregister
410
- * @param {string} [bwId] - data-bw_id value to remove
403
+ * @param {string} [uuid] - bw_uuid_* class token to remove
411
404
  * @category Internal
412
405
  */
413
- bw._deregisterNode = function(el, bwId) {
414
- // Remove data-bw_id entry
415
- if (bwId) {
416
- delete bw._nodeMap[bwId];
406
+ bw._deregisterNode = function(el, uuid) {
407
+ // Remove UUID class entry
408
+ if (uuid) {
409
+ delete bw._nodeMap[uuid];
417
410
  }
418
411
  // Remove id attribute entry
419
412
  var htmlId = el && el.getAttribute ? el.getAttribute('id') : null;
@@ -426,6 +419,13 @@ bw._deregisterNode = function(el, bwId) {
426
419
  // bw.assignUUID() / bw.getUUID() — Explicit UUID addressing for TACO objects
427
420
  // ===================================================================================
428
421
 
422
+ /**
423
+ * Marker class for elements with lifecycle hooks (mounted/unmount/render/state).
424
+ * Used by cleanup() to find lifecycle-managed elements via querySelectorAll('.bw_lc').
425
+ * @private
426
+ */
427
+ var _BW_LC = 'bw_lc';
428
+
429
429
  /**
430
430
  * Regex to match a bw_uuid_* token in a class string.
431
431
  * @private
@@ -614,15 +614,6 @@ bw.html = function(taco, options = {}) {
614
614
  // Handle null/undefined
615
615
  if (taco == null) return '';
616
616
 
617
- // Handle ComponentHandle — use its .taco
618
- if (taco && taco._bwComponent === true) {
619
- var compOptions = Object.assign({}, options);
620
- if (!compOptions.state && taco._state) {
621
- compOptions.state = taco._state;
622
- }
623
- return bw.html(taco.taco, compOptions);
624
- }
625
-
626
617
  // Handle arrays of TACOs
627
618
  if (_isA(taco)) {
628
619
  return taco.map(t => bw.html(t, options)).join('');
@@ -633,24 +624,6 @@ bw.html = function(taco, options = {}) {
633
624
  return taco.v;
634
625
  }
635
626
 
636
- // Handle bw.when() markers
637
- if (taco && taco._bwWhen && options.state) {
638
- var whenExpr = taco.expr.replace(/^\$\{|\}$/g, '');
639
- var whenVal = options.compile
640
- ? bw._resolveTemplate('${' + whenExpr + '}', options.state, true)
641
- : bw._evaluatePath(options.state, whenExpr);
642
- var branch = whenVal ? taco.branches[0] : (taco.branches[1] || null);
643
- return branch ? bw.html(branch, options) : '';
644
- }
645
-
646
- // Handle bw.each() markers
647
- if (taco && taco._bwEach && options.state) {
648
- var eachExpr = taco.expr.replace(/^\$\{|\}$/g, '');
649
- var arr = bw._evaluatePath(options.state, eachExpr);
650
- if (!_isA(arr)) return '';
651
- return arr.map(function(item, idx) { return bw.html(taco.factory(item, idx), options); }).join('');
652
- }
653
-
654
627
  // Handle primitives and non-TACO objects
655
628
  if (!_is(taco, 'object') || !taco.t) {
656
629
  var str = options.raw ? String(taco) : bw.escapeHTML(String(taco));
@@ -714,14 +687,14 @@ bw.html = function(taco, options = {}) {
714
687
  }
715
688
  }
716
689
 
717
- // Add bw_id as a class if lifecycle hooks present
718
- if ((opts.mounted || opts.unmount) && !attrs.class?.includes('bw_id_')) {
719
- const id = opts.bw_id || bw.uuid();
690
+ // Add bw_uuid + bw_lc classes if lifecycle hooks present
691
+ if ((opts.mounted || opts.unmount) && !_UUID_RE.test(attrs.class || '')) {
692
+ const uuid = bw.uuid('uuid');
720
693
  attrStr = attrStr.replace(/class="([^"]*)"/, (_match, classes) => {
721
- return `class="${classes} bw_id_${id}"`.trim();
694
+ return `class="${classes} ${uuid} ${_BW_LC}"`.trim();
722
695
  });
723
696
  if (!attrStr.includes('class=')) {
724
- attrStr += ` class="bw_id_${id}"`;
697
+ attrStr += ` class="${uuid} ${_BW_LC}"`;
725
698
  }
726
699
  }
727
700
 
@@ -949,11 +922,6 @@ bw.createDOM = function(taco, options = {}) {
949
922
  return frag;
950
923
  }
951
924
 
952
- // Handle ComponentHandle — extract .taco for DOM creation
953
- if (taco && taco._bwComponent === true) {
954
- return bw.createDOM(taco.taco, options);
955
- }
956
-
957
925
  // Handle text nodes
958
926
  if (!_is(taco, 'object') || !taco.t) {
959
927
  return document.createTextNode(String(taco));
@@ -994,24 +962,19 @@ bw.createDOM = function(taco, options = {}) {
994
962
  }
995
963
 
996
964
  // Add children, building _bw_refs for fast parent→child access.
997
- // Children with data-bw_id or id attributes get local refs on the parent,
965
+ // Children with id attributes or bw_uuid_* classes get local refs on the parent,
998
966
  // so o.render functions can access them without any DOM lookup.
999
967
  if (content != null) {
1000
968
  if (_isA(content)) {
1001
969
  content.forEach(child => {
1002
970
  if (child != null) {
1003
- // Handle ComponentHandle in content arrays (Level 2 children)
1004
- if (child._bwComponent === true) {
1005
- child.mount(el);
1006
- return;
1007
- }
1008
971
  var childEl = bw.createDOM(child, options);
1009
972
  el.appendChild(childEl);
1010
973
  // Build local refs for addressable children
1011
- var childBwId = (child && child.a) ? (child.a['data-bw_id'] || child.a.id) : null;
1012
- if (childBwId) {
974
+ var childRefId = (child && child.a) ? (child.a.id || bw.getUUID(child)) : null;
975
+ if (childRefId) {
1013
976
  if (!el._bw_refs) el._bw_refs = {};
1014
- el._bw_refs[childBwId] = childEl;
977
+ el._bw_refs[childRefId] = childEl;
1015
978
  }
1016
979
  // Bubble up grandchild refs (flatten one level)
1017
980
  if (childEl._bw_refs) {
@@ -1027,16 +990,13 @@ bw.createDOM = function(taco, options = {}) {
1027
990
  } else if (_is(content, 'object') && content.__bw_raw) {
1028
991
  // Raw HTML content — inject via innerHTML
1029
992
  el.innerHTML = content.v;
1030
- } else if (content._bwComponent === true) {
1031
- // Single ComponentHandle as content
1032
- content.mount(el);
1033
993
  } else if (_is(content, 'object') && content.t) {
1034
994
  var childEl = bw.createDOM(content, options);
1035
995
  el.appendChild(childEl);
1036
- var childBwId = content.a ? (content.a['data-bw_id'] || content.a.id) : null;
1037
- if (childBwId) {
996
+ var childRefId = content.a ? (content.a.id || bw.getUUID(content)) : null;
997
+ if (childRefId) {
1038
998
  if (!el._bw_refs) el._bw_refs = {};
1039
- el._bw_refs[childBwId] = childEl;
999
+ el._bw_refs[childRefId] = childEl;
1040
1000
  }
1041
1001
  if (childEl._bw_refs) {
1042
1002
  if (!el._bw_refs) el._bw_refs = {};
@@ -1066,57 +1026,88 @@ bw.createDOM = function(taco, options = {}) {
1066
1026
 
1067
1027
  // Handle lifecycle hooks and state
1068
1028
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
1069
- const id = attrs['data-bw_id'] || bw.uuid();
1070
- el.setAttribute('data-bw_id', id);
1029
+ // Ensure element has a UUID class for identity
1030
+ var uuid = bw.getUUID(el) || bw.uuid('uuid');
1031
+ el.classList.add(uuid);
1032
+ el.classList.add(_BW_LC);
1071
1033
 
1072
- // Register in node cache under data-bw_id
1073
- bw._registerNode(el, id);
1034
+ // Register in node cache under UUID class
1035
+ bw._registerNode(el, uuid);
1074
1036
 
1075
1037
  // Store state
1076
1038
  if (opts.state) {
1077
1039
  el._bw_state = opts.state;
1078
1040
  }
1079
1041
 
1080
- // o.render — first-class render function (replaces mounted boilerplate)
1042
+ // o.render — store the render function for bw.update()
1081
1043
  if (opts.render) {
1082
1044
  el._bw_render = opts.render;
1045
+ }
1083
1046
 
1084
- if (opts.mounted) {
1085
- _cw('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
1086
- }
1047
+ // Determine what to call on mount:
1048
+ // - If o.mounted exists, call it (it can call el._bw_render() for initial render)
1049
+ // - Otherwise if o.render exists, auto-call it as a convenience shorthand
1050
+ var mountFn = opts.mounted || (opts.render ? function(mountEl) {
1051
+ opts.render(mountEl, mountEl._bw_state || {});
1052
+ } : null);
1087
1053
 
1088
- // Queue initial render (same timing as mounted)
1089
- if (document.body.contains(el)) {
1090
- opts.render(el, el._bw_state || {});
1091
- } else {
1092
- requestAnimationFrame(() => {
1093
- if (document.body.contains(el)) {
1094
- opts.render(el, el._bw_state || {});
1095
- }
1096
- });
1097
- }
1098
- } else if (opts.mounted) {
1099
- // Queue mounted callback (legacy pattern)
1054
+ if (mountFn) {
1100
1055
  if (document.body.contains(el)) {
1101
- opts.mounted(el, el._bw_state || {});
1056
+ mountFn(el, el._bw_state || {});
1102
1057
  } else {
1103
1058
  requestAnimationFrame(() => {
1104
1059
  if (document.body.contains(el)) {
1105
- opts.mounted(el, el._bw_state || {});
1060
+ mountFn(el, el._bw_state || {});
1106
1061
  }
1107
1062
  });
1108
1063
  }
1109
1064
  }
1110
1065
 
1111
- // Store unmount callback
1066
+ // Store unmount callback keyed by UUID class
1112
1067
  if (opts.unmount) {
1113
- bw._unmountCallbacks.set(id, () => {
1068
+ bw._unmountCallbacks.set(uuid, () => {
1114
1069
  opts.unmount(el, el._bw_state || {});
1115
1070
  });
1116
1071
  }
1117
- } else if (attrs['data-bw_id']) {
1118
- // Element has explicit data-bw_id but no lifecycle hooks — still register it
1119
- bw._registerNode(el, attrs['data-bw_id']);
1072
+ }
1073
+
1074
+ // Component handle: attach methods to el.bw namespace
1075
+ if (opts.handle || opts.slots) {
1076
+ if (!el.bw) el.bw = {};
1077
+
1078
+ // Explicit handle methods: fn(el, ...args) -> el.bw.method(...args)
1079
+ if (opts.handle) {
1080
+ for (var hk in opts.handle) {
1081
+ if (_hop.call(opts.handle, hk)) {
1082
+ el.bw[hk] = opts.handle[hk].bind(null, el);
1083
+ }
1084
+ }
1085
+ }
1086
+
1087
+ // Slot declarations: auto-generate setX/getX pairs
1088
+ if (opts.slots) {
1089
+ for (var sk in opts.slots) {
1090
+ if (_hop.call(opts.slots, sk)) {
1091
+ (function(name, selector) {
1092
+ var cap = name.charAt(0).toUpperCase() + name.slice(1);
1093
+ el.bw['set' + cap] = function(value) {
1094
+ var t = el.querySelector(selector);
1095
+ if (!t) return;
1096
+ if (value != null && typeof value === 'object' && value.t) {
1097
+ t.innerHTML = '';
1098
+ t.appendChild(bw.createDOM(value));
1099
+ } else {
1100
+ t.textContent = (value != null) ? String(value) : '';
1101
+ }
1102
+ };
1103
+ el.bw['get' + cap] = function() {
1104
+ var t = el.querySelector(selector);
1105
+ return t ? t.textContent : '';
1106
+ };
1107
+ })(sk, opts.slots[sk]);
1108
+ }
1109
+ }
1110
+ }
1120
1111
  }
1121
1112
 
1122
1113
  return el;
@@ -1163,7 +1154,7 @@ bw.DOM = function(target, taco, options = {}) {
1163
1154
  // the target is the mount point, not the content being replaced)
1164
1155
  const savedState = targetEl._bw_state;
1165
1156
  const savedRender = targetEl._bw_render;
1166
- const savedBwId = targetEl.getAttribute('data-bw_id');
1157
+ const savedUuid = bw.getUUID(targetEl);
1167
1158
  const savedSubs = targetEl._bw_subs;
1168
1159
 
1169
1160
  // Temporarily remove _bw_subs so cleanup doesn't call them
@@ -1175,10 +1166,9 @@ bw.DOM = function(target, taco, options = {}) {
1175
1166
  // Restore the target's own state/render/subs after cleanup
1176
1167
  if (savedState !== undefined) targetEl._bw_state = savedState;
1177
1168
  if (savedRender) targetEl._bw_render = savedRender;
1178
- if (savedBwId) {
1179
- targetEl.setAttribute('data-bw_id', savedBwId);
1180
- // Re-register mount point in node cache (cleanup deregistered it)
1181
- bw._registerNode(targetEl, savedBwId);
1169
+ if (savedUuid) {
1170
+ // UUID class stays on element through cleanup; re-register in cache
1171
+ bw._registerNode(targetEl, savedUuid);
1182
1172
  }
1183
1173
  if (savedSubs) targetEl._bw_subs = savedSubs;
1184
1174
 
@@ -1186,25 +1176,11 @@ bw.DOM = function(target, taco, options = {}) {
1186
1176
  targetEl.innerHTML = '';
1187
1177
 
1188
1178
  if (taco != null) {
1189
- // Handle ComponentHandle (reactive components from bw.component())
1190
- if (taco._bwComponent === true) {
1191
- taco.mount(targetEl);
1192
- }
1193
- // Handle component handles (objects with element property)
1194
- else if (taco.element instanceof Element) {
1195
- targetEl.appendChild(taco.element);
1196
- }
1197
1179
  // Handle arrays
1198
- else if (_isA(taco)) {
1180
+ if (_isA(taco)) {
1199
1181
  taco.forEach(t => {
1200
1182
  if (t != null) {
1201
- if (t._bwComponent === true) {
1202
- t.mount(targetEl);
1203
- } else if (t.element instanceof Element) {
1204
- targetEl.appendChild(t.element);
1205
- } else {
1206
- targetEl.appendChild(bw.createDOM(t, options));
1207
- }
1183
+ targetEl.appendChild(bw.createDOM(t, options));
1208
1184
  }
1209
1185
  });
1210
1186
  }
@@ -1217,205 +1193,36 @@ bw.DOM = function(target, taco, options = {}) {
1217
1193
  return targetEl;
1218
1194
  };
1219
1195
 
1220
- /**
1221
- * Compile props into getter/setter functions for reactive updates.
1222
- *
1223
- * Used internally by `bw.renderComponent()`. Creates a proxy-like object
1224
- * where setting a property triggers `handle.onPropChange()`.
1225
- *
1226
- * @param {Object} handle - Component handle
1227
- * @param {Object} props - Initial props
1228
- * @returns {Object} Compiled props object with getters/setters
1229
- * @category DOM Generation
1230
- */
1231
- bw.compileProps = function(handle, props = {}) {
1232
- const compiledProps = {};
1233
-
1234
- _keys(props).forEach(key => {
1235
- // Create getter/setter for each prop
1236
- Object.defineProperty(compiledProps, key, {
1237
- get() {
1238
- return handle._props[key];
1239
- },
1240
- set(value) {
1241
- const oldValue = handle._props[key];
1242
- if (oldValue !== value) {
1243
- handle._props[key] = value;
1244
- // Trigger update if prop changed
1245
- if (handle.onPropChange) {
1246
- handle.onPropChange(key, value, oldValue);
1247
- }
1248
- }
1249
- },
1250
- enumerable: true,
1251
- configurable: true
1252
- });
1253
- });
1254
-
1255
- return compiledProps;
1256
- };
1196
+ // Deprecation stubs for removed ComponentHandle APIs
1197
+ bw.compileProps = function() { throw new Error('bw.compileProps() removed in v2.0.19. Use o.handle/o.slots instead.'); };
1198
+ bw.renderComponent = function() { throw new Error('bw.renderComponent() removed in v2.0.19. Use bw.mount() with o.handle/o.slots instead.'); };
1257
1199
 
1258
1200
  /**
1259
- * Render a TACO component and return an enhanced handle object.
1201
+ * Mount a TACO into a target element and return the created root element.
1202
+ * Like bw.DOM() but returns the root element of the TACO (not the container),
1203
+ * giving direct access to el.bw handle methods.
1260
1204
  *
1261
- * The handle provides compiled props, state management, child registration,
1262
- * and a destroy method. Used internally by `bw.createCard()`, `bw.createTable()`, etc.
1263
- *
1264
- * @param {Object} taco - TACO object to render
1265
- * @param {Object} [options] - Render options
1266
- * @returns {Object} Component handle with element, props, state, update(), destroy()
1205
+ * @param {string|Element} target - CSS selector or DOM element
1206
+ * @param {Object} taco - TACO to render
1207
+ * @param {Object} [options] - Mount options
1208
+ * @returns {Element} The created root element
1267
1209
  * @category DOM Generation
1268
- */
1269
- bw.renderComponent = function(taco, options = {}) {
1270
- const element = bw.createDOM(taco, options);
1271
-
1272
- // Enhanced handle with prop compilation
1273
- const handle = {
1274
- element,
1275
- taco,
1276
- _props: { ...taco.a }, // Store props internally
1277
- _state: taco.o?.state || {},
1278
- _children: {}, // Store child component references
1279
-
1280
- // Get compiled props with getters/setters
1281
- get props() {
1282
- if (!this._compiledProps) {
1283
- this._compiledProps = bw.compileProps(this, this._props);
1284
- }
1285
- return this._compiledProps;
1286
- },
1287
-
1288
- /**
1289
- * Query all matching elements within this component
1290
- * @param {string} selector - CSS selector
1291
- * @returns {NodeList} Matching elements
1292
- */
1293
- $(selector) {
1294
- return this.element.querySelectorAll(selector);
1295
- },
1296
-
1297
- /**
1298
- * Query the first matching element within this component
1299
- * @param {string} selector - CSS selector
1300
- * @returns {Element|null} First matching element or null
1301
- */
1302
- $first(selector) {
1303
- return this.element.querySelector(selector);
1304
- },
1305
-
1306
- /**
1307
- * Update component with new props and re-render in place
1308
- * @param {Object} newProps - Properties to merge into current props
1309
- * @returns {Object} this handle (for chaining)
1310
- */
1311
- update(newProps) {
1312
- // Update internal props
1313
- Object.assign(this._props, newProps);
1314
-
1315
- // Rebuild TACO with new props
1316
- const newTaco = { ...this.taco, a: { ...this.taco.a, ...newProps } };
1317
- const newElement = bw.createDOM(newTaco, options);
1318
-
1319
- // Replace in DOM
1320
- this.element.replaceWith(newElement);
1321
- this.element = newElement;
1322
- this.taco = newTaco;
1323
-
1324
- return this;
1325
- },
1326
-
1327
- /**
1328
- * Re-render the component from its current TACO, replacing the DOM element
1329
- * @returns {Object} this handle (for chaining)
1330
- */
1331
- render() {
1332
- const newElement = bw.createDOM(this.taco, options);
1333
- this.element.replaceWith(newElement);
1334
- this.element = newElement;
1335
- return this;
1336
- },
1337
-
1338
- /**
1339
- * Called when a compiled prop value changes. Override to customize behavior.
1340
- * Default implementation triggers a full re-render.
1341
- * @param {string} key - Property name that changed
1342
- * @param {*} newValue - New property value
1343
- * @param {*} oldValue - Previous property value
1344
- */
1345
- onPropChange(_key, _newValue, _oldValue) {
1346
- // Auto re-render on prop change by default
1347
- this.render();
1348
- },
1349
-
1350
- // State management
1351
- get state() {
1352
- return this._state;
1353
- },
1354
-
1355
- set state(newState) {
1356
- this._state = newState;
1357
- this.render();
1358
- },
1359
-
1360
- /**
1361
- * Merge state updates and re-render the component
1362
- * @param {Object} updates - State properties to merge
1363
- * @returns {Object} this handle (for chaining)
1364
- */
1365
- setState(updates) {
1366
- Object.assign(this._state, updates);
1367
- this.render();
1368
- return this;
1369
- },
1370
-
1371
- /**
1372
- * Register a child component under a name for later retrieval
1373
- * @param {string} name - Child name key
1374
- * @param {Object} component - Child component handle
1375
- * @returns {Object} this handle (for chaining)
1376
- */
1377
- addChild(name, component) {
1378
- this._children[name] = component;
1379
- return this;
1380
- },
1381
-
1382
- /**
1383
- * Retrieve a registered child component by name
1384
- * @param {string} name - Child name key
1385
- * @returns {Object|undefined} Child component handle
1386
- */
1387
- getChild(name) {
1388
- return this._children[name];
1389
- },
1390
-
1391
- /**
1392
- * Destroy this component and all registered children
1393
- *
1394
- * Calls destroy() recursively on children, runs bw.cleanup(),
1395
- * removes the element from DOM, and clears all internal references.
1396
- */
1397
- destroy() {
1398
- // Destroy children first
1399
- Object.values(this._children).forEach(child => {
1400
- if (child && child.destroy) child.destroy();
1401
- });
1402
-
1403
- // Clean up this component
1404
- bw.cleanup(this.element);
1405
- this.element.remove();
1406
-
1407
- // Clear references
1408
- this._children = {};
1409
- this._props = {};
1410
- this._state = {};
1411
- this._compiledProps = null;
1412
- }
1413
- };
1414
-
1415
- // Store handle reference on element
1416
- element._bwHandle = handle;
1417
-
1418
- return handle;
1210
+ * @example
1211
+ * var el = bw.mount('#app', bw.makeCarousel({ items: slides }));
1212
+ * el.bw.goToSlide(2);
1213
+ * el.bw.next();
1214
+ */
1215
+ bw.mount = function(target, taco, options) {
1216
+ var container = _is(target, 'string') ? bw.$(target)[0] : target;
1217
+ if (!container) {
1218
+ _cw('bw.mount: target not found');
1219
+ return null;
1220
+ }
1221
+ bw.cleanup(container);
1222
+ container.innerHTML = '';
1223
+ var el = bw.createDOM(taco, options || {});
1224
+ container.appendChild(el);
1225
+ return el;
1419
1226
  };
1420
1227
 
1421
1228
  /**
@@ -1436,34 +1243,29 @@ bw.renderComponent = function(taco, options = {}) {
1436
1243
  bw.cleanup = function(element) {
1437
1244
  if (!bw._isBrowser || !element) return;
1438
1245
 
1439
- // Deregister UUID classes from node cache (element + descendants)
1440
- // Covers elements that have UUID but no data-bw_id
1441
- var selfUuidMatch = element.className && element.className.match(_UUID_RE);
1442
- if (selfUuidMatch) delete bw._nodeMap[selfUuidMatch[0]];
1246
+ // Deregister UUID classes from node cache for non-lifecycle UUID elements
1443
1247
  var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
1444
1248
  uuidEls.forEach(function(uel) {
1445
1249
  var m = uel.className && uel.className.match(_UUID_RE);
1446
1250
  if (m) delete bw._nodeMap[m[0]];
1447
1251
  });
1448
1252
 
1449
- // Find all elements with data-bw_id
1450
- const elements = element.querySelectorAll('[data-bw_id]');
1253
+ // Find all lifecycle-managed elements (have bw_lc marker class)
1254
+ const elements = element.querySelectorAll('.' + _BW_LC);
1451
1255
 
1452
1256
  elements.forEach(el => {
1453
- const id = el.getAttribute('data-bw_id');
1454
- const callback = bw._unmountCallbacks.get(id);
1455
-
1456
- if (callback) {
1457
- callback();
1458
- bw._unmountCallbacks.delete(id);
1459
- }
1257
+ var uuid = bw.getUUID(el);
1460
1258
 
1461
- // Deregister from node cache
1462
- bw._deregisterNode(el, id);
1259
+ if (uuid) {
1260
+ const callback = bw._unmountCallbacks.get(uuid);
1261
+ if (callback) {
1262
+ callback();
1263
+ bw._unmountCallbacks.delete(uuid);
1264
+ }
1463
1265
 
1464
- // Deregister UUID class from node cache
1465
- var uuidMatch = el.className && el.className.match(_UUID_RE);
1466
- if (uuidMatch) delete bw._nodeMap[uuidMatch[0]];
1266
+ // Deregister from node cache
1267
+ bw._deregisterNode(el, uuid);
1268
+ }
1467
1269
 
1468
1270
  // Clean up pub/sub subscriptions tied to this element
1469
1271
  if (el._bw_subs) {
@@ -1478,20 +1280,18 @@ bw.cleanup = function(element) {
1478
1280
  });
1479
1281
 
1480
1282
  // Check element itself
1481
- const id = element.getAttribute('data-bw_id');
1482
- if (id) {
1483
- const callback = bw._unmountCallbacks.get(id);
1283
+ var selfUuid = bw.getUUID(element);
1284
+ if (selfUuid) {
1285
+ delete bw._nodeMap[selfUuid];
1286
+
1287
+ const callback = bw._unmountCallbacks.get(selfUuid);
1484
1288
  if (callback) {
1485
1289
  callback();
1486
- bw._unmountCallbacks.delete(id);
1290
+ bw._unmountCallbacks.delete(selfUuid);
1487
1291
  }
1488
1292
 
1489
1293
  // Deregister from node cache
1490
- bw._deregisterNode(element, id);
1491
-
1492
- // Deregister UUID class from node cache
1493
- var elemUuidMatch = element.className && element.className.match(_UUID_RE);
1494
- if (elemUuidMatch) delete bw._nodeMap[elemUuidMatch[0]];
1294
+ bw._deregisterNode(element, selfUuid);
1495
1295
 
1496
1296
  // Clean up pub/sub subscriptions tied to element itself
1497
1297
  if (element._bw_subs) {
@@ -1502,11 +1302,11 @@ bw.cleanup = function(element) {
1502
1302
  delete element._bw_render;
1503
1303
  delete element._bw_refs;
1504
1304
 
1505
- // Clean up ComponentHandle back-reference
1506
- if (element._bwComponentHandle) {
1507
- element._bwComponentHandle.mounted = false;
1508
- element._bwComponentHandle.element = null;
1509
- delete element._bwComponentHandle;
1305
+ } else {
1306
+ // No UUID on element itself, but still check for _bw_subs (from bw.sub())
1307
+ if (element._bw_subs) {
1308
+ element._bw_subs.forEach(function(unsub) { unsub(); });
1309
+ delete element._bw_subs;
1510
1310
  }
1511
1311
  }
1512
1312
  };
@@ -1522,7 +1322,7 @@ bw.cleanup = function(element) {
1522
1322
  * Calls `el._bw_render(el, state)` and emits `bw:statechange` so other
1523
1323
  * components can react without tight coupling.
1524
1324
  *
1525
- * @param {string|Element} target - Element ID, data-bw_id, CSS selector, or DOM element
1325
+ * @param {string|Element} target - Element ID, bw_uuid_* class, CSS selector, or DOM element
1526
1326
  * @returns {Element|null} The element, or null if not found / no render function
1527
1327
  * @category State Management
1528
1328
  * @see bw.patch
@@ -1547,7 +1347,7 @@ bw.update = function(target) {
1547
1347
  * Use `bw.patch()` for lightweight value updates (scores, labels, counters)
1548
1348
  * and `bw.update()` for full structural re-renders.
1549
1349
  *
1550
- * @param {string|Element} id - Element ID, data-bw_id, CSS selector, or DOM element.
1350
+ * @param {string|Element} id - Element ID, bw_uuid_* class, CSS selector, or DOM element.
1551
1351
  * Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
1552
1352
  * @param {string|Object} content - New text content, or TACO object to replace children
1553
1353
  * @param {string} [attr] - If provided, sets this attribute instead of content
@@ -1622,7 +1422,7 @@ bw.patchAll = function(patches) {
1622
1422
  * bubble by default so ancestor elements can listen. Use with `bw.on()` for
1623
1423
  * DOM-scoped communication between components.
1624
1424
  *
1625
- * @param {string|Element} target - Element ID, data-bw_id, CSS selector, or DOM element.
1425
+ * @param {string|Element} target - Element ID, bw_uuid_* class, CSS selector, or DOM element.
1626
1426
  * Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
1627
1427
  * @param {string} eventName - Event name (will be prefixed with 'bw:')
1628
1428
  * @param {*} [detail] - Data to pass with the event
@@ -1649,7 +1449,7 @@ bw.emit = function(target, eventName, detail) {
1649
1449
  * is the first argument so you don't need to destructure `e.detail`.
1650
1450
  * Events bubble, so you can listen on an ancestor element.
1651
1451
  *
1652
- * @param {string|Element} target - Element ID, data-bw_id, CSS selector, or DOM element.
1452
+ * @param {string|Element} target - Element ID, bw_uuid_* class, CSS selector, or DOM element.
1653
1453
  * Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
1654
1454
  * @param {string} eventName - Event name (will be prefixed with 'bw:')
1655
1455
  * @param {Function} handler - Called with (detail, event)
@@ -1747,10 +1547,12 @@ bw.sub = function(topic, handler, el) {
1747
1547
  if (el) {
1748
1548
  if (!el._bw_subs) el._bw_subs = [];
1749
1549
  el._bw_subs.push(unsub);
1750
- // Ensure element has data-bw_id so bw.cleanup() finds it
1751
- if (!el.getAttribute('data-bw_id')) {
1752
- var bwId = 'bw_sub_' + id;
1753
- el.setAttribute('data-bw_id', bwId);
1550
+ // Ensure element has UUID + bw_lc so bw.cleanup() finds it
1551
+ if (!bw.getUUID(el)) {
1552
+ el.classList.add(bw.uuid('uuid'));
1553
+ }
1554
+ if (!el.classList.contains(_BW_LC)) {
1555
+ el.classList.add(_BW_LC);
1754
1556
  }
1755
1557
  }
1756
1558
 
@@ -1972,1087 +1774,46 @@ bw._resolveTemplate = function(str, state, compile) {
1972
1774
  return result;
1973
1775
  };
1974
1776
 
1975
- /**
1976
- * Extract top-level state keys that an expression depends on.
1977
- * @param {string} expr - Expression string
1978
- * @param {string[]} stateKeys - Declared state keys
1979
- * @returns {string[]} Matching dependency keys
1980
- * @private
1981
- */
1982
- bw._extractDeps = function(expr, stateKeys) {
1983
- var deps = [];
1984
- for (var i = 0; i < stateKeys.length; i++) {
1985
- var key = stateKeys[i];
1986
- // Match word boundary: key must be preceded by start/non-word and followed by non-word/end
1987
- var re = new RegExp('(?:^|[^\\w$.])' + key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '(?:[^\\w$]|$)');
1988
- if (re.test(expr) || expr === key || expr.indexOf(key + '.') === 0) {
1989
- deps.push(key);
1990
- }
1991
- }
1992
- return deps;
1993
- };
1994
-
1995
1777
  // ===================================================================================
1996
- // Microtask Batching
1778
+ // Deprecation stubs for removed ComponentHandle APIs (v2.0.19)
1997
1779
  // ===================================================================================
1998
1780
 
1999
- bw._dirtyComponents = [];
2000
- bw._flushScheduled = false;
2001
-
2002
- /**
2003
- * Schedule a microtask flush for dirty components.
2004
- * @private
2005
- */
2006
- bw._scheduleFlush = function() {
2007
- if (bw._flushScheduled) return;
2008
- bw._flushScheduled = true;
2009
- if (typeof Promise !== 'undefined') {
2010
- Promise.resolve().then(bw._doFlush);
2011
- } else {
2012
- setTimeout(bw._doFlush, 0);
2013
- }
2014
- };
2015
-
2016
- /**
2017
- * Flush all dirty components. Deduplicates by _bwId.
2018
- * @private
2019
- */
2020
- bw._doFlush = function() {
2021
- bw._flushScheduled = false;
2022
- var queue = bw._dirtyComponents.slice();
2023
- bw._dirtyComponents = [];
2024
- // Deduplicate by _bwId
2025
- var seen = {};
2026
- for (var i = 0; i < queue.length; i++) {
2027
- var comp = queue[i];
2028
- if (!seen[comp._bwId]) {
2029
- seen[comp._bwId] = true;
2030
- comp._flush();
2031
- }
2032
- }
2033
- };
1781
+ bw._extractDeps = undefined;
1782
+ bw._dirtyComponents = undefined;
1783
+ bw._flushScheduled = undefined;
1784
+ bw._scheduleFlush = undefined;
1785
+ bw._doFlush = undefined;
1786
+ bw._ComponentHandle = undefined;
2034
1787
 
2035
1788
  /**
2036
- * Synchronous flush for testing and imperative code.
2037
- * Forces immediate re-render of all dirty components.
2038
- *
1789
+ * No-op flush (ComponentHandle removed in v2.0.19).
1790
+ * Kept as no-op for backward compatibility.
2039
1791
  * @category Component
2040
1792
  */
2041
- bw.flush = function() {
2042
- bw._doFlush();
2043
- };
2044
-
2045
- // ===================================================================================
2046
- // ComponentHandle — unified reactive component (Phase 1)
2047
- // ===================================================================================
1793
+ bw.flush = function() {};
2048
1794
 
2049
- /**
2050
- * ComponentHandle constructor.
2051
- * Wraps a TACO definition with reactive state, lifecycle hooks,
2052
- * template bindings, and named actions.
2053
- *
2054
- * @param {Object} taco - TACO definition {t, a, c, o}
2055
- * @constructor
2056
- * @private
2057
- */
2058
- function ComponentHandle(taco) {
2059
- this._bwComponent = true; // duck-type marker
2060
- this._bwId = bw.uuid('comp');
2061
- this.taco = taco;
2062
- this.element = null;
2063
- this.mounted = false;
2064
-
2065
- var o = taco.o || {};
2066
- // Copy initial state
2067
- this._state = {};
2068
- if (o.state) {
2069
- for (var k in o.state) {
2070
- if (_hop.call(o.state, k)) {
2071
- this._state[k] = o.state[k];
2072
- }
2073
- }
2074
- }
2075
- // Copy actions
2076
- this._actions = {};
2077
- if (o.actions) {
2078
- for (var k2 in o.actions) {
2079
- if (_hop.call(o.actions, k2)) {
2080
- this._actions[k2] = o.actions[k2];
2081
- }
2082
- }
2083
- }
2084
- // Promote o.methods to handle API (MFC/Qt pattern: component owns its methods)
2085
- this._methods = {};
2086
- if (o.methods) {
2087
- var self = this;
2088
- for (var k3 in o.methods) {
2089
- if (_hop.call(o.methods, k3)) {
2090
- this._methods[k3] = o.methods[k3];
2091
- (function(methodName, methodFn) {
2092
- self[methodName] = function() {
2093
- var args = [self].concat(Array.prototype.slice.call(arguments));
2094
- return methodFn.apply(null, args);
2095
- };
2096
- })(k3, o.methods[k3]);
2097
- }
2098
- }
2099
- }
2100
- // User tag for addressing via bw.message()
2101
- this._userTag = null;
2102
- // Lifecycle hooks
2103
- this._hooks = {
2104
- willMount: o.willMount || null,
2105
- mounted: o.mounted || null,
2106
- willUpdate: o.willUpdate || null,
2107
- onUpdate: o.onUpdate || o.updated || null,
2108
- unmount: o.unmount || null,
2109
- willDestroy: o.willDestroy || null
2110
- };
2111
- // Binding tracking
2112
- this._bindings = [];
2113
- this._dirtyKeys = {};
2114
- this._scheduled = false;
2115
- this._subs = [];
2116
- this._eventListeners = [];
2117
- this._registeredActions = [];
2118
- this._prevValues = {};
2119
- this._compile = !!o.compile;
2120
- this._bw_refs = {};
2121
- this._refCounter = 0;
2122
- // Child component ownership (Bug #5)
2123
- this._children = [];
2124
- this._parent = null;
2125
- // Factory metadata for BCCL rebuild (Bug #6)
2126
- this._factory = taco._bwFactory || null;
2127
- }
2128
-
2129
- // Short alias for ComponentHandle.prototype (see alias block at top of file).
2130
- // 28 method definitions × 25 chars = ~700B raw savings in minified output.
2131
- var _chp = ComponentHandle.prototype;
2132
-
2133
- // ── State Methods ──
2134
-
2135
- /**
2136
- * Get a state value. Dot-path supported: `get('user.name')`
2137
- */
2138
- _chp.get = function(key) {
2139
- return bw._evaluatePath(this._state, key);
2140
- };
2141
-
2142
- /**
2143
- * Set a state value. Dot-path supported. Schedules re-render.
2144
- * @param {string} key - State key (dot-path)
2145
- * @param {*} value - New value
2146
- * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
2147
- */
2148
- _chp.set = function(key, value, opts) {
2149
- // Dot-path set
2150
- var parts = key.split('.');
2151
- var obj = this._state;
2152
- for (var i = 0; i < parts.length - 1; i++) {
2153
- if (!_is(obj[parts[i]], 'object')) {
2154
- if (bw.debug) _cw('bw.debug: set() — auto-creating intermediate "' + parts[i] + '" in path "' + key + '"');
2155
- obj[parts[i]] = {};
2156
- }
2157
- obj = obj[parts[i]];
2158
- }
2159
- obj[parts[parts.length - 1]] = value;
2160
- // Mark top-level key dirty
2161
- this._dirtyKeys[parts[0]] = true;
2162
- if (this.mounted) {
2163
- if (opts && opts.sync) {
2164
- this._flush();
2165
- } else {
2166
- this._scheduleDirty();
2167
- }
2168
- }
2169
- };
2170
-
2171
- /**
2172
- * Get a shallow clone of the full state.
2173
- */
2174
- _chp.getState = function() {
2175
- var clone = {};
2176
- for (var k in this._state) {
2177
- if (_hop.call(this._state, k)) {
2178
- clone[k] = this._state[k];
2179
- }
2180
- }
2181
- return clone;
2182
- };
2183
-
2184
- /**
2185
- * Merge multiple state keys. Schedules re-render.
2186
- * @param {Object} updates - Key-value pairs to merge
2187
- * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
2188
- */
2189
- _chp.setState = function(updates, opts) {
2190
- for (var k in updates) {
2191
- if (_hop.call(updates, k)) {
2192
- this._state[k] = updates[k];
2193
- this._dirtyKeys[k] = true;
2194
- }
2195
- }
2196
- if (this.mounted) {
2197
- if (opts && opts.sync) {
2198
- this._flush();
2199
- } else {
2200
- this._scheduleDirty();
2201
- }
2202
- }
2203
- };
2204
-
2205
- /**
2206
- * Push a value onto an array in state. Clones the array.
2207
- */
2208
- _chp.push = function(key, val) {
2209
- var arr = this.get(key);
2210
- var newArr = _isA(arr) ? arr.slice() : [];
2211
- newArr.push(val);
2212
- this.set(key, newArr);
2213
- };
2214
-
2215
- /**
2216
- * Splice an array in state. Clones the array.
2217
- */
2218
- _chp.splice = function(key, start, deleteCount) {
2219
- var arr = this.get(key);
2220
- var newArr = _isA(arr) ? arr.slice() : [];
2221
- var args = [start, deleteCount].concat(Array.prototype.slice.call(arguments, 3));
2222
- Array.prototype.splice.apply(newArr, args);
2223
- this.set(key, newArr);
2224
- };
2225
-
2226
- // ── Scheduling ──
2227
-
2228
- _chp._scheduleDirty = function() {
2229
- if (!this._scheduled) {
2230
- this._scheduled = true;
2231
- bw._dirtyComponents.push(this);
2232
- bw._scheduleFlush();
2233
- }
2234
- };
2235
-
2236
- // ── Binding Compilation ──
2237
-
2238
- /**
2239
- * Walk the TACO tree and extract ${expr} bindings.
2240
- * Creates binding descriptors with refIds for targeted DOM updates.
2241
- * @private
2242
- */
2243
- _chp._compileBindings = function() {
2244
- this._bindings = [];
2245
- this._refCounter = 0;
2246
- var stateKeys = _keys(this._state);
2247
- var self = this;
2248
-
2249
- function walkTaco(taco, path) {
2250
- if (!_is(taco, 'object') || !taco.t) return taco;
2251
-
2252
- // Check content for bindings
2253
- if (_is(taco.c, 'string') && taco.c.indexOf('${') >= 0) {
2254
- var refId = 'bw_ref_' + self._refCounter++;
2255
- var parsed = bw._parseBindings(taco.c);
2256
- var deps = [];
2257
- for (var j = 0; j < parsed.length; j++) {
2258
- deps = deps.concat(bw._extractDeps(parsed[j].expr, stateKeys));
2259
- }
2260
- self._bindings.push({
2261
- expr: taco.c,
2262
- type: 'content',
2263
- refId: refId,
2264
- deps: deps,
2265
- template: taco.c
2266
- });
2267
- // Inject data-bw_ref on the TACO for createDOM to pick up
2268
- if (!taco.a) taco.a = {};
2269
- taco.a['data-bw_ref'] = refId;
2270
- }
2271
-
2272
- // Check attributes for bindings
2273
- if (taco.a) {
2274
- for (var attrName in taco.a) {
2275
- if (!_hop.call(taco.a, attrName)) continue;
2276
- if (attrName === 'data-bw_ref') continue;
2277
- var attrVal = taco.a[attrName];
2278
- if (_is(attrVal, 'string') && attrVal.indexOf('${') >= 0) {
2279
- var refId2 = 'bw_ref_' + self._refCounter++;
2280
- var parsed2 = bw._parseBindings(attrVal);
2281
- var deps2 = [];
2282
- for (var j2 = 0; j2 < parsed2.length; j2++) {
2283
- deps2 = deps2.concat(bw._extractDeps(parsed2[j2].expr, stateKeys));
2284
- }
2285
- self._bindings.push({
2286
- expr: attrVal,
2287
- type: 'attribute',
2288
- attrName: attrName,
2289
- refId: refId2,
2290
- deps: deps2,
2291
- template: attrVal
2292
- });
2293
- if (!taco.a) taco.a = {};
2294
- taco.a['data-bw_ref'] = taco.a['data-bw_ref'] || refId2;
2295
- // If multiple attribute bindings on same element, store additional marker
2296
- if (taco.a['data-bw_ref'] !== refId2) {
2297
- taco.a['data-bw_ref_' + attrName] = refId2;
2298
- }
2299
- }
2300
- }
2301
- }
2302
-
2303
- // Recurse into children
2304
- if (_isA(taco.c)) {
2305
- for (var i = 0; i < taco.c.length; i++) {
2306
- // Wrap string children with ${expr} in a span so patches target the span, not the parent
2307
- if (_is(taco.c[i], 'string') && taco.c[i].indexOf('${') >= 0) {
2308
- var mixedRefId = 'bw_ref_' + self._refCounter++;
2309
- var mixedParsed = bw._parseBindings(taco.c[i]);
2310
- var mixedDeps = [];
2311
- for (var mi = 0; mi < mixedParsed.length; mi++) {
2312
- mixedDeps = mixedDeps.concat(bw._extractDeps(mixedParsed[mi].expr, stateKeys));
2313
- }
2314
- self._bindings.push({
2315
- expr: taco.c[i],
2316
- type: 'content',
2317
- refId: mixedRefId,
2318
- deps: mixedDeps,
2319
- template: taco.c[i]
2320
- });
2321
- // Replace string with a span wrapper so textContent targets the span only
2322
- taco.c[i] = { t: 'span', a: { 'data-bw_ref': mixedRefId, style: 'display:contents' }, c: taco.c[i] };
2323
- }
2324
- if (_is(taco.c[i], 'object') && taco.c[i].t) {
2325
- walkTaco(taco.c[i], path.concat(i));
2326
- }
2327
- // Handle bw.when/bw.each markers
2328
- if (taco.c[i] && taco.c[i]._bwWhen) {
2329
- var whenRefId = 'bw_ref_' + self._refCounter++;
2330
- var whenDeps = bw._extractDeps(taco.c[i].expr.replace(/^\$\{|\}$/g, ''), stateKeys);
2331
- self._bindings.push({
2332
- expr: taco.c[i].expr,
2333
- type: 'structural',
2334
- subtype: 'when',
2335
- refId: whenRefId,
2336
- deps: whenDeps,
2337
- branches: taco.c[i].branches,
2338
- index: i,
2339
- parentPath: path
2340
- });
2341
- taco.c[i]._refId = whenRefId;
2342
- }
2343
- if (taco.c[i] && taco.c[i]._bwEach) {
2344
- var eachRefId = 'bw_ref_' + self._refCounter++;
2345
- var eachDeps = bw._extractDeps(taco.c[i].expr.replace(/^\$\{|\}$/g, ''), stateKeys);
2346
- self._bindings.push({
2347
- expr: taco.c[i].expr,
2348
- type: 'structural',
2349
- subtype: 'each',
2350
- refId: eachRefId,
2351
- deps: eachDeps,
2352
- factory: taco.c[i].factory,
2353
- index: i,
2354
- parentPath: path
2355
- });
2356
- taco.c[i]._refId = eachRefId;
2357
- }
2358
- }
2359
- } else if (_is(taco.c, 'object') && taco.c.t) {
2360
- walkTaco(taco.c, path.concat(0));
2361
- }
2362
-
2363
- return taco;
2364
- }
2365
-
2366
- walkTaco(this.taco, []);
2367
- };
2368
-
2369
- // ── DOM Reference Collection ──
2370
-
2371
- /**
2372
- * Build ref map from the live DOM after createDOM.
2373
- * @private
2374
- */
2375
- _chp._collectRefs = function() {
2376
- this._bw_refs = {};
2377
- if (!this.element) return;
2378
- var els = this.element.querySelectorAll('[data-bw_ref]');
2379
- for (var i = 0; i < els.length; i++) {
2380
- this._bw_refs[els[i].getAttribute('data-bw_ref')] = els[i];
2381
- }
2382
- // Also check root element
2383
- var rootRef = this.element.getAttribute && this.element.getAttribute('data-bw_ref');
2384
- if (rootRef) {
2385
- this._bw_refs[rootRef] = this.element;
2386
- }
2387
- };
2388
-
2389
- // ── Lifecycle ──
2390
-
2391
- /**
2392
- * Mount the component into a parent DOM element.
2393
- * Creates DOM, compiles bindings, registers actions, and calls lifecycle hooks.
2394
- * @param {Element} parentEl - DOM element to mount into
2395
- */
2396
- _chp.mount = function(parentEl) {
2397
- // willMount hook
2398
- if (this._hooks.willMount) this._hooks.willMount(this);
2399
-
2400
- // Save original TACO for re-renders (structural changes clone from this)
2401
- if (!this._originalTaco) {
2402
- this._originalTaco = this.taco;
2403
- }
2404
-
2405
- // Deep-clone TACO so binding annotations don't mutate original.
2406
- // Custom clone to preserve _bwWhen/_bwEach markers and their factory functions.
2407
- this.taco = this._deepCloneTaco(this._originalTaco);
2408
-
2409
- // Compile bindings (annotates TACO with data-bw_ref attributes)
2410
- this._compileBindings();
2411
-
2412
- // Prepare TACO: resolve initial binding values, evaluate when/each
2413
- this._prepareTaco(this.taco);
2414
-
2415
- // Register named actions in function registry
2416
- var self = this;
2417
- for (var actionName in this._actions) {
2418
- if (_hop.call(this._actions, actionName)) {
2419
- var registeredName = this._bwId + '_' + actionName;
2420
- (function(aName) {
2421
- bw.funcRegister(function(evt) {
2422
- self._actions[aName](self, evt);
2423
- }, registeredName);
2424
- })(actionName);
2425
- this._registeredActions.push(registeredName);
2426
- }
2427
- }
2428
-
2429
- // Wire action names in onclick etc. to dispatch strings
2430
- this._wireActions(this.taco);
2431
-
2432
- // Create DOM (strip o before createDOM to prevent double lifecycle)
2433
- var tacoForDOM = this._tacoForDOM(this.taco);
2434
- this.element = bw.createDOM(tacoForDOM);
2435
- this.element._bwComponentHandle = this;
2436
- this.element.setAttribute('data-bw_comp_id', this._bwId);
2437
-
2438
- // Restore o.render from original TACO (stripped by _tacoForDOM)
2439
- if (this.taco.o && this.taco.o.render) {
2440
- this.element._bw_render = this.taco.o.render;
2441
- }
2442
- if (this._userTag) {
2443
- this.element.classList.add(this._userTag);
2444
- }
2445
1795
 
2446
- // Append to parent
2447
- parentEl.appendChild(this.element);
1796
+ bw.when = function() { throw new Error('bw.when() removed in v2.0.19. Use conditional logic in o.render instead.'); };
1797
+ bw.each = function() { throw new Error('bw.each() removed in v2.0.19. Use array mapping in o.render instead.'); };
1798
+ bw.component = function() { throw new Error('bw.component() removed in v2.0.19. Use o.handle/o.slots on TACO options instead.'); };
2448
1799
 
2449
- // Collect refs from live DOM
2450
- this._collectRefs();
2451
-
2452
- // Resolve initial bindings and apply to DOM
2453
- this._resolveAndApplyAll();
2454
-
2455
- this.mounted = true;
2456
-
2457
- // Scan for child ComponentHandles and link parent/child (Bug #5)
2458
- var childEls = this.element.querySelectorAll('[data-bw_comp_id]');
2459
- for (var ci = 0; ci < childEls.length; ci++) {
2460
- var ch = childEls[ci]._bwComponentHandle;
2461
- if (ch && ch !== this && !ch._parent) {
2462
- ch._parent = this;
2463
- this._children.push(ch);
2464
- }
2465
- }
2466
-
2467
- // mounted hook (backward compat: fn.length === 2 wraps (el, state))
2468
- if (this._hooks.mounted) {
2469
- if (this._hooks.mounted.length === 2) {
2470
- this._hooks.mounted(this.element, this.getState());
2471
- } else {
2472
- this._hooks.mounted(this);
2473
- }
2474
- }
2475
-
2476
- // Invoke o.render on initial mount (if present)
2477
- if (this.element._bw_render) {
2478
- this.element._bw_render(this.element, this._state);
2479
- }
2480
- };
2481
-
2482
- /**
2483
- * Prepare TACO for initial render: resolve when/each markers.
2484
- * @private
2485
- */
2486
- _chp._prepareTaco = function(taco) {
2487
- if (!_is(taco, 'object')) return;
2488
-
2489
- if (_isA(taco.c)) {
2490
- for (var i = taco.c.length - 1; i >= 0; i--) {
2491
- var child = taco.c[i];
2492
- if (child && child._bwWhen) {
2493
- var exprStr = child.expr.replace(/^\$\{|\}$/g, '');
2494
- var val;
2495
- if (this._compile) {
2496
- try {
2497
- val = (new Function('state', 'with(state){return (' + exprStr + ');}'))(this._state);
2498
- } catch(e) { val = false; }
2499
- } else {
2500
- val = bw._evaluatePath(this._state, exprStr);
2501
- }
2502
- var branch = val ? child.branches[0] : (child.branches[1] || null);
2503
- if (branch) {
2504
- // Wrap in a container so we can track it
2505
- taco.c[i] = { t: 'span', a: { 'data-bw_when': child._refId, style: 'display:contents' }, c: branch };
2506
- } else {
2507
- taco.c[i] = { t: 'span', a: { 'data-bw_when': child._refId, style: 'display:contents' }, c: '' };
2508
- }
2509
- }
2510
- if (child && child._bwEach) {
2511
- var eachExprStr = child.expr.replace(/^\$\{|\}$/g, '');
2512
- var arr = bw._evaluatePath(this._state, eachExprStr);
2513
- var items = [];
2514
- if (_isA(arr)) {
2515
- for (var j = 0; j < arr.length; j++) {
2516
- items.push(child.factory(arr[j], j));
2517
- }
2518
- }
2519
- taco.c[i] = { t: 'span', a: { 'data-bw_each': child._refId, style: 'display:contents' }, c: items };
2520
- }
2521
- if (_is(taco.c[i], 'object') && taco.c[i].t) {
2522
- this._prepareTaco(taco.c[i]);
2523
- }
2524
- }
2525
- } else if (_is(taco.c, 'object') && taco.c.t) {
2526
- this._prepareTaco(taco.c);
2527
- }
2528
- };
2529
-
2530
- /**
2531
- * Wire action name strings (in onclick etc.) to dispatch function calls.
2532
- * @private
2533
- */
2534
- _chp._wireActions = function(taco) {
2535
- if (!_is(taco, 'object') || !taco.t) return;
2536
- if (taco.a) {
2537
- for (var key in taco.a) {
2538
- if (!_hop.call(taco.a, key)) continue;
2539
- if (key.startsWith('on') && _is(taco.a[key], 'string')) {
2540
- var actionName = taco.a[key];
2541
- if (actionName in this._actions) {
2542
- var registeredName = this._bwId + '_' + actionName;
2543
- // Replace string with actual function for createDOM event binding
2544
- (function(rName) {
2545
- taco.a[key] = function(evt) {
2546
- bw.funcGetById(rName)(evt);
2547
- };
2548
- })(registeredName);
2549
- }
2550
- }
2551
- }
2552
- }
2553
- if (_isA(taco.c)) {
2554
- for (var i = 0; i < taco.c.length; i++) {
2555
- this._wireActions(taco.c[i]);
2556
- }
2557
- } else if (_is(taco.c, 'object') && taco.c.t) {
2558
- this._wireActions(taco.c);
2559
- }
2560
- };
2561
-
2562
- /**
2563
- * Deep-clone a TACO tree, preserving _bwWhen/_bwEach markers and their factories.
2564
- * @private
2565
- */
2566
- _chp._deepCloneTaco = function(taco) {
2567
- if (taco == null) return taco;
2568
- // Preserve _bwWhen / _bwEach markers (contain functions)
2569
- if (taco._bwWhen) {
2570
- return { _bwWhen: true, expr: taco.expr, branches: [
2571
- this._deepCloneTaco(taco.branches[0]),
2572
- taco.branches[1] ? this._deepCloneTaco(taco.branches[1]) : null
2573
- ], _refId: taco._refId };
2574
- }
2575
- if (taco._bwEach) {
2576
- return { _bwEach: true, expr: taco.expr, factory: taco.factory, _refId: taco._refId };
2577
- }
2578
- if (!_is(taco, 'object') || !taco.t) return taco;
2579
- var result = { t: taco.t };
2580
- if (taco.a) {
2581
- result.a = {};
2582
- for (var k in taco.a) {
2583
- if (_hop.call(taco.a, k)) result.a[k] = taco.a[k];
2584
- }
2585
- }
2586
- if (taco.c != null) {
2587
- if (_isA(taco.c)) {
2588
- result.c = taco.c.map(function(child) { return this._deepCloneTaco(child); }.bind(this));
2589
- } else if (_is(taco.c, 'object')) {
2590
- result.c = this._deepCloneTaco(taco.c);
2591
- } else {
2592
- result.c = taco.c;
2593
- }
2594
- }
2595
- if (taco.o) result.o = taco.o; // Keep o reference (not deep-cloned; hooks are functions)
2596
- return result;
2597
- };
2598
-
2599
- /**
2600
- * Create a copy of TACO suitable for createDOM (strips o to prevent double lifecycle).
2601
- * @private
2602
- */
2603
- _chp._tacoForDOM = function(taco) {
2604
- if (!_is(taco, 'object') || !taco.t) return taco;
2605
- var result = { t: taco.t };
2606
- if (taco.a) result.a = taco.a;
2607
- if (taco.c != null) {
2608
- if (_isA(taco.c)) {
2609
- result.c = taco.c.map(function(child) { return this._tacoForDOM(child); }.bind(this));
2610
- } else if (_is(taco.c, 'object') && taco.c.t) {
2611
- result.c = this._tacoForDOM(taco.c);
2612
- } else {
2613
- result.c = taco.c;
2614
- }
2615
- }
2616
- // Intentionally strip o (no mounted/unmount/state/render on sub-elements)
2617
- if (taco.o && (taco.o.mounted || taco.o.render || taco.o.unmount)) {
2618
- _cw('bw: _tacoForDOM stripped o.mounted/render/unmount from child <' + taco.t +
2619
- '>. Use onclick attribute or bw.component() for child interactivity.');
2620
- }
2621
- return result;
2622
- };
2623
-
2624
- /**
2625
- * Unmount: remove from DOM, deactivate, preserve state for re-mount.
2626
- */
2627
- _chp.unmount = function() {
2628
- if (!this.mounted) return;
2629
-
2630
- // unmount hook
2631
- if (this._hooks.unmount) {
2632
- this._hooks.unmount(this);
2633
- }
2634
-
2635
- // Remove DOM event listeners
2636
- for (var i = 0; i < this._eventListeners.length; i++) {
2637
- var l = this._eventListeners[i];
2638
- if (this.element) {
2639
- this.element.removeEventListener(l.event, l.handler);
2640
- }
2641
- }
2642
- this._eventListeners = [];
2643
-
2644
- // Unsubscribe pub/sub
2645
- for (var j = 0; j < this._subs.length; j++) {
2646
- this._subs[j]();
2647
- }
2648
- this._subs = [];
2649
-
2650
- // Remove from DOM
2651
- if (this.element && this.element.parentNode) {
2652
- this.element.parentNode.removeChild(this.element);
2653
- }
2654
-
2655
- this.mounted = false;
2656
- // State preserved — can re-mount
2657
- };
2658
-
2659
- /**
2660
- * Destroy: unmount + clear state + unregister actions.
2661
- */
2662
- _chp.destroy = function() {
2663
- // willDestroy hook
2664
- if (this._hooks.willDestroy) {
2665
- this._hooks.willDestroy(this);
2666
- }
2667
-
2668
- // Cascade destroy to children depth-first (Bug #5)
2669
- for (var ci = this._children.length - 1; ci >= 0; ci--) {
2670
- this._children[ci].destroy();
2671
- }
2672
- this._children = [];
2673
- if (this._parent) {
2674
- var idx = this._parent._children.indexOf(this);
2675
- if (idx >= 0) this._parent._children.splice(idx, 1);
2676
- this._parent = null;
2677
- }
2678
-
2679
- this.unmount();
2680
-
2681
- // Unregister actions from function registry
2682
- for (var i = 0; i < this._registeredActions.length; i++) {
2683
- bw.funcUnregister(this._registeredActions[i]);
2684
- }
2685
- this._registeredActions = [];
2686
-
2687
- // Clear state
2688
- this._state = {};
2689
- this._bindings = [];
2690
- this._bw_refs = {};
2691
- this._prevValues = {};
2692
- this._dirtyKeys = {};
2693
- if (this.element) {
2694
- delete this.element._bwComponentHandle;
2695
- this.element = null;
2696
- }
2697
- };
2698
-
2699
- // ── Flush & Binding Resolution ──
2700
-
2701
- /**
2702
- * Flush dirty state: resolve changed bindings and apply to DOM.
2703
- * @private
2704
- */
2705
- _chp._flush = function() {
2706
- this._scheduled = false;
2707
- var changedKeys = _keys(this._dirtyKeys);
2708
- this._dirtyKeys = {};
2709
- if (changedKeys.length === 0 || !this.mounted) return;
2710
-
2711
- // Factory rebuild: if a BCCL factory exists and changed keys overlap factory props,
2712
- // rebuild the TACO from the factory with merged state (Bug #6)
2713
- if (this._factory) {
2714
- var rebuildNeeded = false;
2715
- for (var fi = 0; fi < changedKeys.length; fi++) {
2716
- if (_hop.call(this._factory.props, changedKeys[fi])) {
2717
- rebuildNeeded = true; break;
2718
- }
2719
- }
2720
- if (rebuildNeeded) {
2721
- var merged = {};
2722
- for (var mk in this._factory.props) if (_hop.call(this._factory.props, mk)) merged[mk] = this._factory.props[mk];
2723
- for (var sk in this._state) if (_hop.call(this._state, sk)) merged[sk] = this._state[sk];
2724
- this._factory.props = merged;
2725
- var newTaco = bw.make(this._factory.type, merged);
2726
- newTaco._bwFactory = this._factory;
2727
- this.taco = newTaco;
2728
- this._originalTaco = this._deepCloneTaco(newTaco);
2729
- this._render();
2730
- if (this._hooks.onUpdate) this._hooks.onUpdate(this, changedKeys);
2731
- return;
2732
- }
2733
- }
2734
-
2735
- // willUpdate hook
2736
- if (this._hooks.willUpdate) {
2737
- this._hooks.willUpdate(this, changedKeys);
2738
- }
2739
-
2740
- // Check if any structural bindings are affected
2741
- var needsFullRender = false;
2742
- for (var i = 0; i < this._bindings.length; i++) {
2743
- var b = this._bindings[i];
2744
- if (b.type === 'structural') {
2745
- for (var j = 0; j < b.deps.length; j++) {
2746
- if (changedKeys.indexOf(b.deps[j]) >= 0) {
2747
- needsFullRender = true;
2748
- break;
2749
- }
2750
- }
2751
- if (needsFullRender) break;
2752
- }
2753
- }
2754
-
2755
- if (needsFullRender) {
2756
- this._render();
2757
- } else {
2758
- var patches = this._resolveBindings(changedKeys);
2759
- this._applyPatches(patches);
2760
- }
2761
-
2762
- // onUpdate hook
2763
- if (this._hooks.onUpdate) {
2764
- this._hooks.onUpdate(this, changedKeys);
2765
- }
2766
- };
2767
-
2768
- /**
2769
- * Resolve bindings whose deps intersect with changedKeys.
2770
- * Returns list of patches to apply.
2771
- * @private
2772
- */
2773
- _chp._resolveBindings = function(changedKeys) {
2774
- var patches = [];
2775
- for (var i = 0; i < this._bindings.length; i++) {
2776
- var b = this._bindings[i];
2777
- if (b.type === 'structural') continue;
2778
-
2779
- // Check if any dep matches
2780
- var affected = false;
2781
- for (var j = 0; j < b.deps.length; j++) {
2782
- if (changedKeys.indexOf(b.deps[j]) >= 0) {
2783
- affected = true;
2784
- break;
2785
- }
2786
- }
2787
- if (!affected) continue;
2788
-
2789
- // Evaluate
2790
- var newVal = bw._resolveTemplate(b.template, this._state, this._compile);
2791
- var prevKey = b.refId + '_' + (b.attrName || 'content');
2792
- if (this._prevValues[prevKey] !== newVal) {
2793
- this._prevValues[prevKey] = newVal;
2794
- patches.push({
2795
- refId: b.refId,
2796
- type: b.type,
2797
- attrName: b.attrName,
2798
- value: newVal
2799
- });
2800
- }
2801
- }
2802
- return patches;
2803
- };
2804
-
2805
- /**
2806
- * Apply patches to DOM.
2807
- * @private
2808
- */
2809
- _chp._applyPatches = function(patches) {
2810
- for (var i = 0; i < patches.length; i++) {
2811
- var p = patches[i];
2812
- var el = this._bw_refs[p.refId];
2813
- if (!el) {
2814
- if (bw.debug) _cw('bw.debug: _applyPatches — ref "' + p.refId + '" not found in DOM');
2815
- continue;
2816
- }
2817
- if (p.type === 'content') {
2818
- el.textContent = p.value;
2819
- } else if (p.type === 'attribute') {
2820
- if (p.attrName === 'class') {
2821
- el.className = p.value;
2822
- } else {
2823
- el.setAttribute(p.attrName, p.value);
2824
- }
2825
- }
2826
- }
2827
- };
2828
-
2829
- /**
2830
- * Resolve all bindings and apply (used for initial render).
2831
- * @private
2832
- */
2833
- _chp._resolveAndApplyAll = function() {
2834
- var patches = [];
2835
- for (var i = 0; i < this._bindings.length; i++) {
2836
- var b = this._bindings[i];
2837
- if (b.type === 'structural') continue;
2838
-
2839
- var newVal = bw._resolveTemplate(b.template, this._state, this._compile);
2840
- var prevKey = b.refId + '_' + (b.attrName || 'content');
2841
- this._prevValues[prevKey] = newVal;
2842
- patches.push({
2843
- refId: b.refId,
2844
- type: b.type,
2845
- attrName: b.attrName,
2846
- value: newVal
2847
- });
2848
- }
2849
- this._applyPatches(patches);
2850
- };
2851
-
2852
- /**
2853
- * Full re-render for structural changes (when/each branch switches).
2854
- * @private
2855
- */
2856
- _chp._render = function() {
2857
- if (!this.element || !this.element.parentNode) return;
2858
- var parent = this.element.parentNode;
2859
- var nextSibling = this.element.nextSibling;
2860
-
2861
- // Remove old DOM
2862
- parent.removeChild(this.element);
2863
-
2864
- // Re-prepare TACO with current state (deep clone preserving functions)
2865
- this.taco = this._deepCloneTaco(this._originalTaco || this.taco);
2866
-
2867
- // Re-compile bindings and prepare
2868
- this._compileBindings();
2869
- this._prepareTaco(this.taco);
2870
- this._wireActions(this.taco);
2871
-
2872
- var tacoForDOM = this._tacoForDOM(this.taco);
2873
- this.element = bw.createDOM(tacoForDOM);
2874
- this.element._bwComponentHandle = this;
2875
- this.element.setAttribute('data-bw_comp_id', this._bwId);
2876
-
2877
- // Re-insert at same position
2878
- if (nextSibling) {
2879
- parent.insertBefore(this.element, nextSibling);
2880
- } else {
2881
- parent.appendChild(this.element);
2882
- }
2883
-
2884
- // Re-collect refs and apply all bindings
2885
- this._collectRefs();
2886
- this._resolveAndApplyAll();
2887
- };
2888
-
2889
- // ── Event & Pub/Sub Methods ──
2890
-
2891
- /**
2892
- * Add a DOM event listener on the component's root element.
2893
- * @param {string} event - Event name (e.g., 'click')
2894
- * @param {Function} handler - Event handler
2895
- */
2896
- _chp.on = function(event, handler) {
2897
- if (this.element) {
2898
- this.element.addEventListener(event, handler);
2899
- }
2900
- this._eventListeners.push({ event: event, handler: handler });
2901
- };
2902
-
2903
- /**
2904
- * Remove a DOM event listener.
2905
- * @param {string} event - Event name
2906
- * @param {Function} handler - Handler to remove
2907
- */
2908
- _chp.off = function(event, handler) {
2909
- if (this.element) {
2910
- this.element.removeEventListener(event, handler);
2911
- }
2912
- this._eventListeners = this._eventListeners.filter(function(l) {
2913
- return !(l.event === event && l.handler === handler);
2914
- });
2915
- };
2916
-
2917
- /**
2918
- * Subscribe to a pub/sub topic. Lifecycle-tied: auto-unsubs on destroy.
2919
- * @param {string} topic - Topic name
2920
- * @param {Function} handler - Handler function
2921
- * @returns {Function} Unsubscribe function
2922
- */
2923
- _chp.sub = function(topic, handler) {
2924
- var unsub = bw.sub(topic, handler);
2925
- this._subs.push(unsub);
2926
- return unsub;
2927
- };
2928
-
2929
- /**
2930
- * Call a named action.
2931
- * @param {string} name - Action name
2932
- * @param {...*} args - Arguments passed after comp
2933
- */
2934
- _chp.action = function(name) {
2935
- var fn = this._actions[name];
2936
- if (!fn) {
2937
- _cw('ComponentHandle.action: unknown action "' + name + '"');
2938
- return;
2939
- }
2940
- var args = [this].concat(Array.prototype.slice.call(arguments, 1));
2941
- return fn.apply(null, args);
2942
- };
2943
-
2944
- /**
2945
- * querySelector within the component's DOM.
2946
- * @param {string} sel - CSS selector
2947
- * @returns {Element|null}
2948
- */
2949
- _chp.select = function(sel) {
2950
- return this.element ? this.element.querySelector(sel) : null;
2951
- };
2952
-
2953
- /**
2954
- * querySelectorAll within the component's DOM.
2955
- * @param {string} sel - CSS selector
2956
- * @returns {Element[]}
2957
- */
2958
- _chp.selectAll = function(sel) {
2959
- if (!this.element) return [];
2960
- return Array.prototype.slice.call(this.element.querySelectorAll(sel));
2961
- };
2962
-
2963
- /**
2964
- * Tag this component with a user-defined ID for addressing via bw.message().
2965
- * The tag is added as a CSS class on the root element (DOM IS the registry).
2966
- * @param {string} tag - User-defined identifier (e.g. 'dashboard_prod_east')
2967
- * @returns {ComponentHandle} this (for chaining)
2968
- */
2969
- _chp.userTag = function(tag) {
2970
- this._userTag = tag;
2971
- if (this.element) {
2972
- this.element.classList.add(tag);
2973
- }
2974
- return this;
2975
- };
2976
-
2977
- // Expose ComponentHandle on bw (for testing and advanced use)
2978
- bw._ComponentHandle = ComponentHandle;
2979
-
2980
- // ===================================================================================
2981
- // Control Flow Helpers
2982
- // ===================================================================================
2983
-
2984
- /**
2985
- * Conditional rendering helper.
2986
- * Returns a marker object that ComponentHandle detects during binding compilation.
2987
- * In static contexts (bw.html with state), evaluates immediately.
2988
- *
2989
- * @param {string} expr - Expression string like '${loggedIn}'
2990
- * @param {Object} tacoTrue - TACO to render when truthy
2991
- * @param {Object} [tacoFalse] - TACO to render when falsy
2992
- * @returns {Object} Marker object with _bwWhen flag
2993
- * @category Component
2994
- */
2995
- bw.when = function(expr, tacoTrue, tacoFalse) {
2996
- return { _bwWhen: true, expr: expr, branches: [tacoTrue, tacoFalse || null] };
2997
- };
2998
-
2999
- /**
3000
- * List rendering helper.
3001
- * Returns a marker object that ComponentHandle detects during binding compilation.
3002
- *
3003
- * @param {string} expr - Expression string like '${items}'
3004
- * @param {Function} fn - Factory function(item, index) returning TACO
3005
- * @returns {Object} Marker object with _bwEach flag
3006
- * @category Component
3007
- */
3008
- bw.each = function(expr, fn) {
3009
- return { _bwEach: true, expr: expr, factory: fn };
3010
- };
3011
-
3012
- // ===================================================================================
3013
- // bw.component() — Factory for ComponentHandle
3014
- // ===================================================================================
3015
-
3016
- /**
3017
- * Create a ComponentHandle from a TACO definition.
3018
- * The returned handle has .get(), .set(), .mount(), .destroy(), etc.
3019
- *
3020
- * @param {Object} taco - TACO definition with {t, a, c, o}
3021
- * @returns {ComponentHandle} Reactive component handle
3022
- * @category Component
3023
- * @see bw.DOM
3024
- * @example
3025
- * var counter = bw.component({
3026
- * t: 'div', c: [{ t: 'h3', c: 'Count: ${count}' }],
3027
- * o: { state: { count: 0 } }
3028
- * });
3029
- * bw.DOM('#app', counter);
3030
- * counter.set('count', 42); // DOM auto-updates
3031
- */
3032
- bw.component = function(taco) {
3033
- return new ComponentHandle(taco);
3034
- };
3035
1800
 
3036
1801
  // ===================================================================================
3037
1802
  // bw.message() — SendMessage() for the web
3038
1803
  // ===================================================================================
3039
1804
 
3040
1805
  /**
3041
- * Dispatch a message to a component by UUID or user tag.
3042
- * Finds the component's DOM element, looks up its ComponentHandle,
3043
- * and calls the named method. This is the bitwrench equivalent of
3044
- * Win32 SendMessage(hwnd, msg, wParam, lParam).
1806
+ * Dispatch a message to a component by UUID, CSS class, or selector.
1807
+ * Finds the element, looks up el.bw, and calls the named method.
1808
+ * This is the bitwrench equivalent of Win32 SendMessage(hwnd, msg, wParam, lParam).
3045
1809
  *
3046
- * @param {string} target - Component UUID (bw_uuid_*), comp ID (data-bw_comp_id), or user tag (CSS class)
3047
- * @param {string} action - Method name to call on the component
1810
+ * @param {string} target - Component UUID (bw_uuid_*), CSS class, or selector
1811
+ * @param {string} action - Method name to call on el.bw
3048
1812
  * @param {*} data - Data to pass to the method
3049
1813
  * @returns {boolean} True if message was dispatched successfully
3050
1814
  * @category Component
3051
1815
  * @example
3052
- * // Tag a component
3053
- * myDash.userTag('dashboard_prod');
3054
- * // Dispatch locally
3055
- * bw.message('dashboard_prod', 'addAlert', { severity: 'warning', text: 'CPU spike' });
1816
+ * bw.message('my_carousel', 'goToSlide', 2);
3056
1817
  * // Or from SSE handler:
3057
1818
  * es.onmessage = function(e) {
3058
1819
  * var msg = JSON.parse(e.data);
@@ -3060,23 +1821,13 @@ bw.component = function(taco) {
3060
1821
  * };
3061
1822
  */
3062
1823
  bw.message = function(target, action, data) {
3063
- // Try bw._el() first (handles UUID class, nodeMap cache, getElementById)
3064
1824
  var el = bw._el(target);
3065
- // Then try data-bw_comp_id attribute
3066
- if (!el || !el._bwComponentHandle) {
3067
- el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
3068
- }
3069
- // Then try CSS class (user tag)
3070
- if (!el || !el._bwComponentHandle) {
3071
- el = bw.$('.' + target)[0];
3072
- }
3073
- if (!el || !el._bwComponentHandle) return false;
3074
- var comp = el._bwComponentHandle;
3075
- if (!_is(comp[action], 'function')) {
3076
- _cw('bw.message: unknown action "' + action + '" on component ' + target);
1825
+ if (!el) el = bw.$('.' + target)[0];
1826
+ if (!el || !el.bw || typeof el.bw[action] !== 'function') {
1827
+ _cw('bw.message: no handle method "' + action + '" on ' + target);
3077
1828
  return false;
3078
1829
  }
3079
- comp[action](data);
1830
+ el.bw[action](data);
3080
1831
  return true;
3081
1832
  };
3082
1833
 
@@ -3302,132 +2053,29 @@ bw.apply = function(msg) {
3302
2053
  // ===================================================================================
3303
2054
 
3304
2055
  /**
3305
- * Inspect a component's state, bindings, methods, and metadata.
3306
- * Works with DOM elements, CSS selectors, or ComponentHandle objects.
3307
- * Returns the ComponentHandle for console chaining.
2056
+ * Inspect a DOM element's bitwrench state, handle methods, and metadata.
2057
+ * Works with DOM elements or CSS selectors.
3308
2058
  *
3309
- * @param {string|Element|ComponentHandle} target - Selector, element, or handle
3310
- * @returns {ComponentHandle|null} The component handle, or null if not found
2059
+ * @param {string|Element} target - Selector or DOM element
2060
+ * @returns {Element|null} The element, or null if not found
3311
2061
  * @category Component
3312
2062
  * @example
3313
- * // In browser console, click element in Elements panel then:
2063
+ * bw.inspect('#my-carousel');
3314
2064
  * bw.inspect($0);
3315
- * // Or by selector:
3316
- * var h = bw.inspect('#my-dashboard');
3317
- * h.set('count', 99); // chain from returned handle
3318
2065
  */
3319
2066
  bw.inspect = function(target) {
3320
- var el = target;
3321
- var comp;
3322
- if (target && target._bwComponent === true) {
3323
- el = target.element;
3324
- comp = target;
3325
- } else {
3326
- if (_is(target, 'string')) {
3327
- el = bw.$(target)[0];
3328
- }
3329
- if (!el) {
3330
- _cw('bw.inspect: element not found');
3331
- return null;
3332
- }
3333
- comp = el._bwComponentHandle;
3334
- }
3335
- if (!comp) {
3336
- _cl('bw.inspect: no ComponentHandle on this element');
3337
- _cl(' Tag:', el.tagName);
3338
- _cl(' Classes:', el.className);
3339
- _cl(' _bw_state:', el._bw_state || '(none)');
3340
- return null;
3341
- }
3342
- var deps = comp._bindings.reduce(function(s, b) {
3343
- return s.concat(b.deps || []);
3344
- }, []).filter(function(v, i, a) { return a.indexOf(v) === i; });
3345
- console.group('Component: ' + comp._bwId);
3346
- _cl('State:', comp._state);
3347
- _cl('Bindings:', comp._bindings.length, '(deps:', deps, ')');
3348
- _cl('Methods:', _keys(comp._methods));
3349
- _cl('Actions:', _keys(comp._actions));
3350
- _cl('User tag:', comp._userTag || '(none)');
3351
- _cl('Mounted:', comp.mounted);
3352
- _cl('Element:', comp.element);
2067
+ var el = _is(target, 'string') ? bw.$(target)[0] : target;
2068
+ if (!el) { _cw('bw.inspect: element not found'); return null; }
2069
+ console.group('Element: ' + (bw.getUUID(el) || el.id || el.tagName));
2070
+ _cl('State:', el._bw_state || '(none)');
2071
+ _cl('Handle:', el.bw ? _keys(el.bw) : '(none)');
2072
+ _cl('Classes:', el.className);
2073
+ _cl('Refs:', el._bw_refs || '(none)');
3353
2074
  console.groupEnd();
3354
- return comp;
2075
+ return el;
3355
2076
  };
3356
2077
 
3357
- // ===================================================================================
3358
- // bw.compile() — Pre-compile TACO into optimized factory
3359
- // ===================================================================================
3360
-
3361
- /**
3362
- * Pre-compile a TACO definition into a factory function.
3363
- * The factory produces ComponentHandles with pre-compiled binding evaluators.
3364
- *
3365
- * Phase 1: validates API surface. Template cloning optimization deferred.
3366
- *
3367
- * @param {Object} taco - TACO definition
3368
- * @returns {Function} Factory function(initialState?) → ComponentHandle
3369
- * @category Component
3370
- */
3371
- bw.compile = function(taco) {
3372
- // Pre-extract all binding expressions
3373
- var precompiled = [];
3374
- function walkExpressions(node) {
3375
- if (!_is(node, 'object')) return;
3376
- if (_is(node.c, 'string') && node.c.indexOf('${') >= 0) {
3377
- var parsed = bw._parseBindings(node.c);
3378
- for (var i = 0; i < parsed.length; i++) {
3379
- try {
3380
- precompiled.push({
3381
- expr: parsed[i].expr,
3382
- fn: new Function('state', 'with(state){return (' + parsed[i].expr + ');}')
3383
- });
3384
- } catch(e) {
3385
- precompiled.push({ expr: parsed[i].expr, fn: function() { return ''; } });
3386
- }
3387
- }
3388
- }
3389
- if (node.a) {
3390
- for (var key in node.a) {
3391
- if (_hop.call(node.a, key)) {
3392
- var v = node.a[key];
3393
- if (_is(v, 'string') && v.indexOf('${') >= 0) {
3394
- var parsed2 = bw._parseBindings(v);
3395
- for (var j = 0; j < parsed2.length; j++) {
3396
- try {
3397
- precompiled.push({
3398
- expr: parsed2[j].expr,
3399
- fn: new Function('state', 'with(state){return (' + parsed2[j].expr + ');}')
3400
- });
3401
- } catch(e2) {
3402
- precompiled.push({ expr: parsed2[j].expr, fn: function() { return ''; } });
3403
- }
3404
- }
3405
- }
3406
- }
3407
- }
3408
- }
3409
- if (_isA(node.c)) {
3410
- for (var k = 0; k < node.c.length; k++) walkExpressions(node.c[k]);
3411
- } else if (_is(node.c, 'object') && node.c.t) {
3412
- walkExpressions(node.c);
3413
- }
3414
- }
3415
- walkExpressions(taco);
3416
-
3417
- return function(initialState) {
3418
- var handle = new ComponentHandle(taco);
3419
- handle._compile = true;
3420
- handle._precompiledBindings = precompiled;
3421
- if (initialState) {
3422
- for (var k in initialState) {
3423
- if (_hop.call(initialState, k)) {
3424
- handle._state[k] = initialState[k];
3425
- }
3426
- }
3427
- }
3428
- return handle;
3429
- };
3430
- };
2078
+ bw.compile = function() { throw new Error('bw.compile() removed in v2.0.19. Use o.handle/o.slots on TACO options instead.'); };
3431
2079
 
3432
2080
  /**
3433
2081
  * Generate CSS from JavaScript objects.
@@ -4653,8 +3301,8 @@ bw.render = function(element, position, taco) {
4653
3301
  };
4654
3302
  }
4655
3303
 
4656
- // Generate unique ID if not provided
4657
- const componentId = taco.o?.id || bw.uuid();
3304
+ // Generate unique UUID class if not provided
3305
+ const componentId = taco.o?.id || bw.uuid('uuid');
4658
3306
 
4659
3307
  // Create DOM element
4660
3308
  let domElement;
@@ -4669,9 +3317,10 @@ bw.render = function(element, position, taco) {
4669
3317
  };
4670
3318
  }
4671
3319
 
4672
- // Add component ID to element
4673
- domElement.setAttribute('data-bw_id', componentId);
4674
-
3320
+ // Add component ID as class + lifecycle marker
3321
+ domElement.classList.add(componentId);
3322
+ domElement.classList.add(_BW_LC);
3323
+
4675
3324
  // Insert into DOM based on position
4676
3325
  try {
4677
3326
  switch(position) {
@@ -4745,7 +3394,8 @@ bw.render = function(element, position, taco) {
4745
3394
 
4746
3395
  // Re-render
4747
3396
  const newElement = bw.createDOM(this._taco);
4748
- newElement.setAttribute('data-bw_id', componentId);
3397
+ newElement.classList.add(componentId);
3398
+ newElement.classList.add(_BW_LC);
4749
3399
 
4750
3400
  // Replace in DOM
4751
3401
  parent.replaceChild(newElement, this.element);
@@ -4937,13 +3587,12 @@ bw.BCCL = components.BCCL;
4937
3587
  // Variant class helper: bw.variantClass('primary') → 'bw_primary'
4938
3588
  bw.variantClass = components.variantClass;
4939
3589
 
4940
- // Create functions that return handles (plain renderComponent, no Handle overlay)
3590
+ // Create functions that return DOM elements (createCard, createTable, etc.)
4941
3591
  Object.entries(components).forEach(([name, fn]) => {
4942
3592
  if (name.startsWith('make')) {
4943
- const createName = 'create' + name.substring(4); // createCard, createTable, etc.
3593
+ const createName = 'create' + name.substring(4);
4944
3594
  bw[createName] = function(props) {
4945
- const taco = fn(props);
4946
- return bw.renderComponent(taco);
3595
+ return bw.createDOM(fn(props));
4947
3596
  };
4948
3597
  }
4949
3598
  });