fix(angular): race condition when fast navigation (#17197)

fixes #17194
fixes #16449
fixes #15413
This commit is contained in:
Manu MA
2019-01-22 15:03:43 +01:00
committed by GitHub
parent 3defbf3a8f
commit a945b03144
11 changed files with 165 additions and 56 deletions

View File

@@ -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();
}
});
}

View File

@@ -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,

View 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');
});
});

View File

@@ -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,

View File

@@ -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,

View File

@@ -0,0 +1,3 @@
<p>
navigation-page1 works!
</p>

View File

@@ -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++;
}
}

View File

@@ -0,0 +1,3 @@
<p>
navigation-page2 works!
</p>

View File

@@ -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');
}
}

View File

@@ -0,0 +1,3 @@
<p>
navigation-page3 works!
</p>

View File

@@ -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');
}
}