@odoo/owl 3.0.0-alpha.36 → 3.0.0-alpha.38

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/dist/owl.cjs.js CHANGED
@@ -823,7 +823,7 @@ function assertType(value, validation, errorMessage = "Value does not match the
823
823
  ${issueStrings}`);
824
824
  }
825
825
  }
826
- function createContext(issues, value, path, parent) {
826
+ function createContext(issues, value, path, parent, depthOffset = 1) {
827
827
  return {
828
828
  issueDepth: 0,
829
829
  path,
@@ -844,11 +844,11 @@ function createContext(issues, value, path, parent) {
844
844
  validate(type) {
845
845
  type(this);
846
846
  if (!this.isValid && parent) {
847
- parent.issueDepth = this.issueDepth + 1;
847
+ parent.issueDepth = this.issueDepth + depthOffset;
848
848
  }
849
849
  },
850
850
  withIssues(issues2) {
851
- return createContext(issues2, this.value, this.path, this);
851
+ return createContext(issues2, this.value, this.path, this, 0);
852
852
  },
853
853
  withKey(key) {
854
854
  return createContext(issues, this.value[key], this.path.concat(key), this);
@@ -1068,8 +1068,7 @@ function validateObject(context, schema, isStrict) {
1068
1068
  if (missingKeys.length) {
1069
1069
  context.addIssue({
1070
1070
  message: "object value has missing keys",
1071
- missingKeys,
1072
- expectedKeys: keys
1071
+ missingKeys
1073
1072
  });
1074
1073
  }
1075
1074
  if (isStrict) {
@@ -1082,8 +1081,7 @@ function validateObject(context, schema, isStrict) {
1082
1081
  if (unknownKeys.length) {
1083
1082
  context.addIssue({
1084
1083
  message: "object value has unknown keys",
1085
- unknownKeys,
1086
- expectedKeys: keys
1084
+ unknownKeys
1087
1085
  });
1088
1086
  }
1089
1087
  }
@@ -1531,7 +1529,7 @@ function markup(valueOrStrings, ...placeholders) {
1531
1529
  }
1532
1530
 
1533
1531
  // ../owl-runtime/dist/owl-runtime.es.js
1534
- var version = "3.0.0-alpha.36";
1532
+ var version = "3.0.0-alpha.38";
1535
1533
  var fibersInError = /* @__PURE__ */ new WeakMap();
1536
1534
  var nodeErrorHandlers = /* @__PURE__ */ new WeakMap();
1537
1535
  function invokeErrorHandlers(node, error, finalize, markFibers) {
@@ -1621,7 +1619,7 @@ var config2 = {
1621
1619
  return false;
1622
1620
  }
1623
1621
  };
1624
- var txt = document.createTextNode("");
1622
+ var txt = globalThis.document?.createTextNode("");
1625
1623
  var VToggler = class {
1626
1624
  key;
1627
1625
  child;
@@ -3427,6 +3425,11 @@ var ComponentNode = class extends Scope {
3427
3425
  willPatch = [];
3428
3426
  patched = [];
3429
3427
  signalComputation;
3428
+ // t-ref signals bound to an element hosted by this component, mapped to their
3429
+ // atom (so the element can be read without subscribing). Swept by isConnected
3430
+ // after each patch and after this subtree is removed, to unset a ref pointing
3431
+ // at a bulk-removed element (slot host, enclosing t-if) — see sweepRefs.
3432
+ trackedRefs = null;
3430
3433
  constructor(C, props2, app, parent, parentKey) {
3431
3434
  super(app);
3432
3435
  this.parent = parent;
@@ -3535,9 +3538,15 @@ var ComponentNode = class extends Scope {
3535
3538
  }
3536
3539
  destroy() {
3537
3540
  let shouldRemove = this.status === STATUS.MOUNTED;
3538
- this._destroy();
3541
+ removalDepth++;
3542
+ try {
3543
+ this._destroy();
3544
+ } finally {
3545
+ removalDepth--;
3546
+ }
3539
3547
  if (shouldRemove) {
3540
3548
  this.bdom.remove();
3549
+ sweepRemovedRefs();
3541
3550
  }
3542
3551
  }
3543
3552
  _destroy() {
@@ -3547,12 +3556,42 @@ var ComponentNode = class extends Scope {
3547
3556
  cb.call(component);
3548
3557
  }
3549
3558
  }
3559
+ if (removalDepth && this.trackedRefs) {
3560
+ (removed ||= []).push(this);
3561
+ }
3550
3562
  for (let childKey in this.children) {
3551
3563
  this.children[childKey]._destroy();
3552
3564
  }
3553
3565
  this.finalize((e) => handleError({ error: e, node: this }));
3554
3566
  disposeComputation(this.signalComputation);
3555
3567
  }
3568
+ /**
3569
+ * Unset any tracked t-ref whose element is no longer in the document, and stop
3570
+ * tracking it (createRef re-registers it on the next render if the element
3571
+ * comes back). `isConnected` is the discriminator: a ref the block's own
3572
+ * remove() failed to clear (bulk removal) points at a detached element and is
3573
+ * cleared, while a ref a surviving sibling just took over (t-if/t-else with a
3574
+ * shared signal) points at a still-connected element and is left alone.
3575
+ *
3576
+ * Called after this component's dom settles: at the tail of `_patch` (before
3577
+ * user `onPatched`), so an element removed in place is caught, and — for the
3578
+ * nodes collected during `_destroy` — after a removed subtree is detached.
3579
+ */
3580
+ sweepRefs() {
3581
+ const refs = this.trackedRefs;
3582
+ if (!refs) {
3583
+ return;
3584
+ }
3585
+ for (const [ref2, atom] of refs) {
3586
+ const el = atom.value;
3587
+ if (!el) {
3588
+ refs.delete(ref2);
3589
+ } else if (!el.isConnected) {
3590
+ ref2.set(null);
3591
+ refs.delete(ref2);
3592
+ }
3593
+ }
3594
+ }
3556
3595
  /**
3557
3596
  * Finds a child that has dom that is not yet updated, and update it. This
3558
3597
  * method is meant to be used only in the context of repatching the dom after
@@ -3568,7 +3607,14 @@ var ComponentNode = class extends Scope {
3568
3607
  child.updateDom();
3569
3608
  }
3570
3609
  } else {
3571
- this.bdom.patch(this.fiber.bdom, false);
3610
+ removalDepth++;
3611
+ try {
3612
+ this.bdom.patch(this.fiber.bdom, false);
3613
+ } finally {
3614
+ removalDepth--;
3615
+ }
3616
+ this.sweepRefs();
3617
+ sweepRemovedRefs();
3572
3618
  this.fiber.appliedToDom = true;
3573
3619
  this.fiber = null;
3574
3620
  }
@@ -3595,6 +3641,14 @@ var ComponentNode = class extends Scope {
3595
3641
  moveBeforeVNode(other, afterNode) {
3596
3642
  this.bdom.moveBeforeVNode(other ? other.bdom : null, afterNode);
3597
3643
  }
3644
+ /**
3645
+ * Register a t-ref signal bound to an element this component hosts, so its
3646
+ * lifecycle can clear it (see sweepRefs / _destroy). Idempotent — re-tracking
3647
+ * the same signal on each render just refreshes its atom.
3648
+ */
3649
+ trackRef(ref2, atom) {
3650
+ (this.trackedRefs ||= /* @__PURE__ */ new Map()).set(ref2, atom);
3651
+ }
3598
3652
  patch() {
3599
3653
  if (this.fiber && this.fiber.parent) {
3600
3654
  this._patch();
@@ -3608,7 +3662,14 @@ var ComponentNode = class extends Scope {
3608
3662
  }
3609
3663
  const fiber = this.fiber;
3610
3664
  this.children = fiber.childrenMap;
3611
- this.bdom.patch(fiber.bdom, hasChildren);
3665
+ removalDepth++;
3666
+ try {
3667
+ this.bdom.patch(fiber.bdom, hasChildren);
3668
+ } finally {
3669
+ removalDepth--;
3670
+ }
3671
+ this.sweepRefs();
3672
+ sweepRemovedRefs();
3612
3673
  fiber.appliedToDom = true;
3613
3674
  this.fiber = null;
3614
3675
  }
@@ -3619,6 +3680,17 @@ var ComponentNode = class extends Scope {
3619
3680
  this.bdom.remove();
3620
3681
  }
3621
3682
  };
3683
+ var removalDepth = 0;
3684
+ var removed = null;
3685
+ function sweepRemovedRefs() {
3686
+ if (removalDepth === 0 && removed) {
3687
+ const nodes = removed;
3688
+ removed = null;
3689
+ for (let i = 0; i < nodes.length; i++) {
3690
+ nodes[i].sweepRefs();
3691
+ }
3692
+ }
3693
+ }
3622
3694
  function getComponentScope() {
3623
3695
  const scope = useScope();
3624
3696
  if (!(scope instanceof ComponentNode)) {
@@ -3821,7 +3893,7 @@ function safeOutput(value, defaultValue) {
3821
3893
  }
3822
3894
  return toggler(safeKey, block);
3823
3895
  }
3824
- function createRef(ref2) {
3896
+ function createRef(ref2, node) {
3825
3897
  if (!ref2) {
3826
3898
  throw new OwlError(`Ref is undefined or null`);
3827
3899
  }
@@ -3836,6 +3908,9 @@ function createRef(ref2) {
3836
3908
  remove2 = atom ? (prevEl) => {
3837
3909
  if (atom.value === prevEl) ref2.set(null);
3838
3910
  } : () => ref2.set(null);
3911
+ if (atom) {
3912
+ node.trackRef(ref2, atom);
3913
+ }
3839
3914
  } else {
3840
3915
  throw new OwlError(
3841
3916
  `Ref should implement either a 'set' function or 'add' and 'delete' functions`
@@ -4587,8 +4662,8 @@ var blockDom = {
4587
4662
  };
4588
4663
  var __info__ = {
4589
4664
  version: App.version,
4590
- date: "2026-06-11T18:30:46.327Z",
4591
- hash: "eb837019",
4665
+ date: "2026-06-22T09:21:02.010Z",
4666
+ hash: "ac221141",
4592
4667
  url: "https://github.com/odoo/owl"
4593
4668
  };
4594
4669
 
@@ -6119,7 +6194,7 @@ ${code}`;
6119
6194
  if (ast.ref) {
6120
6195
  const refExpr = compileExpr(ast.ref);
6121
6196
  this.helpers.add("createRef");
6122
- const setRefStr = `createRef(${refExpr})`;
6197
+ const setRefStr = `createRef(${refExpr}, node)`;
6123
6198
  const idx = block.insertData(setRefStr, "ref");
6124
6199
  attrs["block-ref"] = String(idx);
6125
6200
  }
package/dist/owl.es.js CHANGED
@@ -745,7 +745,7 @@ function assertType(value, validation, errorMessage = "Value does not match the
745
745
  ${issueStrings}`);
746
746
  }
747
747
  }
748
- function createContext(issues, value, path, parent) {
748
+ function createContext(issues, value, path, parent, depthOffset = 1) {
749
749
  return {
750
750
  issueDepth: 0,
751
751
  path,
@@ -766,11 +766,11 @@ function createContext(issues, value, path, parent) {
766
766
  validate(type) {
767
767
  type(this);
768
768
  if (!this.isValid && parent) {
769
- parent.issueDepth = this.issueDepth + 1;
769
+ parent.issueDepth = this.issueDepth + depthOffset;
770
770
  }
771
771
  },
772
772
  withIssues(issues2) {
773
- return createContext(issues2, this.value, this.path, this);
773
+ return createContext(issues2, this.value, this.path, this, 0);
774
774
  },
775
775
  withKey(key) {
776
776
  return createContext(issues, this.value[key], this.path.concat(key), this);
@@ -990,8 +990,7 @@ function validateObject(context, schema, isStrict) {
990
990
  if (missingKeys.length) {
991
991
  context.addIssue({
992
992
  message: "object value has missing keys",
993
- missingKeys,
994
- expectedKeys: keys
993
+ missingKeys
995
994
  });
996
995
  }
997
996
  if (isStrict) {
@@ -1004,8 +1003,7 @@ function validateObject(context, schema, isStrict) {
1004
1003
  if (unknownKeys.length) {
1005
1004
  context.addIssue({
1006
1005
  message: "object value has unknown keys",
1007
- unknownKeys,
1008
- expectedKeys: keys
1006
+ unknownKeys
1009
1007
  });
1010
1008
  }
1011
1009
  }
@@ -1453,7 +1451,7 @@ function markup(valueOrStrings, ...placeholders) {
1453
1451
  }
1454
1452
 
1455
1453
  // ../owl-runtime/dist/owl-runtime.es.js
1456
- var version = "3.0.0-alpha.36";
1454
+ var version = "3.0.0-alpha.38";
1457
1455
  var fibersInError = /* @__PURE__ */ new WeakMap();
1458
1456
  var nodeErrorHandlers = /* @__PURE__ */ new WeakMap();
1459
1457
  function invokeErrorHandlers(node, error, finalize, markFibers) {
@@ -1543,7 +1541,7 @@ var config2 = {
1543
1541
  return false;
1544
1542
  }
1545
1543
  };
1546
- var txt = document.createTextNode("");
1544
+ var txt = globalThis.document?.createTextNode("");
1547
1545
  var VToggler = class {
1548
1546
  key;
1549
1547
  child;
@@ -3349,6 +3347,11 @@ var ComponentNode = class extends Scope {
3349
3347
  willPatch = [];
3350
3348
  patched = [];
3351
3349
  signalComputation;
3350
+ // t-ref signals bound to an element hosted by this component, mapped to their
3351
+ // atom (so the element can be read without subscribing). Swept by isConnected
3352
+ // after each patch and after this subtree is removed, to unset a ref pointing
3353
+ // at a bulk-removed element (slot host, enclosing t-if) — see sweepRefs.
3354
+ trackedRefs = null;
3352
3355
  constructor(C, props2, app, parent, parentKey) {
3353
3356
  super(app);
3354
3357
  this.parent = parent;
@@ -3457,9 +3460,15 @@ var ComponentNode = class extends Scope {
3457
3460
  }
3458
3461
  destroy() {
3459
3462
  let shouldRemove = this.status === STATUS.MOUNTED;
3460
- this._destroy();
3463
+ removalDepth++;
3464
+ try {
3465
+ this._destroy();
3466
+ } finally {
3467
+ removalDepth--;
3468
+ }
3461
3469
  if (shouldRemove) {
3462
3470
  this.bdom.remove();
3471
+ sweepRemovedRefs();
3463
3472
  }
3464
3473
  }
3465
3474
  _destroy() {
@@ -3469,12 +3478,42 @@ var ComponentNode = class extends Scope {
3469
3478
  cb.call(component);
3470
3479
  }
3471
3480
  }
3481
+ if (removalDepth && this.trackedRefs) {
3482
+ (removed ||= []).push(this);
3483
+ }
3472
3484
  for (let childKey in this.children) {
3473
3485
  this.children[childKey]._destroy();
3474
3486
  }
3475
3487
  this.finalize((e) => handleError({ error: e, node: this }));
3476
3488
  disposeComputation(this.signalComputation);
3477
3489
  }
3490
+ /**
3491
+ * Unset any tracked t-ref whose element is no longer in the document, and stop
3492
+ * tracking it (createRef re-registers it on the next render if the element
3493
+ * comes back). `isConnected` is the discriminator: a ref the block's own
3494
+ * remove() failed to clear (bulk removal) points at a detached element and is
3495
+ * cleared, while a ref a surviving sibling just took over (t-if/t-else with a
3496
+ * shared signal) points at a still-connected element and is left alone.
3497
+ *
3498
+ * Called after this component's dom settles: at the tail of `_patch` (before
3499
+ * user `onPatched`), so an element removed in place is caught, and — for the
3500
+ * nodes collected during `_destroy` — after a removed subtree is detached.
3501
+ */
3502
+ sweepRefs() {
3503
+ const refs = this.trackedRefs;
3504
+ if (!refs) {
3505
+ return;
3506
+ }
3507
+ for (const [ref2, atom] of refs) {
3508
+ const el = atom.value;
3509
+ if (!el) {
3510
+ refs.delete(ref2);
3511
+ } else if (!el.isConnected) {
3512
+ ref2.set(null);
3513
+ refs.delete(ref2);
3514
+ }
3515
+ }
3516
+ }
3478
3517
  /**
3479
3518
  * Finds a child that has dom that is not yet updated, and update it. This
3480
3519
  * method is meant to be used only in the context of repatching the dom after
@@ -3490,7 +3529,14 @@ var ComponentNode = class extends Scope {
3490
3529
  child.updateDom();
3491
3530
  }
3492
3531
  } else {
3493
- this.bdom.patch(this.fiber.bdom, false);
3532
+ removalDepth++;
3533
+ try {
3534
+ this.bdom.patch(this.fiber.bdom, false);
3535
+ } finally {
3536
+ removalDepth--;
3537
+ }
3538
+ this.sweepRefs();
3539
+ sweepRemovedRefs();
3494
3540
  this.fiber.appliedToDom = true;
3495
3541
  this.fiber = null;
3496
3542
  }
@@ -3517,6 +3563,14 @@ var ComponentNode = class extends Scope {
3517
3563
  moveBeforeVNode(other, afterNode) {
3518
3564
  this.bdom.moveBeforeVNode(other ? other.bdom : null, afterNode);
3519
3565
  }
3566
+ /**
3567
+ * Register a t-ref signal bound to an element this component hosts, so its
3568
+ * lifecycle can clear it (see sweepRefs / _destroy). Idempotent — re-tracking
3569
+ * the same signal on each render just refreshes its atom.
3570
+ */
3571
+ trackRef(ref2, atom) {
3572
+ (this.trackedRefs ||= /* @__PURE__ */ new Map()).set(ref2, atom);
3573
+ }
3520
3574
  patch() {
3521
3575
  if (this.fiber && this.fiber.parent) {
3522
3576
  this._patch();
@@ -3530,7 +3584,14 @@ var ComponentNode = class extends Scope {
3530
3584
  }
3531
3585
  const fiber = this.fiber;
3532
3586
  this.children = fiber.childrenMap;
3533
- this.bdom.patch(fiber.bdom, hasChildren);
3587
+ removalDepth++;
3588
+ try {
3589
+ this.bdom.patch(fiber.bdom, hasChildren);
3590
+ } finally {
3591
+ removalDepth--;
3592
+ }
3593
+ this.sweepRefs();
3594
+ sweepRemovedRefs();
3534
3595
  fiber.appliedToDom = true;
3535
3596
  this.fiber = null;
3536
3597
  }
@@ -3541,6 +3602,17 @@ var ComponentNode = class extends Scope {
3541
3602
  this.bdom.remove();
3542
3603
  }
3543
3604
  };
3605
+ var removalDepth = 0;
3606
+ var removed = null;
3607
+ function sweepRemovedRefs() {
3608
+ if (removalDepth === 0 && removed) {
3609
+ const nodes = removed;
3610
+ removed = null;
3611
+ for (let i = 0; i < nodes.length; i++) {
3612
+ nodes[i].sweepRefs();
3613
+ }
3614
+ }
3615
+ }
3544
3616
  function getComponentScope() {
3545
3617
  const scope = useScope();
3546
3618
  if (!(scope instanceof ComponentNode)) {
@@ -3743,7 +3815,7 @@ function safeOutput(value, defaultValue) {
3743
3815
  }
3744
3816
  return toggler(safeKey, block);
3745
3817
  }
3746
- function createRef(ref2) {
3818
+ function createRef(ref2, node) {
3747
3819
  if (!ref2) {
3748
3820
  throw new OwlError(`Ref is undefined or null`);
3749
3821
  }
@@ -3758,6 +3830,9 @@ function createRef(ref2) {
3758
3830
  remove2 = atom ? (prevEl) => {
3759
3831
  if (atom.value === prevEl) ref2.set(null);
3760
3832
  } : () => ref2.set(null);
3833
+ if (atom) {
3834
+ node.trackRef(ref2, atom);
3835
+ }
3761
3836
  } else {
3762
3837
  throw new OwlError(
3763
3838
  `Ref should implement either a 'set' function or 'add' and 'delete' functions`
@@ -4509,8 +4584,8 @@ var blockDom = {
4509
4584
  };
4510
4585
  var __info__ = {
4511
4586
  version: App.version,
4512
- date: "2026-06-11T18:30:46.327Z",
4513
- hash: "eb837019",
4587
+ date: "2026-06-22T09:21:02.010Z",
4588
+ hash: "ac221141",
4514
4589
  url: "https://github.com/odoo/owl"
4515
4590
  };
4516
4591
 
@@ -6041,7 +6116,7 @@ ${code}`;
6041
6116
  if (ast.ref) {
6042
6117
  const refExpr = compileExpr(ast.ref);
6043
6118
  this.helpers.add("createRef");
6044
- const setRefStr = `createRef(${refExpr})`;
6119
+ const setRefStr = `createRef(${refExpr}, node)`;
6045
6120
  const idx = block.insertData(setRefStr, "ref");
6046
6121
  attrs["block-ref"] = String(idx);
6047
6122
  }
package/dist/owl.iife.js CHANGED
@@ -823,7 +823,7 @@ var owl = (() => {
823
823
  ${issueStrings}`);
824
824
  }
825
825
  }
826
- function createContext(issues, value, path, parent) {
826
+ function createContext(issues, value, path, parent, depthOffset = 1) {
827
827
  return {
828
828
  issueDepth: 0,
829
829
  path,
@@ -844,11 +844,11 @@ ${issueStrings}`);
844
844
  validate(type) {
845
845
  type(this);
846
846
  if (!this.isValid && parent) {
847
- parent.issueDepth = this.issueDepth + 1;
847
+ parent.issueDepth = this.issueDepth + depthOffset;
848
848
  }
849
849
  },
850
850
  withIssues(issues2) {
851
- return createContext(issues2, this.value, this.path, this);
851
+ return createContext(issues2, this.value, this.path, this, 0);
852
852
  },
853
853
  withKey(key) {
854
854
  return createContext(issues, this.value[key], this.path.concat(key), this);
@@ -1068,8 +1068,7 @@ ${issueStrings}`);
1068
1068
  if (missingKeys.length) {
1069
1069
  context.addIssue({
1070
1070
  message: "object value has missing keys",
1071
- missingKeys,
1072
- expectedKeys: keys
1071
+ missingKeys
1073
1072
  });
1074
1073
  }
1075
1074
  if (isStrict) {
@@ -1082,8 +1081,7 @@ ${issueStrings}`);
1082
1081
  if (unknownKeys.length) {
1083
1082
  context.addIssue({
1084
1083
  message: "object value has unknown keys",
1085
- unknownKeys,
1086
- expectedKeys: keys
1084
+ unknownKeys
1087
1085
  });
1088
1086
  }
1089
1087
  }
@@ -1531,7 +1529,7 @@ ${issueStrings}`);
1531
1529
  }
1532
1530
 
1533
1531
  // ../owl-runtime/dist/owl-runtime.es.js
1534
- var version = "3.0.0-alpha.36";
1532
+ var version = "3.0.0-alpha.38";
1535
1533
  var fibersInError = /* @__PURE__ */ new WeakMap();
1536
1534
  var nodeErrorHandlers = /* @__PURE__ */ new WeakMap();
1537
1535
  function invokeErrorHandlers(node, error, finalize, markFibers) {
@@ -1621,7 +1619,7 @@ ${issueStrings}`);
1621
1619
  return false;
1622
1620
  }
