@@ -29,6 +29,16 @@ export class NativeModalRef {
2929 portalOutlet : NativeScriptDomPortalOutlet ;
3030 detachedLoaderRef : ComponentRef < DetachedLoader > ;
3131 modalViewRef : NgViewRef < any > ;
32+ /**
33+ * The actual NativeScript view passed to `parentView.showModal(...)`.
34+ *
35+ * For component portals this is the stable `targetView` ContentView
36+ * wrapper that owns the Angular host PVC. For template portals it
37+ * remains `modalViewRef.firstNativeLikeView` (the historical
38+ * behavior). Keeping a direct reference avoids walking parent
39+ * chains when programmatically closing the modal.
40+ */
41+ modalView ?: View ;
3242
3343 private _closeCallback : ( ) => void ;
3444 private _isDismissed = false ;
@@ -61,11 +71,18 @@ export class NativeModalRef {
6171 this . _closeCallback = once ( async ( ) => {
6272 this . stateChanged . next ( { state : 'closing' } ) ;
6373 if ( ! this . _isDismissed ) {
64- this . modalViewRef . firstNativeLikeView ?. closeModal ( ) ;
74+ // Prefer `modalView` (the actual presented view) over the
75+ // legacy `firstNativeLikeView`. Both paths ultimately reach
76+ // the same `_closeModalCallback` via parent walk, but going
77+ // through the presented view is one hop instead of three and
78+ // works even if the rendered first root has been replaced by
79+ // an HMR `ɵɵreplaceMetadata` cycle.
80+ const closeTarget = this . modalView ?? this . modalViewRef . firstNativeLikeView ;
81+ closeTarget ?. closeModal ( ) ;
6582 }
6683 await this . location ?. _closeModalNavigation ( ) ;
6784 // this.detachedLoaderRef?.destroy();
68- if ( this . modalViewRef ?. firstNativeLikeView . isLoaded ) {
85+ if ( this . modalViewRef ?. firstNativeLikeView ? .isLoaded ) {
6986 fromEvent ( this . modalViewRef . firstNativeLikeView , 'unloaded' )
7087 . pipe ( take ( 1 ) )
7188 . subscribe ( ( ) => this . stateChanged . next ( { state : 'closed' } ) ) ;
@@ -126,6 +143,15 @@ export class NativeModalRef {
126143 attachComponentPortal < T > ( portal : ComponentPortal < T > ) : ComponentRef < T > {
127144 this . startModalNavigation ( ) ;
128145
146+ // `targetView` is a stable ContentView wrapper we own. The Angular
147+ // component's host (a `ProxyViewContainer`) is attached as its
148+ // content via the portal outlet below. We present `targetView`
149+ // itself as the modal — *not* the first rendered template root —
150+ // so that component-level HMR (`ɵɵreplaceMetadata`) can re-render
151+ // into the PVC and the modal automatically displays the new
152+ // content via the wrapper. Presenting the rendered first root
153+ // directly worked for the initial open but left subsequent HMR
154+ // updates rendering into a detached PVC, producing a blank modal.
129155 const targetView = new ContentView ( ) ;
130156 this . portalOutlet = new NativeScriptDomPortalOutlet (
131157 targetView ,
@@ -136,15 +162,21 @@ export class NativeModalRef {
136162 const componentRef = this . portalOutlet . attach ( portal ) ;
137163 componentRef . changeDetectorRef . detectChanges ( ) ;
138164 this . modalViewRef = new NgViewRef ( componentRef ) ;
165+ this . modalView = targetView ;
139166 if ( this . modalViewRef . firstNativeLikeView !== this . modalViewRef . view ) {
140167 ( < any > this . modalViewRef . view ) . _ngDialogRoot = this . modalViewRef . firstNativeLikeView ;
141168 }
142- this . modalViewRef . firstNativeLikeView [ '__ng_modal_id__' ] = this . _id ;
143- // if we don't detach the view from its parent, ios gets mad
144- this . modalViewRef . detachNativeLikeView ( ) ;
169+ // Tag both the wrapper (the actual modal root) and the rendered
170+ // first root so `getClosestDialog`'s parent walk finds the modal
171+ // id regardless of whether it starts from a view inside the
172+ // template or from the wrapper.
173+ targetView [ '__ng_modal_id__' ] = this . _id ;
174+ if ( this . modalViewRef . firstNativeLikeView ) {
175+ this . modalViewRef . firstNativeLikeView [ '__ng_modal_id__' ] = this . _id ;
176+ }
145177
146178 const userOptions = this . _config . nativeOptions || { } ;
147- this . parentView . showModal ( this . modalViewRef . firstNativeLikeView , {
179+ this . parentView . showModal ( targetView , {
148180 context : null ,
149181 ...userOptions ,
150182 closeCallback : async ( ) => {
0 commit comments