mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-17 18:54:11 +08:00
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:
20
angular/test/apps/ng16/e2e/src/modal-nav-params.spec.ts
Normal file
20
angular/test/apps/ng16/e2e/src/modal-nav-params.spec.ts
Normal 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');
|
||||
});
|
||||
|
||||
});
|
@ -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
|
||||
}
|
||||
};
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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]
|
||||
})
|
||||
|
@ -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(
|
||||
|
Reference in New Issue
Block a user