1623
1621
  };
1624
- var txt = document.createTextNode("");
1622
+ var txt = globalThis.document?.createTextNode("");
1625
1623
  var VToggler = class {
1626
1624
  key;
1627
1625
  child;
@@ -3427,6 +3425,11 @@ ${issueStrings}`);
3427
3425
  willPatch = [];
3428
3426
  patched = [];
3429
3427
  signalComputation;
3428
+ // t-ref signals bound to an element hosted by this component, mapped to their
3429
+ // atom (so the element can be read without subscribing). Swept by isConnected
3430
+ // after each patch and after this subtree is removed, to unset a ref pointing
3431
+ // at a bulk-removed element (slot host, enclosing t-if) — see sweepRefs.
3432
+ trackedRefs = null;
3430
3433
  constructor(C, props2, app, parent, parentKey) {
3431
3434
  super(app);
3432
3435
  this.parent = parent;
@@ -3535,9 +3538,15 @@ ${issueStrings}`);
3535
3538
  }
3536
3539
  destroy() {
3537
3540
  let shouldRemove = this.status === STATUS.MOUNTED;
3538
- this._destroy();
3541
+ removalDepth++;
3542
+ try {
3543
+ this._destroy();
3544
+ } finally {
3545
+ removalDepth--;
3546
+ }
3539
3547
  if (shouldRemove) {
3540
3548
  this.bdom.remove();
3549
+ sweepRemovedRefs();
3541
3550
  }
3542
3551
  }
