fix(upgrade): call setInterval outside the Angular zone
This wraps the $interval service when using upgrade to run the $interval() call outside the Angular zone. However, the callback is invoked within the Angular zone, so changes still propagate to downgraded components.
This commit is contained in:
parent
bb2fc6b8da
commit
269bbe0e7d
|
@ -158,6 +158,12 @@ export interface IInjectorService {
|
||||||
has(key: string): boolean;
|
has(key: string): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IIntervalService {
|
||||||
|
(func: Function, delay: number, count?: number, invokeApply?: boolean,
|
||||||
|
...args: any[]): Promise<any>;
|
||||||
|
cancel(promise: Promise<any>): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ITestabilityService {
|
export interface ITestabilityService {
|
||||||
findBindings(element: Element, expression: string, opt_exactMatch?: boolean): Element[];
|
findBindings(element: Element, expression: string, opt_exactMatch?: boolean): Element[];
|
||||||
findModels(element: Element, expression: string, opt_exactMatch?: boolean): Element[];
|
findModels(element: Element, expression: string, opt_exactMatch?: boolean): Element[];
|
||||||
|
|
|
@ -11,6 +11,7 @@ export const $CONTROLLER = '$controller';
|
||||||
export const $DELEGATE = '$delegate';
|
export const $DELEGATE = '$delegate';
|
||||||
export const $HTTP_BACKEND = '$httpBackend';
|
export const $HTTP_BACKEND = '$httpBackend';
|
||||||
export const $INJECTOR = '$injector';
|
export const $INJECTOR = '$injector';
|
||||||
|
export const $INTERVAL = '$interval';
|
||||||
export const $PARSE = '$parse';
|
export const $PARSE = '$parse';
|
||||||
export const $PROVIDE = '$provide';
|
export const $PROVIDE = '$provide';
|
||||||
export const $ROOT_SCOPE = '$rootScope';
|
export const $ROOT_SCOPE = '$rootScope';
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import {Injector, NgModule, NgZone, Testability, ɵNOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR as NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR} from '@angular/core';
|
import {Injector, NgModule, NgZone, Testability, ɵNOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR as NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR} from '@angular/core';
|
||||||
|
|
||||||
import * as angular from '../common/angular1';
|
import * as angular from '../common/angular1';
|
||||||
import {$$TESTABILITY, $DELEGATE, $INJECTOR, $PROVIDE, INJECTOR_KEY, UPGRADE_MODULE_NAME} from '../common/constants';
|
import {$$TESTABILITY, $DELEGATE, $INJECTOR, $INTERVAL, $PROVIDE, INJECTOR_KEY, UPGRADE_MODULE_NAME} from '../common/constants';
|
||||||
import {controllerKey} from '../common/util';
|
import {controllerKey} from '../common/util';
|
||||||
|
|
||||||
import {angular1Providers, setTempInjectorRef} from './angular1_providers';
|
import {angular1Providers, setTempInjectorRef} from './angular1_providers';
|
||||||
|
@ -190,6 +190,33 @@ export class UpgradeModule {
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($injector.has($INTERVAL)) {
|
||||||
|
$provide.decorator($INTERVAL, [
|
||||||
|
$DELEGATE,
|
||||||
|
(intervalDelegate: angular.IIntervalService) => {
|
||||||
|
// Wrap the $interval service so that setInterval is called outside NgZone,
|
||||||
|
// but the callback is still invoked within it. This is so that $interval
|
||||||
|
// won't block stability, which preserves the behavior from AngularJS.
|
||||||
|
let wrappedInterval =
|
||||||
|
(fn: Function, delay: number, count?: number, invokeApply?: boolean,
|
||||||
|
...pass: any[]) => {
|
||||||
|
return this.ngZone.runOutsideAngular(() => {
|
||||||
|
return intervalDelegate((...args: any[]) => {
|
||||||
|
// Run callback in the next VM turn - $interval calls
|
||||||
|
// $rootScope.$apply, and running the callback in NgZone will
|
||||||
|
// cause a '$digest already in progress' error if it's in the
|
||||||
|
// same vm turn.
|
||||||
|
setTimeout(() => { this.ngZone.run(() => fn(...args)); });
|
||||||
|
}, delay, count, invokeApply, ...pass);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
(wrappedInterval as any)['cancel'] = intervalDelegate.cancel;
|
||||||
|
return wrappedInterval;
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
|
@ -73,5 +73,42 @@ export function main() {
|
||||||
expect(ng2Stable).toEqual(true);
|
expect(ng2Stable).toEqual(true);
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should not wait for $interval', fakeAsync(() => {
|
||||||
|
const ng1Module = angular.module('ng1', []);
|
||||||
|
const element = html('<div></div>');
|
||||||
|
|
||||||
|
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
|
||||||
|
|
||||||
|
const ng2Testability: Testability = upgrade.injector.get(Testability);
|
||||||
|
const $interval: angular.IIntervalService = upgrade.$injector.get('$interval');
|
||||||
|
|
||||||
|
let ng2Stable = false;
|
||||||
|
let intervalDone = false;
|
||||||
|
|
||||||
|
const id = $interval((arg: string) => {
|
||||||
|
// should only be called once
|
||||||
|
expect(intervalDone).toEqual(false);
|
||||||
|
|
||||||
|
intervalDone = true;
|
||||||
|
expect(NgZone.isInAngularZone()).toEqual(true);
|
||||||
|
expect(arg).toEqual('passed argument');
|
||||||
|
}, 200, 0, true, 'passed argument');
|
||||||
|
|
||||||
|
ng2Testability.whenStable(() => { ng2Stable = true; });
|
||||||
|
|
||||||
|
tick(100);
|
||||||
|
|
||||||
|
expect(intervalDone).toEqual(false);
|
||||||
|
expect(ng2Stable).toEqual(true);
|
||||||
|
|
||||||
|
tick(200);
|
||||||
|
expect(intervalDone).toEqual(true);
|
||||||
|
expect($interval.cancel(id)).toEqual(true);
|
||||||
|
|
||||||
|
// Interval should not fire after cancel
|
||||||
|
tick(200);
|
||||||
|
});
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue