@nativescript/angular 21.0.1-alpha.6 → 21.0.1-alpha.8

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.
@@ -22,6 +22,7 @@ class InvisibleNode extends View {
22
22
  this.name = name;
23
23
  this.nodeType = 1;
24
24
  this.nodeName = getClassName(this);
25
+ this.tagName = this.nodeName;
25
26
  }
26
27
  toString() {
27
28
  return `${this.nodeName}(${this.id})-${this.name}`;
@@ -3144,6 +3145,12 @@ class ViewUtil {
3144
3145
  }
3145
3146
  const ngView = view;
3146
3147
  ngView.nodeName = name;
3148
+ // Angular 21+ reads `rootElement.tagName.toLowerCase()` during component bootstrap
3149
+ // (`locateHostElement`) to reject `<script>` host elements. Native Views have no
3150
+ // intrinsic `tagName`, so without this assignment the boot throws
3151
+ // `Cannot read properties of undefined (reading 'toLowerCase')`. Mirror DOM
3152
+ // conventions where `tagName` equals `nodeName` for element nodes.
3153
+ ngView.tagName = name;
3147
3154
  ngView.meta = getViewMeta(name);
3148
3155
  // we're setting the node type of the view
3149
3156
  // to 'element' because of checks done in the
@@ -3545,22 +3552,35 @@ class NativeScriptRenderer {
3545
3552
  if (NativeScriptDebug.enabled) {
3546
3553
  NativeScriptDebug.rendererLog(`NativeScriptRenderer.selectRootElement: ${selectorOrNode}`);
3547
3554
  }
3555
+ // Angular 21+ reads `rootElement.tagName.toLowerCase()` after this call
3556
+ // (`locateHostElement`) to reject `<script>` hosts. Guarantee every return
3557
+ // path produces a View with a non-empty string `tagName`; otherwise the
3558
+ // bootstrap throws `Cannot read properties of undefined (reading 'toLowerCase')`.
3559
+ const ensureTagName = (view, fallback) => {
3560
+ if (view && typeof view.tagName !== 'string') {
3561
+ try {
3562
+ view.tagName = view.nodeName || fallback || 'view';
3563
+ }
3564
+ catch { }
3565
+ }
3566
+ return view;
3567
+ };
3548
3568
  if (selectorOrNode instanceof View) {
3549
- return selectorOrNode;
3569
+ return ensureTagName(selectorOrNode, '');
3550
3570
  }
3551
3571
  if (selectorOrNode && selectorOrNode[0] === '#') {
3552
3572
  const result = getViewById(this.rootView, selectorOrNode.slice(1));
3553
- return (result || this.rootView);
3573
+ return ensureTagName((result || this.rootView), selectorOrNode);
3554
3574
  }
3555
3575
  if (typeof selectorOrNode === 'string') {
3556
3576
  const view = this.viewUtil.createView(selectorOrNode);
3557
3577
  if (getFirstNativeLikeView(view) === view) {
3558
3578
  // view is nativelike!
3559
3579
  this.appendChild(this.rootView, view);
3560
- return view;
3580
+ return ensureTagName(view, selectorOrNode);
3561
3581
  }
3562
3582
  }
3563
- return this.rootView;
3583
+ return ensureTagName(this.rootView, '');
3564
3584
  }
3565
3585
  parentNode(node) {
3566
3586
  if (NativeScriptDebug.enabled) {
@@ -6399,6 +6419,120 @@ function buildNonAnimatedRestoreConfig(original) {
6399
6419
  return cloned;
6400
6420
  }
6401
6421
 
6422
+ /**
6423
+ * Pure helpers that mirror NativeScript's modal-host properties
6424
+ * (`_dialogFragment` on Android, `viewController` on iOS) from the
6425
+ * ContentView wrapper that `attachComponentPortal` presents as the
6426
+ * modal down onto every native-like descendant.
6427
+ *
6428
+ * Why? `parentView.showModal(targetView, ...)` stamps those props on
6429
+ * `targetView` itself — but user template code reads them off the
6430
+ * rendered template root that lives *inside* the wrapper (e.g.
6431
+ * `onLoaded($event)` → `args.object._dialogFragment.getDialog()`).
6432
+ * Without mirroring, every fresh modal open + every HMR
6433
+ * `ɵɵreplaceMetadata` re-render hands the user `undefined`, which is
6434
+ * how we hit:
6435
+ *
6436
+ * Cannot read properties of undefined (reading 'getDialog')
6437
+ * at CheckinComponent.onLoaded
6438
+ *
6439
+ * The helpers live in a standalone module on purpose, mirroring
6440
+ * `dialog-hmr-animation.ts`: they don't pull `@angular/core` or
6441
+ * `@nativescript/core`, so they're trivially unit-testable in the
6442
+ * Jest Node runner without an ESM transform or a NativeScript
6443
+ * runtime stub.
6444
+ */
6445
+ /**
6446
+ * Marker property used to make {@link installPvcModalHostPropPropagation}
6447
+ * idempotent. Exported only so the spec can assert install
6448
+ * idempotency without re-declaring the constant.
6449
+ */
6450
+ const PVC_ADD_VIEW_WRAPPED_MARKER = '__ng_modal_propagate_addview__';
6451
+ /**
6452
+ * Mirror `wrapper`'s modal-host props onto every descendant of
6453
+ * `root` via NativeScript's logical `eachChildView` walk.
6454
+ *
6455
+ * - The wrapper itself is **never** written to. NS core owns that
6456
+ * assignment and we must not shadow the canonical reference.
6457
+ * - Writes are idempotent: a descendant that already holds the same
6458
+ * reference is skipped, so repeat calls (HMR re-renders, multiple
6459
+ * PVC adds in one render pass) stay cheap.
6460
+ * - No-op when the modal isn't shown yet, has already closed, or
6461
+ * when either input is missing. NS clears both props to null on
6462
+ * close, so propagating after that point would only persist stale
6463
+ * references on the descendants.
6464
+ */
6465
+ function propagateModalHostPropsToDescendants(wrapper, root) {
6466
+ if (!wrapper || !root) {
6467
+ return;
6468
+ }
6469
+ const dialogFragment = wrapper._dialogFragment;
6470
+ const viewController = wrapper.viewController;
6471
+ if (dialogFragment == null && viewController == null) {
6472
+ return;
6473
+ }
6474
+ const visit = (view) => {
6475
+ if (!view) {
6476
+ return;
6477
+ }
6478
+ if (view !== wrapper) {
6479
+ if (dialogFragment !== undefined && view._dialogFragment !== dialogFragment) {
6480
+ view._dialogFragment = dialogFragment;
6481
+ }
6482
+ if (viewController !== undefined && view.viewController !== viewController) {
6483
+ view.viewController = viewController;
6484
+ }
6485
+ }
6486
+ view.eachChildView?.((child) => {
6487
+ visit(child);
6488
+ return true;
6489
+ });
6490
+ };
6491
+ visit(root);
6492
+ }
6493
+ /**
6494
+ * Idempotently wrap `host._addView` so every child added after
6495
+ * install — typically the new template root produced by Angular's
6496
+ * `ɵɵreplaceMetadata` HMR cycle — has `wrapper`'s modal-host props
6497
+ * mirrored onto it (and its current subtree) **before** NS attaches
6498
+ * the view.
6499
+ *
6500
+ * Why "before"? `_addView` on a loaded parent synchronously calls
6501
+ * `child.callLoaded()` deep inside, which fires the `loaded` event
6502
+ * chain on the new template root. User code (e.g.
6503
+ * `onLoaded($event)` → `args.object._dialogFragment.getDialog()`)
6504
+ * runs from inside that synchronous call. The props **must** be
6505
+ * present on the child by the time `_addView` runs — pre-hook
6506
+ * position is the only place that guarantees this without
6507
+ * monkey-patching NS internals.
6508
+ *
6509
+ * The wrap reads `wrapper._dialogFragment` / `wrapper.viewController`
6510
+ * lazily (per call) via {@link propagateModalHostPropsToDescendants},
6511
+ * so a wrap installed while the modal is open keeps doing the right
6512
+ * thing if HMR re-render happens later, and gracefully no-ops after
6513
+ * the modal closes (both props become null on the wrapper).
6514
+ *
6515
+ * No-op when `host` lacks `_addView` (e.g. a non-View element) or
6516
+ * when the wrap is already installed (`PVC_ADD_VIEW_WRAPPED_MARKER`
6517
+ * sentinel). The wrap is intentionally NOT removable — the host
6518
+ * lives only as long as the modal, so the wrap is GC'd with it.
6519
+ */
6520
+ function installPvcModalHostPropPropagation(host, wrapper) {
6521
+ if (!host || !wrapper) {
6522
+ return;
6523
+ }
6524
+ const target = host;
6525
+ if (target[PVC_ADD_VIEW_WRAPPED_MARKER] || typeof target._addView !== 'function') {
6526
+ return;
6527
+ }
6528
+ const origAddView = target._addView.bind(target);
6529
+ target._addView = (view, atIndex) => {
6530
+ propagateModalHostPropsToDescendants(wrapper, view);
6531
+ return origAddView(view, atIndex);
6532
+ };
6533
+ target[PVC_ADD_VIEW_WRAPPED_MARKER] = true;
6534
+ }
6535
+
6402
6536
  let NativeModalRef = class NativeModalRef {
6403
6537
  constructor(_config, _injector, location) {
6404
6538
  this._config = _config;
@@ -6528,6 +6662,25 @@ let NativeModalRef = class NativeModalRef {
6528
6662
  },
6529
6663
  cancelable: !this._config.disableClose,
6530
6664
  });
6665
+ // After `showModal`, NativeScript core has stamped
6666
+ // `_dialogFragment` (Android) / `viewController` (iOS) on
6667
+ // `targetView` itself — but user template code (`onLoaded($event)`
6668
+ // → `args.object._dialogFragment.getDialog()...`) reads those
6669
+ // props on the rendered template root, which is a *descendant*
6670
+ // of `targetView`, not `targetView` itself. Mirror them down so
6671
+ // the template root (and any nested loaded handler) sees the
6672
+ // real host objects instead of `undefined`, then install a
6673
+ // lazy wrap on the host PVC's `_addView` so future child
6674
+ // additions — typically the new template root produced by
6675
+ // Angular's `ɵɵreplaceMetadata` HMR cycle — get the same
6676
+ // props mirrored *before* NS attaches the view and fires
6677
+ // its `loaded` event chain. See `modal-host-props.ts` for the
6678
+ // full rationale.
6679
+ propagateModalHostPropsToDescendants(targetView, targetView);
6680
+ const hostView = componentRef.location?.nativeElement;
6681
+ if (hostView) {
6682
+ installPvcModalHostPropPropagation(hostView, targetView);
6683
+ }
6531
6684
  return componentRef;
6532
6685
  }
6533
6686
  _startExitAnimation() {