3543
3552
  _destroy() {
@@ -3547,12 +3556,42 @@ ${issueStrings}`);
3547
3556
  cb.call(component);
3548
3557
  }
3549
3558
  }
3559
+ if (removalDepth && this.trackedRefs) {
3560
+ (removed ||= []).push(this);
3561
+ }
3550
3562
  for (let childKey in this.children) {
3551
3563
  this.children[childKey]._destroy();
3552
3564
  }
3553
3565
  this.finalize((e) => handleError({ error: e, node: this }));
3554
3566
  disposeComputation(this.signalComputation);
3555
3567
  }
3568
+ /**
3569
+ * Unset any tracked t-ref whose element is no longer in the document, and stop
3570
+ * tracking it (createRef re-registers it on the next render if the element
3571
+ * comes back). `isConnected` is the discriminator: a ref the block's own
3572
+ * remove() failed to clear (bulk removal) points at a detached element and is
3573
+ * cleared, while a ref a surviving sibling just took over (t-if/t-else with a
3574
+ * shared signal) points at a still-connected element and is left alone.
3575
+ *
3576
+ * Called after this component's dom settles: at the tail of `_patch` (before
3577
+ * user `onPatched`), so an element removed in place is caught, and — for the
3578
+ * nodes collected during `_destroy` — after a removed subtree is detached.
3579
+ */
3580
+ sweepRefs() {
3581
+ const refs = this.trackedRefs;
3582
+ if (!refs) {
3583
+ return;
3584
+ }
3585
+ for (const [ref2, atom] of refs) {
3586
+ const el = atom.value;
3587
+ if (!el) {
3588
+ refs.delete(ref2);
3589
+ } else if (!el.isConnected) {
3590
+ ref2.set(null);
3591
+ refs.delete(ref2);
3592
+ }
3593
+ }
3594
+ }
3556
3595
  /**
3557
3596
  * Finds a child that has dom that is not yet updated, and update it. This
3558
3597
  * method is meant to be used only in the context of repatching the dom after
@@ -3568,7 +3607,14 @@ ${issueStrings}`);
3568
3607
  child.updateDom();
