fix(nav): root component is mounted with root params (#27676)

Issue number: Resolves #27146

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

When an `ion-nav` is presented multiple times in an overlay, the user's
`root` component will attempt to be attached twice. This results in the
view being mounted without the `rootParams` being defined, causing an
exception in a user's application.

This behavior occurs due to the `root` watch callback firing twice. The
second time the `ion-nav` is presented in an overlay, the watch callback
will execute _before_ `componentDidLoad` fires. This results in the
watch callback firing twice, once from the underlying change detection
and the second time from [manually calling the
function](https://github.com/ionic-team/ionic-framework/blob/main/core/src/components/nav/nav.tsx#L115)
from `componentDidLoad`.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- `ion-nav` includes a new flag to track when `componentDidLoad`
executes. This allows us to prevent the behavior of the `rootChanged`
callback from happening when the component has not loaded.
- `ion-nav` consistently attaches the `rootParams` to the `root`
component.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

Dev-build: `7.0.15-dev.11687924603.10a1e477`.

### How to test

You can install against the reproduction app in the linked issue to
verify the behavior before and after.
- Before, the app will throw an exception when presenting the modal the
second time.
- After, the app will not throw an exception when presenting the modal
the second time. Information that you fill out on the main screen form
will be rendered inside the modal content.
This commit is contained in:
Sean Perkins
2023-06-28 11:27:25 -04:00
committed by GitHub
parent 458d16e742
commit 1f06be4a31
5 changed files with 131 additions and 5 deletions

View File

@ -0,0 +1,20 @@
describe('Modal Nav Params', () => {
beforeEach(() => {
cy.visit('/version-test/modal-nav-params');
});
it('should assign the rootParams when presented in a modal multiple times', () => {
cy.contains('Open Modal').click();
cy.get('ion-modal').should('exist').should('be.visible');
cy.get('ion-modal').contains('OK');
cy.contains("Close").click();
cy.get('ion-modal').should('not.be.visible');
cy.contains('Open Modal').click();
cy.get('ion-modal').should('exist').should('be.visible');
cy.get('ion-modal').contains('OK').should('exist');
});
});

View File

@ -0,0 +1,45 @@
import { Component } from "@angular/core";
import { IonicModule } from "@ionic/angular";
import { NavRootComponent } from "./nav-root.component";
@Component({
selector: 'app-modal-nav-params',
template: `
<ion-header>
<ion-toolbar>
<ion-title>Modal Nav Params</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-button id="open">Open Modal</ion-button>
<ion-modal #modal trigger="open">
<ng-template>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button (click)="modal.dismiss()">Close</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-nav [root]="root" [rootParams]="rootParams"></ion-nav>
</ion-content>
</ng-template>
</ion-modal>
</ion-content>
`,
standalone: true,
imports: [IonicModule, NavRootComponent]
})
export class ModalNavParamsComponent {
root = NavRootComponent;
rootParams = {
params: {
id: 123
}
};
}

View File

@ -0,0 +1,38 @@
import { JsonPipe } from "@angular/common";
import { Component } from "@angular/core";
import { IonicModule } from "@ionic/angular";
/**
* This is used to track if any occurences of
* the ion-nav root component being attached to
* the DOM result in the rootParams not being
* assigned to the component instance.
*
* https://github.com/ionic-team/ionic-framework/issues/27146
*/
let rootParamsException = false;
@Component({
selector: 'app-modal-content',
template: `
{{ hasException ? 'ERROR' : 'OK' }}
`,
standalone: true,
imports: [IonicModule, JsonPipe]
})
export class NavRootComponent {
params: any;
ngOnInit() {
if (this.params === undefined) {
rootParamsException = true;
}
}
get hasException() {
return rootParamsException;
}
}

View File

@ -3,7 +3,12 @@ import { RouterModule } from "@angular/router";
@NgModule({
imports: [
RouterModule.forChild([])
RouterModule.forChild([
{
path: 'modal-nav-params',
loadComponent: () => import('./modal-nav-params/modal-nav-params.component').then(m => m.ModalNavParamsComponent)
}
])
],
exports: [RouterModule]
})

View File

@ -2,6 +2,7 @@ import type { EventEmitter } from '@stencil/core';
import { Build, Component, Element, Event, Method, Prop, Watch, h } from '@stencil/core';
import { getTimeGivenProgression } from '@utils/animation/cubic-bezier';
import { assert } from '@utils/helpers';
import { printIonWarning } from '@utils/logging';
import type { TransitionOptions } from '@utils/transition';
import { lifecycle, setPageHidden, transition } from '@utils/transition';
@ -36,6 +37,7 @@ export class Nav implements NavOutlet {
private destroyed = false;
private views: ViewController[] = [];
private gesture?: Gesture;
private didLoad = false;
@Element() el!: HTMLElement;
@ -77,12 +79,25 @@ export class Nav implements NavOutlet {
@Watch('root')
rootChanged() {
const isDev = Build.isDev;
if (this.root !== undefined) {
if (!this.useRouter) {
this.setRoot(this.root, this.rootParams);
} else if (isDev) {
console.warn('<ion-nav> does not support a root attribute when using ion-router.');
if (this.root === undefined) {
return;
}
if (this.didLoad === false) {
/**
* If the component has not loaded yet, we can skip setting up the root component.
* It will be called when `componentDidLoad` fires.
*/
return;
}
if (!this.useRouter) {
if (this.root !== undefined) {
this.setRoot(this.root, this.rootParams);
}
} else if (isDev) {
printIonWarning('<ion-nav> does not support a root attribute when using ion-router.', this.el);
}
}
@ -112,6 +127,9 @@ export class Nav implements NavOutlet {
}
async componentDidLoad() {
// We want to set this flag before any watch callbacks are manually called
this.didLoad = true;
this.rootChanged();
this.gesture = (await import('../../utils/gesture/swipe-back')).createSwipeBackGesture(