mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
fix(angular): race condition when fast navigation (#17197)
fixes #17194 fixes #16449 fixes #15413
This commit is contained in:
@@ -9,9 +9,8 @@ import { RouteView, StackEvent, computeStackId, destroyView, getUrl, insertView,
|
||||
|
||||
export class StackController {
|
||||
|
||||
private viewsSnapshot: RouteView[] = [];
|
||||
private views: RouteView[] = [];
|
||||
private runningTransition?: Promise<boolean>;
|
||||
private runningTask?: Promise<any>;
|
||||
private skipTransition = false;
|
||||
private tabsPrefix: string[] | undefined;
|
||||
private activeView: RouteView | undefined;
|
||||
@@ -50,7 +49,7 @@ export class StackController {
|
||||
return view;
|
||||
}
|
||||
|
||||
async setActive(enteringView: RouteView): Promise<StackEvent> {
|
||||
setActive(enteringView: RouteView): Promise<StackEvent> {
|
||||
let { direction, animation } = this.navCtrl.consumeTransition();
|
||||
const leavingView = this.activeView;
|
||||
const tabSwitch = isTabSwitch(enteringView, leavingView);
|
||||
@@ -58,15 +57,18 @@ export class StackController {
|
||||
direction = 'back';
|
||||
animation = undefined;
|
||||
}
|
||||
this.insertView(enteringView, direction);
|
||||
await this.transition(enteringView, leavingView, animation, this.canGoBack(1), false);
|
||||
await this.cleanupAsync();
|
||||
return {
|
||||
enteringView,
|
||||
direction,
|
||||
animation,
|
||||
tabSwitch
|
||||
};
|
||||
const viewsSnapshot = this.views.slice();
|
||||
const views = this.insertView(enteringView, direction);
|
||||
return this.wait(async () => {
|
||||
await this.transition(enteringView, leavingView, animation, this.canGoBack(1), false);
|
||||
await cleanupAsync(enteringView, views, viewsSnapshot);
|
||||
return {
|
||||
enteringView,
|
||||
direction,
|
||||
animation,
|
||||
tabSwitch
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
canGoBack(deep: number, stackId = this.getActiveStackId()): boolean {
|
||||
@@ -84,19 +86,21 @@ export class StackController {
|
||||
});
|
||||
}
|
||||
|
||||
startBackTransition() {
|
||||
async startBackTransition() {
|
||||
const leavingView = this.activeView;
|
||||
if (leavingView) {
|
||||
const views = this.getStack(leavingView.stackId);
|
||||
const enteringView = views[views.length - 2];
|
||||
enteringView.ref.changeDetectorRef.reattach();
|
||||
this.transition(
|
||||
enteringView, // entering view
|
||||
leavingView, // leaving view
|
||||
'back',
|
||||
true,
|
||||
true
|
||||
);
|
||||
await this.wait(() => {
|
||||
return this.transition(
|
||||
enteringView, // entering view
|
||||
leavingView, // leaving view
|
||||
'back',
|
||||
true,
|
||||
true
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,33 +134,7 @@ export class StackController {
|
||||
private insertView(enteringView: RouteView, direction: RouterDirection) {
|
||||
this.activeView = enteringView;
|
||||
this.views = insertView(this.views, enteringView, direction);
|
||||
}
|
||||
|
||||
private cleanupAsync() {
|
||||
return new Promise(resolve => {
|
||||
requestAnimationFrame(() => {
|
||||
this.cleanup();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private cleanup() {
|
||||
const activeRoute = this.activeView;
|
||||
const views = this.views;
|
||||
this.viewsSnapshot
|
||||
.filter(view => !views.includes(view))
|
||||
.forEach(view => destroyView(view));
|
||||
|
||||
views.forEach(view => {
|
||||
if (view !== activeRoute) {
|
||||
const element = view.element;
|
||||
element.setAttribute('aria-hidden', 'true');
|
||||
element.classList.add('ion-page-hidden');
|
||||
view.ref.changeDetectorRef.detach();
|
||||
}
|
||||
});
|
||||
this.viewsSnapshot = views.slice();
|
||||
return this.views.slice();
|
||||
}
|
||||
|
||||
private async transition(
|
||||
@@ -166,10 +144,6 @@ export class StackController {
|
||||
showGoBack: boolean,
|
||||
progressAnimation: boolean
|
||||
) {
|
||||
if (this.runningTransition !== undefined) {
|
||||
await this.runningTransition;
|
||||
this.runningTransition = undefined;
|
||||
}
|
||||
if (this.skipTransition) {
|
||||
this.skipTransition = false;
|
||||
return;
|
||||
@@ -184,14 +158,46 @@ export class StackController {
|
||||
}
|
||||
|
||||
await containerEl.componentOnReady();
|
||||
this.runningTransition = containerEl.commit(enteringEl, leavingEl, {
|
||||
await containerEl.commit(enteringEl, leavingEl, {
|
||||
deepWait: true,
|
||||
duration: direction === undefined ? 0 : undefined,
|
||||
direction,
|
||||
showGoBack,
|
||||
progressAnimation
|
||||
});
|
||||
await this.runningTransition;
|
||||
}
|
||||
}
|
||||
|
||||
private async wait<T>(task: () => Promise<T>): Promise<T> {
|
||||
if (this.runningTask !== undefined) {
|
||||
await this.runningTask;
|
||||
this.runningTask = undefined;
|
||||
}
|
||||
const promise = this.runningTask = task();
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupAsync(activeRoute: RouteView, views: RouteView[], viewsSnapshot: RouteView[]) {
|
||||
return new Promise(resolve => {
|
||||
requestAnimationFrame(() => {
|
||||
cleanup(activeRoute, views, viewsSnapshot);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function cleanup(activeRoute: RouteView, views: RouteView[], viewsSnapshot: RouteView[]) {
|
||||
viewsSnapshot
|
||||
.filter(view => !views.includes(view))
|
||||
.forEach(destroyView);
|
||||
|
||||
views.forEach(view => {
|
||||
if (view !== activeRoute) {
|
||||
const element = view.element;
|
||||
element.setAttribute('aria-hidden', 'true');
|
||||
element.classList.add('ion-page-hidden');
|
||||
view.ref.changeDetectorRef.detach();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('form', () => {
|
||||
});
|
||||
|
||||
it('ion-toggle should change', async () => {
|
||||
await element(by.css('ion-toggle')).click();
|
||||
await element(by.css('form ion-toggle')).click();
|
||||
await testData({
|
||||
'datetime': '2010-08-20',
|
||||
'select': null,
|
||||
@@ -84,7 +84,7 @@ describe('form', () => {
|
||||
});
|
||||
|
||||
it('ion-toggle should change only after blur', async () => {
|
||||
await element(by.css('ion-toggle')).click();
|
||||
await element(by.css('form ion-toggle')).click();
|
||||
await testData({
|
||||
'datetime': '2010-08-20',
|
||||
'select': null,
|
||||
|
||||
24
angular/test/test-app/e2e/src/navigation.e2e-spec.ts
Normal file
24
angular/test/test-app/e2e/src/navigation.e2e-spec.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { browser, element, by } from 'protractor';
|
||||
import { handleErrorMessages, waitTime, testStack } from './utils';
|
||||
|
||||
describe('navigation', () => {
|
||||
|
||||
afterEach(() => {
|
||||
handleErrorMessages();
|
||||
});
|
||||
|
||||
it('should navigate correctly', async () => {
|
||||
await browser.get('/navigation/page1');
|
||||
await waitTime(2000);
|
||||
await testStack('ion-router-outlet', ['app-navigation-page2', 'app-navigation-page1']);
|
||||
|
||||
const pageHidden = element(by.css('app-navigation-page2'));
|
||||
expect(await pageHidden.getAttribute('aria-hidden')).toEqual('true');
|
||||
expect(await pageHidden.getAttribute('class')).toEqual('ion-page ion-page-hidden');
|
||||
|
||||
const pageVisible = element(by.css('app-navigation-page1'));
|
||||
expect(await pageVisible.getAttribute('aria-hidden')).toEqual(null);
|
||||
expect(await pageVisible.getAttribute('class')).toEqual('ion-page can-go-back');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -18,6 +18,9 @@ import { ViewChildComponent } from './view-child/view-child.component';
|
||||
import { ProvidersComponent } from './providers/providers.component';
|
||||
import { SlidesComponent } from './slides/slides.component';
|
||||
import { FormComponent } from './form/form.component';
|
||||
import { NavigationPage1Component } from './navigation-page1/navigation-page1.component';
|
||||
import { NavigationPage2Component } from './navigation-page2/navigation-page2.component';
|
||||
import { NavigationPage3Component } from './navigation-page3/navigation-page3.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: HomePageComponent },
|
||||
@@ -32,6 +35,14 @@ const routes: Routes = [
|
||||
{ path: 'virtual-scroll', component: VirtualScrollComponent },
|
||||
{ path: 'virtual-scroll-detail/:itemId', component: VirtualScrollDetailComponent },
|
||||
{ path: 'tabs', redirectTo: '/tabs/account', pathMatch: 'full' },
|
||||
{
|
||||
path: 'navigation',
|
||||
children: [
|
||||
{ path: 'page1', component: NavigationPage1Component },
|
||||
{ path: 'page2', component: NavigationPage2Component },
|
||||
{ path: 'page3', component: NavigationPage3Component }
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'tabs',
|
||||
component: TabsComponent,
|
||||
|
||||
@@ -27,6 +27,9 @@ import { ViewChildComponent } from './view-child/view-child.component';
|
||||
import { ProvidersComponent } from './providers/providers.component';
|
||||
import { SlidesComponent } from './slides/slides.component';
|
||||
import { FormComponent } from './form/form.component';
|
||||
import { NavigationPage1Component } from './navigation-page1/navigation-page1.component';
|
||||
import { NavigationPage2Component } from './navigation-page2/navigation-page2.component';
|
||||
import { NavigationPage3Component } from './navigation-page3/navigation-page3.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -51,7 +54,10 @@ import { FormComponent } from './form/form.component';
|
||||
ViewChildComponent,
|
||||
ProvidersComponent,
|
||||
SlidesComponent,
|
||||
FormComponent
|
||||
FormComponent,
|
||||
NavigationPage1Component,
|
||||
NavigationPage2Component,
|
||||
NavigationPage3Component
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<p>
|
||||
navigation-page1 works!
|
||||
</p>
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { NavController } from '@ionic/angular';
|
||||
|
||||
let count = 0;
|
||||
@Component({
|
||||
selector: 'app-navigation-page1',
|
||||
templateUrl: './navigation-page1.component.html',
|
||||
})
|
||||
export class NavigationPage1Component {
|
||||
constructor(
|
||||
private navController: NavController
|
||||
) {}
|
||||
|
||||
ionViewDidEnter() {
|
||||
if (count < 1) {
|
||||
this.navController.navigateBack('/navigation/page2');
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<p>
|
||||
navigation-page2 works!
|
||||
</p>
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { NavController } from '@ionic/angular';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-navigation-page2',
|
||||
templateUrl: './navigation-page2.component.html',
|
||||
})
|
||||
export class NavigationPage2Component {
|
||||
constructor(
|
||||
private navController: NavController
|
||||
) {}
|
||||
|
||||
ionViewDidEnter() {
|
||||
this.navController.navigateForward('/navigation/page1');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<p>
|
||||
navigation-page3 works!
|
||||
</p>
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { NavController } from '@ionic/angular';
|
||||
|
||||
@Component({
|
||||
selector: 'app-navigation-page3',
|
||||
templateUrl: './navigation-page3.component.html',
|
||||
})
|
||||
export class NavigationPage3Component {
|
||||
constructor(
|
||||
private navController: NavController
|
||||
) {}
|
||||
|
||||
ionViewDidEnter() {
|
||||
this.navController.navigateRoot('/navigation/page2');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user