3569
3608
  }
3570
3609
  } else {
3571
- this.bdom.patch(this.fiber.bdom, false);
3610
+ removalDepth++;
3611
+ try {
3612
+ this.bdom.patch(this.fiber.bdom, false);
3613
+ } finally {
3614
+ removalDepth--;
3615
+ }
3616
+ this.sweepRefs();
3617
+ sweepRemovedRefs();
3572
3618
  this.fiber.appliedToDom = true;
3573
3619
  this.fiber = null;
3574
3620
  }
@@ -3595,6 +3641,14 @@ ${issueStrings}`);
3595
3641
  moveBeforeVNode(other, afterNode) {
3596
3642
  this.bdom.moveBeforeVNode(other ? other.bdom : null, afterNode);
3597
3643
  }
3644
+ /**
3645
+ * Register a t-ref signal bound to an element this component hosts, so its
3646
+ * lifecycle can clear it (see sweepRefs / _destroy). Idempotent — re-tracking
3647
+ * the same signal on each render just refreshes its atom.
3648
+ */
3649
+ trackRef(ref2, atom) {
3650
+ (this.trackedRefs ||= /* @__PURE__ */ new Map()).set(ref2, atom);
3651
+ }
3598
3652
  patch() {
3599
3653
  if (this.fiber && this.fiber.parent) {
3600
3654
  this._patch();
@@ -3608,7 +3662,14 @@ ${issueStrings}`);
3608
3662
  }
