Skip to content

Commit

Permalink
feat(router): warn if navigation triggered outside Angular zone (#24959)
Browse files Browse the repository at this point in the history
closes #15770, closes #15946, closes #24728

PR Close #24959
  • Loading branch information
trotyl authored and mhevery committed Sep 5, 2018
1 parent 234661b commit 010e35d
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 4 deletions.
2 changes: 1 addition & 1 deletion packages/core/src/zone.ts
Expand Up @@ -7,4 +7,4 @@
*/ */


// Public API for Zone // Public API for Zone
export {NgZone} from './zone/ng_zone'; export {NgZone, NoopNgZone as ɵNoopNgZone} from './zone/ng_zone';
12 changes: 11 additions & 1 deletion packages/router/src/router.ts
Expand Up @@ -7,7 +7,7 @@
*/ */


import {Location} from '@angular/common'; import {Location} from '@angular/common';
import {Compiler, Injector, NgModuleFactoryLoader, NgModuleRef, Type, isDevMode} from '@angular/core'; import {Compiler, Injector, NgModuleFactoryLoader, NgModuleRef, NgZone, Optional, Type, isDevMode, ɵConsole as Console} from '@angular/core';
import {BehaviorSubject, Observable, Subject, Subscription, of } from 'rxjs'; import {BehaviorSubject, Observable, Subject, Subscription, of } from 'rxjs';
import {concatMap, map, mergeMap} from 'rxjs/operators'; import {concatMap, map, mergeMap} from 'rxjs/operators';


Expand Down Expand Up @@ -224,6 +224,8 @@ export class Router {
private navigationId: number = 0; private navigationId: number = 0;
private configLoader: RouterConfigLoader; private configLoader: RouterConfigLoader;
private ngModule: NgModuleRef<any>; private ngModule: NgModuleRef<any>;
private console: Console;
private isNgZoneEnabled: boolean = false;


public readonly events: Observable<Event> = new Subject<Event>(); public readonly events: Observable<Event> = new Subject<Event>();
public readonly routerState: RouterState; public readonly routerState: RouterState;
Expand Down Expand Up @@ -314,6 +316,9 @@ export class Router {
const onLoadEnd = (r: Route) => this.triggerEvent(new RouteConfigLoadEnd(r)); const onLoadEnd = (r: Route) => this.triggerEvent(new RouteConfigLoadEnd(r));


this.ngModule = injector.get(NgModuleRef); this.ngModule = injector.get(NgModuleRef);
this.console = injector.get(Console);
const ngZone = injector.get(NgZone);
this.isNgZoneEnabled = ngZone instanceof NgZone;


this.resetConfig(config); this.resetConfig(config);
this.currentUrlTree = createEmptyUrlTree(); this.currentUrlTree = createEmptyUrlTree();
Expand Down Expand Up @@ -495,6 +500,11 @@ export class Router {
*/ */
navigateByUrl(url: string|UrlTree, extras: NavigationExtras = {skipLocationChange: false}): navigateByUrl(url: string|UrlTree, extras: NavigationExtras = {skipLocationChange: false}):
Promise<boolean> { Promise<boolean> {
if (isDevMode() && this.isNgZoneEnabled && !NgZone.isInAngularZone()) {
this.console.warn(
`Navigation triggered outside Angular zone, did you forget to call 'ngZone.run()'?`);
}

const urlTree = url instanceof UrlTree ? url : this.parseUrl(url); const urlTree = url instanceof UrlTree ? url : this.parseUrl(url);
const mergedTree = this.urlHandlingStrategy.merge(urlTree, this.rawUrlTree); const mergedTree = this.urlHandlingStrategy.merge(urlTree, this.rawUrlTree);


Expand Down
51 changes: 49 additions & 2 deletions packages/router/test/integration.spec.ts
Expand Up @@ -8,7 +8,7 @@


import {CommonModule, Location} from '@angular/common'; import {CommonModule, Location} from '@angular/common';
import {SpyLocation} from '@angular/common/testing'; import {SpyLocation} from '@angular/common/testing';
import {ChangeDetectionStrategy, Component, Injectable, NgModule, NgModuleFactoryLoader, NgModuleRef} from '@angular/core'; import {ChangeDetectionStrategy, Component, Injectable, NgModule, NgModuleFactoryLoader, NgModuleRef, NgZone, ɵConsole as Console, ɵNoopNgZone as NoopNgZone} from '@angular/core';
import {ComponentFixture, TestBed, fakeAsync, inject, tick} from '@angular/core/testing'; import {ComponentFixture, TestBed, fakeAsync, inject, tick} from '@angular/core/testing';
import {By} from '@angular/platform-browser/src/dom/debug/by'; import {By} from '@angular/platform-browser/src/dom/debug/by';
import {expect} from '@angular/platform-browser/testing/src/matchers'; import {expect} from '@angular/platform-browser/testing/src/matchers';
Expand All @@ -20,10 +20,13 @@ import {forEach} from '../src/utils/collection';
import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing'; import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing';


describe('Integration', () => { describe('Integration', () => {
const noopConsole: Console = {log() {}, warn() {}};

beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: imports:
[RouterTestingModule.withRoutes([{path: 'simple', component: SimpleCmp}]), TestModule] [RouterTestingModule.withRoutes([{path: 'simple', component: SimpleCmp}]), TestModule],
providers: [{provide: Console, useValue: noopConsole}]
}); });
}); });


Expand Down Expand Up @@ -154,6 +157,50 @@ describe('Integration', () => {
}))); })));
}); });


describe('navigation warning', () => {
let warnings: string[] = [];

class MockConsole {
warn(message: string) { warnings.push(message); }
}

beforeEach(() => {
warnings = [];
TestBed.overrideProvider(Console, {useValue: new MockConsole()});
});

describe('with NgZone enabled', () => {
it('should warn when triggered outside Angular zone',
fakeAsync(inject([Router, NgZone], (router: Router, ngZone: NgZone) => {
ngZone.runOutsideAngular(() => { router.navigateByUrl('/simple'); });

expect(warnings.length).toBe(1);
expect(warnings[0])
.toBe(
`Navigation triggered outside Angular zone, did you forget to call 'ngZone.run()'?`);
})));

it('should not warn when triggered inside Angular zone',
fakeAsync(inject([Router, NgZone], (router: Router, ngZone: NgZone) => {
ngZone.run(() => { router.navigateByUrl('/simple'); });

expect(warnings.length).toBe(0);
})));
});

describe('with NgZone disabled', () => {
beforeEach(() => { TestBed.overrideProvider(NgZone, {useValue: new NoopNgZone()}); });

it('should not warn when triggered outside Angular zone',
fakeAsync(inject([Router, NgZone], (router: Router, ngZone: NgZone) => {

ngZone.runOutsideAngular(() => { router.navigateByUrl('/simple'); });

expect(warnings.length).toBe(0);
})));
});
});

describe('should execute navigations serially', () => { describe('should execute navigations serially', () => {
let log: any[] = []; let log: any[] = [];


Expand Down

3 comments on commit 010e35d

@YuZaiShui
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's mean of Navigation triggered outside Angular zone? Whether i must inject NgZone and call NgZone.runOutsideAngular ?

@trotyl
Copy link
Contributor Author

@trotyl trotyl commented on 010e35d Feb 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whether i must inject NgZone and call NgZone.runOutsideAngular ?

@YuZaiShui You must NOT invoke navigation under runOutsideAngular() and the warning occurs if you did.

@YuZaiShui
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@trotyl thanks for your answer.

Please sign in to comment.