3609
3663
  const fiber = this.fiber;
3610
3664
  this.children = fiber.childrenMap;
3611
- this.bdom.patch(fiber.bdom, hasChildren);
3665
+ removalDepth++;
3666
+ try {
3667
+ this.bdom.patch(fiber.bdom, hasChildren);
3668
+ } finally {
3669
+ removalDepth--;
3670
+ }
3671
+ this.sweepRefs();
3672
+ sweepRemovedRefs();
3612
3673
  fiber.appliedToDom = true;
3613
3674
  this.fiber = null;
3614
3675
  }
@@ -3619,6 +3680,17 @@ ${issueStrings}`);
3619
3680
  this.bdom.remove();
3620
3681
  }
3621
3682
  };
3683
+ var removalDepth = 0;
3684
+ var removed = null;
3685
+ function sweepRemovedRefs() {
3686
+ if (removalDepth === 0 && removed) {
3687
+ const nodes = removed;
3688
+ removed = null;
3689
+ for (let i = 0; i < nodes.length; i++) {
3690
+ nodes[i].sweepRefs();
3691
+ }
3692
+ }
3693
+ }
3622
3694
  function getComponentScope() {
3623
3695
  const scope = useScope();
3624
3696
  if (!(scope instanceof ComponentNode)) {
@@ -3821,7 +3893,7 @@ ${issueStrings}`);
3821
3893
  }
3822
3894
  return toggler(safeKey, block);
3823
3895
  }
3824
- function createRef(ref2) {
3896
+ function createRef(ref2, node) {
3825
3897
  if (!ref2) {
3826
3898
  throw new OwlError(`Ref is undefined or null`);
3827
3899
  }
@@ -3836,6 +3908,9 @@ ${issueStrings}`);
3836
3908
  remove2 = atom ? (prevEl) => {
3837
3909
  if (atom.value === prevEl) ref2.set(null);
3838
3910
  } : () => ref2.set(null);
3911
+ if (atom) {
3912
+ node.trackRef(ref2, atom);
3913
+ }
3839
3914
  } else {
3840
3915
  throw new OwlError(
3841
3916
  `Ref should implement either a 'set' function or 'add' and 'delete' functions`
@@ -4587,8 +4662,8 @@ ${issueStrings}`);
4587
4662
  };
4588
4663
  var __info__ = {
4589
4664
  version: App.version,
4590
- date: "2026-06-11T18:30:46.327Z",
4591
- hash: "eb837019",
4665
+ date: "2026-06-22T09:21:02.010Z",
4666
+ hash: "ac221141",
4592
4667
  url: "https://github.com/odoo/owl"
4593
4668
  };
4594
4669
 
@@ -6119,7 +6194,7 @@ ${code}`;
6119
6194
  if (ast.ref) {
6120
6195
  const refExpr = compileExpr(ast.ref);
6121
6196
  this.helpers.add("createRef");
6122
- const setRefStr = `createRef(${refExpr})`;
6197
+ const setRefStr = `createRef(${refExpr}, node)`;
6123
6198
  const idx = block.insertData(setRefStr, "ref");
6124
6199
  attrs["block-ref"] = String(idx);
6125
6200
  }