fix(upgrade): fix/improve support for lifecycle hooks (#13020)
With the exception of `$onChanges()`, all lifecycle hooks in ng1 are called on the controller, regardless if it is the binding destination or not (i.e. regardless of the value of `bindToController`). This change makes `upgrade` mimic that behavior when calling lifecycle hooks. Additionally, calling the `$onInit()` hook has been moved before calling the linking functions, which also mimics the ng1 behavior.
This commit is contained in:

committed by
Igor Minar

parent
018865ee6b
commit
21942a88f0
@ -12,6 +12,21 @@ import * as angular from './angular_js';
|
|||||||
import {NG1_COMPILE, NG1_CONTROLLER, NG1_HTTP_BACKEND, NG1_SCOPE, NG1_TEMPLATE_CACHE} from './constants';
|
import {NG1_COMPILE, NG1_CONTROLLER, NG1_HTTP_BACKEND, NG1_SCOPE, NG1_TEMPLATE_CACHE} from './constants';
|
||||||
import {controllerKey} from './util';
|
import {controllerKey} from './util';
|
||||||
|
|
||||||
|
interface IBindingDestination {
|
||||||
|
[key: string]: any;
|
||||||
|
$onChanges?: (changes: SimpleChanges) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IControllerInstance extends IBindingDestination {
|
||||||
|
$doCheck?: () => void;
|
||||||
|
$onDestroy?: () => void;
|
||||||
|
$onInit?: () => void;
|
||||||
|
$postLink?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type LifecycleHook = '$doCheck' | '$onChanges' | '$onDestroy' | '$onInit' | '$postLink';
|
||||||
|
|
||||||
|
|
||||||
const CAMEL_CASE = /([A-Z])/g;
|
const CAMEL_CASE = /([A-Z])/g;
|
||||||
const INITIAL_VALUE = {
|
const INITIAL_VALUE = {
|
||||||
__UNINITIALIZED__: true
|
__UNINITIALIZED__: true
|
||||||
@ -134,11 +149,11 @@ export class UpgradeNg1ComponentAdapterBuilder {
|
|||||||
httpBackend: angular.IHttpBackendService): Promise<angular.ILinkFn> {
|
httpBackend: angular.IHttpBackendService): Promise<angular.ILinkFn> {
|
||||||
if (this.directive.template !== undefined) {
|
if (this.directive.template !== undefined) {
|
||||||
this.linkFn = compileHtml(
|
this.linkFn = compileHtml(
|
||||||
typeof this.directive.template === 'function' ? this.directive.template() :
|
isFunction(this.directive.template) ? this.directive.template() :
|
||||||
this.directive.template);
|
this.directive.template);
|
||||||
} else if (this.directive.templateUrl) {
|
} else if (this.directive.templateUrl) {
|
||||||
const url = typeof this.directive.templateUrl === 'function' ? this.directive.templateUrl() :
|
const url = isFunction(this.directive.templateUrl) ? this.directive.templateUrl() :
|
||||||
this.directive.templateUrl;
|
this.directive.templateUrl;
|
||||||
const html = templateCache.get(url);
|
const html = templateCache.get(url);
|
||||||
if (html !== undefined) {
|
if (html !== undefined) {
|
||||||
this.linkFn = compileHtml(html);
|
this.linkFn = compileHtml(html);
|
||||||
@ -193,7 +208,8 @@ export class UpgradeNg1ComponentAdapterBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck {
|
class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck {
|
||||||
destinationObj: any = null;
|
private controllerInstance: IControllerInstance = null;
|
||||||
|
destinationObj: IBindingDestination = null;
|
||||||
checkLastValues: any[] = [];
|
checkLastValues: any[] = [];
|
||||||
componentScope: angular.IScope;
|
componentScope: angular.IScope;
|
||||||
element: Element;
|
element: Element;
|
||||||
@ -209,7 +225,8 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck {
|
|||||||
this.$element = angular.element(this.element);
|
this.$element = angular.element(this.element);
|
||||||
const controllerType = directive.controller;
|
const controllerType = directive.controller;
|
||||||
if (directive.bindToController && controllerType) {
|
if (directive.bindToController && controllerType) {
|
||||||
this.destinationObj = this.buildController(controllerType);
|
this.controllerInstance = this.buildController(controllerType);
|
||||||
|
this.destinationObj = this.controllerInstance;
|
||||||
} else {
|
} else {
|
||||||
this.destinationObj = this.componentScope;
|
this.destinationObj = this.componentScope;
|
||||||
}
|
}
|
||||||
@ -231,8 +248,13 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck {
|
|||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
if (!this.directive.bindToController && this.directive.controller) {
|
if (!this.directive.bindToController && this.directive.controller) {
|
||||||
this.buildController(this.directive.controller);
|
this.controllerInstance = this.buildController(this.directive.controller);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.controllerInstance && isFunction(this.controllerInstance.$onInit)) {
|
||||||
|
this.controllerInstance.$onInit();
|
||||||
|
}
|
||||||
|
|
||||||
let link = this.directive.link;
|
let link = this.directive.link;
|
||||||
if (typeof link == 'object') link = (<angular.IDirectivePrePost>link).pre;
|
if (typeof link == 'object') link = (<angular.IDirectivePrePost>link).pre;
|
||||||
if (link) {
|
if (link) {
|
||||||
@ -257,8 +279,9 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck {
|
|||||||
parentBoundTranscludeFn: (scope: any /** TODO #9100 */,
|
parentBoundTranscludeFn: (scope: any /** TODO #9100 */,
|
||||||
cloneAttach: any /** TODO #9100 */) => { cloneAttach(childNodes); }
|
cloneAttach: any /** TODO #9100 */) => { cloneAttach(childNodes); }
|
||||||
});
|
});
|
||||||
if (this.destinationObj.$onInit) {
|
|
||||||
this.destinationObj.$onInit();
|
if (this.controllerInstance && isFunction(this.controllerInstance.$postLink)) {
|
||||||
|
this.controllerInstance.$postLink();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,7 +292,8 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck {
|
|||||||
this.setComponentProperty(name, change.currentValue);
|
this.setComponentProperty(name, change.currentValue);
|
||||||
ng1Changes[this.propertyMap[name]] = change;
|
ng1Changes[this.propertyMap[name]] = change;
|
||||||
});
|
});
|
||||||
if (this.destinationObj.$onChanges) {
|
|
||||||
|
if (isFunction(this.destinationObj.$onChanges)) {
|
||||||
this.destinationObj.$onChanges(ng1Changes);
|
this.destinationObj.$onChanges(ng1Changes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -290,14 +314,15 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.destinationObj.$doCheck && this.directive.controller) {
|
|
||||||
this.destinationObj.$doCheck();
|
if (this.controllerInstance && isFunction(this.controllerInstance.$doCheck)) {
|
||||||
|
this.controllerInstance.$doCheck();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
if (this.destinationObj.$onDestroy && this.directive.controller) {
|
if (this.controllerInstance && isFunction(this.controllerInstance.$onDestroy)) {
|
||||||
this.destinationObj.$onDestroy();
|
this.controllerInstance.$onDestroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -353,3 +378,7 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck {
|
|||||||
`Directive '${this.directive.name}' require syntax unrecognized: ${this.directive.require}`);
|
`Directive '${this.directive.name}' require syntax unrecognized: ${this.directive.require}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isFunction(value: any): value is Function {
|
||||||
|
return typeof value === 'function';
|
||||||
|
}
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ChangeDetectorRef, Class, Component, EventEmitter, NO_ERRORS_SCHEMA, NgModule, SimpleChanges, Testability, destroyPlatform, forwardRef} from '@angular/core';
|
import {ChangeDetectorRef, Class, Component, EventEmitter, NO_ERRORS_SCHEMA, NgModule, SimpleChange, SimpleChanges, Testability, destroyPlatform, forwardRef} from '@angular/core';
|
||||||
import {async, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing';
|
import {async, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing';
|
||||||
import {BrowserModule} from '@angular/platform-browser';
|
import {BrowserModule} from '@angular/platform-browser';
|
||||||
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||||
@ -900,167 +900,560 @@ export function main() {
|
|||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should call $onInit of components', async(() => {
|
describe('with lifecycle hooks', () => {
|
||||||
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
|
it('should call `$onInit()` on controller', async(() => {
|
||||||
const $onInitSpy = jasmine.createSpy('$onInit');
|
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
|
||||||
|
const $onInitSpyA = jasmine.createSpy('$onInitA');
|
||||||
|
const $onInitSpyB = jasmine.createSpy('$onInitB');
|
||||||
|
|
||||||
@Component({selector: 'ng2', template: '<ng1></ng1>'})
|
@Component({selector: 'ng2', template: '<ng1-a></ng1-a> | <ng1-b></ng1-b>'})
|
||||||
class Ng2Component {
|
class Ng2Component {
|
||||||
}
|
|
||||||
|
|
||||||
angular.module('ng1', [])
|
|
||||||
.component('ng1', {
|
|
||||||
bindings: {},
|
|
||||||
template: '',
|
|
||||||
controller: function() { this.$onInit = $onInitSpy; }
|
|
||||||
})
|
|
||||||
.directive('ng2', adapter.downgradeNg2Component(Ng2Component));
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [adapter.upgradeNg1Component('ng1'), Ng2Component],
|
|
||||||
imports: [BrowserModule],
|
|
||||||
})
|
|
||||||
class Ng2Module {
|
|
||||||
}
|
|
||||||
|
|
||||||
const element = html(`<div><ng2></ng2></div>`);
|
|
||||||
adapter.bootstrap(element, ['ng1']).ready((ref) => {
|
|
||||||
expect($onInitSpy).toHaveBeenCalled();
|
|
||||||
ref.dispose();
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should call $doCheck of components', async(() => {
|
|
||||||
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
|
|
||||||
const $doCheckSpy = jasmine.createSpy('$doCheck');
|
|
||||||
let changeDetector: ChangeDetectorRef;
|
|
||||||
|
|
||||||
@Component({selector: 'ng2', template: '<ng1></ng1>'})
|
|
||||||
class Ng2Component {
|
|
||||||
constructor(cd: ChangeDetectorRef) { changeDetector = cd; }
|
|
||||||
}
|
|
||||||
|
|
||||||
angular.module('ng1', [])
|
|
||||||
.component('ng1', {
|
|
||||||
bindings: {},
|
|
||||||
template: '{{$ctrl.value}}',
|
|
||||||
controller: function() { this.$doCheck = $doCheckSpy; }
|
|
||||||
})
|
|
||||||
.directive('ng2', adapter.downgradeNg2Component(Ng2Component));
|
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [adapter.upgradeNg1Component('ng1'), Ng2Component],
|
|
||||||
imports: [BrowserModule],
|
|
||||||
})
|
|
||||||
class Ng2Module {
|
|
||||||
}
|
|
||||||
|
|
||||||
const element = html(`<div><ng2></ng2></div>`);
|
|
||||||
adapter.bootstrap(element, ['ng1']).ready((ref) => {
|
|
||||||
expect($doCheckSpy).toHaveBeenCalled();
|
|
||||||
|
|
||||||
$doCheckSpy.calls.reset();
|
|
||||||
changeDetector.detectChanges();
|
|
||||||
|
|
||||||
expect($doCheckSpy).toHaveBeenCalled();
|
|
||||||
|
|
||||||
ref.dispose();
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should call $onChanges of components', fakeAsync(() => {
|
|
||||||
const EXPECTED_VALUE = '$onChanges called';
|
|
||||||
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
|
|
||||||
const $onChangesSpy = jasmine.createSpy('$onChanges');
|
|
||||||
let ng2Instance: any;
|
|
||||||
|
|
||||||
@Component({selector: 'ng2', template: '<ng1 [val]="val"></ng1>'})
|
|
||||||
class Ng2Component {
|
|
||||||
constructor() { ng2Instance = this; }
|
|
||||||
}
|
|
||||||
|
|
||||||
angular.module('ng1Module', [])
|
|
||||||
.component('ng1', {
|
|
||||||
bindings: {val: '<'},
|
|
||||||
template: '',
|
|
||||||
controller: function() { this.$onChanges = $onChangesSpy; }
|
|
||||||
})
|
|
||||||
.directive('ng2', adapter.downgradeNg2Component(Ng2Component));
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [adapter.upgradeNg1Component('ng1'), Ng2Component],
|
|
||||||
imports: [BrowserModule],
|
|
||||||
})
|
|
||||||
class Ng2Module {
|
|
||||||
}
|
|
||||||
|
|
||||||
const element = html(`<div><ng2></ng2></div>`);
|
|
||||||
adapter.bootstrap(element, ['ng1Module']).ready((ref) => {
|
|
||||||
|
|
||||||
ng2Instance.val = EXPECTED_VALUE;
|
|
||||||
tick();
|
|
||||||
ref.ng1RootScope.$digest();
|
|
||||||
|
|
||||||
expect($onChangesSpy).toHaveBeenCalled();
|
|
||||||
const changes = $onChangesSpy.calls.mostRecent().args[0] as SimpleChanges;
|
|
||||||
expect(changes['val'].currentValue).toEqual(EXPECTED_VALUE);
|
|
||||||
|
|
||||||
ref.dispose();
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should call $onDestroy of components', fakeAsync(() => {
|
|
||||||
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
|
|
||||||
const $onDestroySpy = jasmine.createSpy('$onDestroy');
|
|
||||||
|
|
||||||
@Component({selector: 'ng2', template: '<ng1></ng1>'})
|
|
||||||
class Ng2Component {
|
|
||||||
}
|
|
||||||
|
|
||||||
angular.module('ng1', [])
|
|
||||||
.component('ng1', {
|
|
||||||
bindings: {},
|
|
||||||
template: '<div>ng1</div>',
|
|
||||||
controller: function() { this.$onDestroy = $onDestroySpy; }
|
|
||||||
})
|
|
||||||
.directive('ng2', adapter.downgradeNg2Component(Ng2Component));
|
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [adapter.upgradeNg1Component('ng1'), Ng2Component],
|
|
||||||
imports: [BrowserModule],
|
|
||||||
})
|
|
||||||
class Ng2Module {
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const element = html(`<div ng-if="!destroy"><ng2></ng2></div>`);
|
|
||||||
adapter.bootstrap(element, ['ng1']).ready((ref) => {
|
|
||||||
const $rootScope = ref.ng1RootScope as any;
|
|
||||||
|
|
||||||
$rootScope.destroy = false;
|
|
||||||
tick();
|
|
||||||
$rootScope.$digest();
|
|
||||||
|
|
||||||
expect($onDestroySpy).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
$rootScope.destroy = true;
|
|
||||||
tick();
|
|
||||||
$rootScope.$digest();
|
|
||||||
|
|
||||||
expect($onDestroySpy).toHaveBeenCalled();
|
|
||||||
|
|
||||||
ref.dispose();
|
|
||||||
|
|
||||||
if (!(global as any)['requestAnimationFrame']) {
|
|
||||||
// Needed for browser which don't support RAF and use a 16.6 setTimeout instead in
|
|
||||||
// ng1's AnimateRunner.
|
|
||||||
// This setTimeout remains at the end of the test and needs to be discarded.
|
|
||||||
tick(20);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}));
|
angular.module('ng1', [])
|
||||||
|
.directive('ng1A', () => ({
|
||||||
|
template: '',
|
||||||
|
scope: {},
|
||||||
|
bindToController: true,
|
||||||
|
controllerAs: '$ctrl',
|
||||||
|
controller: class {$onInit() { $onInitSpyA(); }}
|
||||||
|
}))
|
||||||
|
.directive('ng1B', () => ({
|
||||||
|
template: '',
|
||||||
|
scope: {},
|
||||||
|
bindToController: false,
|
||||||
|
controllerAs: '$ctrl',
|
||||||
|
controller: function() { this.$onInit = $onInitSpyB; }
|
||||||
|
}))
|
||||||
|
.directive('ng2', adapter.downgradeNg2Component(Ng2Component));
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
adapter.upgradeNg1Component('ng1A'), adapter.upgradeNg1Component('ng1B'),
|
||||||
|
Ng2Component
|
||||||
|
],
|
||||||
|
imports: [BrowserModule],
|
||||||
|
})
|
||||||
|
class Ng2Module {
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = html(`<div><ng2></ng2></div>`);
|
||||||
|
adapter.bootstrap(element, ['ng1']).ready((ref) => {
|
||||||
|
expect($onInitSpyA).toHaveBeenCalled();
|
||||||
|
expect($onInitSpyB).toHaveBeenCalled();
|
||||||
|
|
||||||
|
ref.dispose();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not call `$onInit()` on scope', async(() => {
|
||||||
|
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
|
||||||
|
const $onInitSpy = jasmine.createSpy('$onInit');
|
||||||
|
|
||||||
|
@Component({selector: 'ng2', template: '<ng1-a></ng1-a> | <ng1-b></ng1-b>'})
|
||||||
|
class Ng2Component {
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module('ng1', [])
|
||||||
|
.directive('ng1A', () => ({
|
||||||
|
template: '',
|
||||||
|
scope: {},
|
||||||
|
bindToController: true,
|
||||||
|
controllerAs: '$ctrl',
|
||||||
|
controller: function($scope: angular.IScope) {
|
||||||
|
Object.getPrototypeOf($scope).$onInit = $onInitSpy;
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.directive('ng1B', () => ({
|
||||||
|
template: '',
|
||||||
|
scope: {},
|
||||||
|
bindToController: false,
|
||||||
|
controllerAs: '$ctrl',
|
||||||
|
controller: function($scope: angular.IScope) {
|
||||||
|
$scope['$onInit'] = $onInitSpy;
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.directive('ng2', adapter.downgradeNg2Component(Ng2Component));
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
adapter.upgradeNg1Component('ng1A'), adapter.upgradeNg1Component('ng1B'),
|
||||||
|
Ng2Component
|
||||||
|
],
|
||||||
|
imports: [BrowserModule],
|
||||||
|
})
|
||||||
|
class Ng2Module {
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = html(`<div><ng2></ng2></div>`);
|
||||||
|
adapter.bootstrap(element, ['ng1']).ready((ref) => {
|
||||||
|
expect($onInitSpy).not.toHaveBeenCalled();
|
||||||
|
ref.dispose();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should call `$doCheck()` on controller', async(() => {
|
||||||
|
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
|
||||||
|
const $doCheckSpyA = jasmine.createSpy('$doCheckA');
|
||||||
|
const $doCheckSpyB = jasmine.createSpy('$doCheckB');
|
||||||
|
let changeDetector: ChangeDetectorRef;
|
||||||
|
|
||||||
|
@Component({selector: 'ng2', template: '<ng1-a></ng1-a> | <ng1-b></ng1-b>'})
|
||||||
|
class Ng2Component {
|
||||||
|
constructor(cd: ChangeDetectorRef) { changeDetector = cd; }
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module('ng1', [])
|
||||||
|
.directive('ng1A', () => ({
|
||||||
|
template: '',
|
||||||
|
scope: {},
|
||||||
|
bindToController: true,
|
||||||
|
controllerAs: '$ctrl',
|
||||||
|
controller: class {$doCheck() { $doCheckSpyA(); }}
|
||||||
|
}))
|
||||||
|
.directive('ng1B', () => ({
|
||||||
|
template: '',
|
||||||
|
scope: {},
|
||||||
|
bindToController: false,
|
||||||
|
controllerAs: '$ctrl',
|
||||||
|
controller: function() { this.$doCheck = $doCheckSpyB; }
|
||||||
|
}))
|
||||||
|
.directive('ng2', adapter.downgradeNg2Component(Ng2Component));
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
adapter.upgradeNg1Component('ng1A'), adapter.upgradeNg1Component('ng1B'),
|
||||||
|
Ng2Component
|
||||||
|
],
|
||||||
|
imports: [BrowserModule],
|
||||||
|
})
|
||||||
|
class Ng2Module {
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = html(`<div><ng2></ng2></div>`);
|
||||||
|
adapter.bootstrap(element, ['ng1']).ready((ref) => {
|
||||||
|
expect($doCheckSpyA).toHaveBeenCalled();
|
||||||
|
expect($doCheckSpyB).toHaveBeenCalled();
|
||||||
|
|
||||||
|
$doCheckSpyA.calls.reset();
|
||||||
|
$doCheckSpyB.calls.reset();
|
||||||
|
changeDetector.detectChanges();
|
||||||
|
|
||||||
|
expect($doCheckSpyA).toHaveBeenCalled();
|
||||||
|
expect($doCheckSpyB).toHaveBeenCalled();
|
||||||
|
|
||||||
|
ref.dispose();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not call `$doCheck()` on scope', async(() => {
|
||||||
|
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
|
||||||
|
const $doCheckSpyA = jasmine.createSpy('$doCheckA');
|
||||||
|
const $doCheckSpyB = jasmine.createSpy('$doCheckB');
|
||||||
|
let changeDetector: ChangeDetectorRef;
|
||||||
|
|
||||||
|
@Component({selector: 'ng2', template: '<ng1-a></ng1-a> | <ng1-b></ng1-b>'})
|
||||||
|
class Ng2Component {
|
||||||
|
constructor(cd: ChangeDetectorRef) { changeDetector = cd; }
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module('ng1', [])
|
||||||
|
.directive('ng1A', () => ({
|
||||||
|
template: '',
|
||||||
|
scope: {},
|
||||||
|
bindToController: true,
|
||||||
|
controllerAs: '$ctrl',
|
||||||
|
controller: function($scope: angular.IScope) {
|
||||||
|
Object.getPrototypeOf($scope).$doCheck = $doCheckSpyA;
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.directive('ng1B', () => ({
|
||||||
|
template: '',
|
||||||
|
scope: {},
|
||||||
|
bindToController: false,
|
||||||
|
controllerAs: '$ctrl',
|
||||||
|
controller: function($scope: angular.IScope) {
|
||||||
|
$scope['$doCheck'] = $doCheckSpyB;
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.directive('ng2', adapter.downgradeNg2Component(Ng2Component));
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
adapter.upgradeNg1Component('ng1A'), adapter.upgradeNg1Component('ng1B'),
|
||||||
|
Ng2Component
|
||||||
|
],
|
||||||
|
imports: [BrowserModule],
|
||||||
|
})
|
||||||
|
class Ng2Module {
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = html(`<div><ng2></ng2></div>`);
|
||||||
|
adapter.bootstrap(element, ['ng1']).ready((ref) => {
|
||||||
|
$doCheckSpyA.calls.reset();
|
||||||
|
$doCheckSpyB.calls.reset();
|
||||||
|
changeDetector.detectChanges();
|
||||||
|
|
||||||
|
expect($doCheckSpyA).not.toHaveBeenCalled();
|
||||||
|
expect($doCheckSpyB).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
ref.dispose();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should call `$postLink()` on controller', async(() => {
|
||||||
|
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
|
||||||
|
const $postLinkSpyA = jasmine.createSpy('$postLinkA');
|
||||||
|
const $postLinkSpyB = jasmine.createSpy('$postLinkB');
|
||||||
|
|
||||||
|
@Component({selector: 'ng2', template: '<ng1-a></ng1-a> | <ng1-b></ng1-b>'})
|
||||||
|
class Ng2Component {
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module('ng1', [])
|
||||||
|
.directive('ng1A', () => ({
|
||||||
|
template: '',
|
||||||
|
scope: {},
|
||||||
|
bindToController: true,
|
||||||
|
controllerAs: '$ctrl',
|
||||||
|
controller: class {$postLink() { $postLinkSpyA(); }}
|
||||||
|
}))
|
||||||
|
.directive('ng1B', () => ({
|
||||||
|
template: '',
|
||||||
|
scope: {},
|
||||||
|
bindToController: false,
|
||||||
|
controllerAs: '$ctrl',
|
||||||
|
controller: function() { this.$postLink = $postLinkSpyB; }
|
||||||
|
}))
|
||||||
|
.directive('ng2', adapter.downgradeNg2Component(Ng2Component));
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
adapter.upgradeNg1Component('ng1A'), adapter.upgradeNg1Component('ng1B'),
|
||||||
|
Ng2Component
|
||||||
|
],
|
||||||
|
imports: [BrowserModule],
|
||||||
|
})
|
||||||
|
class Ng2Module {
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = html(`<div><ng2></ng2></div>`);
|
||||||
|
adapter.bootstrap(element, ['ng1']).ready((ref) => {
|
||||||
|
expect($postLinkSpyA).toHaveBeenCalled();
|
||||||
|
expect($postLinkSpyB).toHaveBeenCalled();
|
||||||
|
|
||||||
|
ref.dispose();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not call `$postLink()` on scope', async(() => {
|
||||||
|
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
|
||||||
|
const $postLinkSpy = jasmine.createSpy('$postLink');
|
||||||
|
|
||||||
|
@Component({selector: 'ng2', template: '<ng1-a></ng1-a> | <ng1-b></ng1-b>'})
|
||||||
|
class Ng2Component {
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module('ng1', [])
|
||||||
|
.directive('ng1A', () => ({
|
||||||
|
template: '',
|
||||||
|
scope: {},
|
||||||
|
bindToController: true,
|
||||||
|
controllerAs: '$ctrl',
|
||||||
|
controller: function($scope: angular.IScope) {
|
||||||
|
Object.getPrototypeOf($scope).$postLink = $postLinkSpy;
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.directive('ng1B', () => ({
|
||||||
|
template: '',
|
||||||
|
scope: {},
|
||||||
|
bindToController: false,
|
||||||
|
controllerAs: '$ctrl',
|
||||||
|
controller: function($scope: angular.IScope) {
|
||||||
|
$scope['$postLink'] = $postLinkSpy;
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.directive('ng2', adapter.downgradeNg2Component(Ng2Component));
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
adapter.upgradeNg1Component('ng1A'), adapter.upgradeNg1Component('ng1B'),
|
||||||
|
Ng2Component
|
||||||
|
],
|
||||||
|
imports: [BrowserModule],
|
||||||
|
})
|
||||||
|
class Ng2Module {
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = html(`<div><ng2></ng2></div>`);
|
||||||
|
adapter.bootstrap(element, ['ng1']).ready((ref) => {
|
||||||
|
expect($postLinkSpy).not.toHaveBeenCalled();
|
||||||
|
ref.dispose();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should call `$onChanges()` on binding destination', fakeAsync(() => {
|
||||||
|
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
|
||||||
|
const $onChangesControllerSpyA = jasmine.createSpy('$onChangesControllerA');
|
||||||
|
const $onChangesControllerSpyB = jasmine.createSpy('$onChangesControllerB');
|
||||||
|
const $onChangesScopeSpy = jasmine.createSpy('$onChangesScope');
|
||||||
|
let ng2Instance: any;
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ng2',
|
||||||
|
template: '<ng1-a [valA]="val"></ng1-a> | <ng1-b [valB]="val"></ng1-b>'
|
||||||
|
})
|
||||||
|
class Ng2Component {
|
||||||
|
constructor() { ng2Instance = this; }
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module('ng1', [])
|
||||||
|
.directive('ng1A', () => ({
|
||||||
|
template: '',
|
||||||
|
scope: {valA: '<'},
|
||||||
|
bindToController: true,
|
||||||
|
controllerAs: '$ctrl',
|
||||||
|
controller: function($scope: angular.IScope) {
|
||||||
|
this.$onChanges = $onChangesControllerSpyA;
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.directive(
|
||||||
|
'ng1B',
|
||||||
|
() => ({
|
||||||
|
template: '',
|
||||||
|
scope: {valB: '<'},
|
||||||
|
bindToController: false,
|
||||||
|
controllerAs: '$ctrl',
|
||||||
|
controller: class {
|
||||||
|
$onChanges(changes: SimpleChanges) { $onChangesControllerSpyB(changes); }
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.directive('ng2', adapter.downgradeNg2Component(Ng2Component))
|
||||||
|
.run(($rootScope: angular.IRootScopeService) => {
|
||||||
|
Object.getPrototypeOf($rootScope).$onChanges = $onChangesScopeSpy;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
adapter.upgradeNg1Component('ng1A'), adapter.upgradeNg1Component('ng1B'),
|
||||||
|
Ng2Component
|
||||||
|
],
|
||||||
|
imports: [BrowserModule],
|
||||||
|
})
|
||||||
|
class Ng2Module {
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = html(`<div><ng2></ng2></div>`);
|
||||||
|
adapter.bootstrap(element, ['ng1']).ready((ref) => {
|
||||||
|
// Initial `$onChanges()` call
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect($onChangesControllerSpyA.calls.count()).toBe(1);
|
||||||
|
expect($onChangesControllerSpyA.calls.argsFor(0)[0]).toEqual({
|
||||||
|
valA: jasmine.any(SimpleChange)
|
||||||
|
});
|
||||||
|
|
||||||
|
expect($onChangesControllerSpyB).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect($onChangesScopeSpy.calls.count()).toBe(1);
|
||||||
|
expect($onChangesScopeSpy.calls.argsFor(0)[0]).toEqual({
|
||||||
|
valB: jasmine.any(SimpleChange)
|
||||||
|
});
|
||||||
|
|
||||||
|
$onChangesControllerSpyA.calls.reset();
|
||||||
|
$onChangesControllerSpyB.calls.reset();
|
||||||
|
$onChangesScopeSpy.calls.reset();
|
||||||
|
|
||||||
|
// `$onChanges()` call after a change
|
||||||
|
ng2Instance.val = 'new value';
|
||||||
|
tick();
|
||||||
|
ref.ng1RootScope.$digest();
|
||||||
|
|
||||||
|
expect($onChangesControllerSpyA.calls.count()).toBe(1);
|
||||||
|
expect($onChangesControllerSpyA.calls.argsFor(0)[0]).toEqual({
|
||||||
|
valA: jasmine.objectContaining({currentValue: 'new value'})
|
||||||
|
});
|
||||||
|
|
||||||
|
expect($onChangesControllerSpyB).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect($onChangesScopeSpy.calls.count()).toBe(1);
|
||||||
|
expect($onChangesScopeSpy.calls.argsFor(0)[0]).toEqual({
|
||||||
|
valB: jasmine.objectContaining({currentValue: 'new value'})
|
||||||
|
});
|
||||||
|
|
||||||
|
ref.dispose();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should call `$onDestroy()` on controller', fakeAsync(() => {
|
||||||
|
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
|
||||||
|
const $onDestroySpyA = jasmine.createSpy('$onDestroyA');
|
||||||
|
const $onDestroySpyB = jasmine.createSpy('$onDestroyB');
|
||||||
|
let ng2ComponentInstance: Ng2Component;
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ng2',
|
||||||
|
template: `
|
||||||
|
<div *ngIf="!ng2Destroy">
|
||||||
|
<ng1-a></ng1-a> | <ng1-b></ng1-b>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
class Ng2Component {
|
||||||
|
ng2Destroy: boolean = false;
|
||||||
|
constructor() { ng2ComponentInstance = this; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// On browsers that don't support `requestAnimationFrame` (IE 9, Android <= 4.3),
|
||||||
|
// `$animate` will use `setTimeout(..., 16.6)` instead. This timeout will still be on
|
||||||
|
// the queue at the end of the test, causing it to fail.
|
||||||
|
// Mocking animations (via `ngAnimateMock`) avoids the issue.
|
||||||
|
angular.module('ng1', ['ngAnimateMock'])
|
||||||
|
.directive('ng1A', () => ({
|
||||||
|
template: '',
|
||||||
|
scope: {},
|
||||||
|
bindToController: true,
|
||||||
|
controllerAs: '$ctrl',
|
||||||
|
controller: class {$onDestroy() { $onDestroySpyA(); }}
|
||||||
|
}))
|
||||||
|
.directive('ng1B', () => ({
|
||||||
|
template: '',
|
||||||
|
scope: {},
|
||||||
|
bindToController: false,
|
||||||
|
controllerAs: '$ctrl',
|
||||||
|
controller: function() { this.$onDestroy = $onDestroySpyB; }
|
||||||
|
}))
|
||||||
|
.directive('ng2', adapter.downgradeNg2Component(Ng2Component));
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
adapter.upgradeNg1Component('ng1A'), adapter.upgradeNg1Component('ng1B'),
|
||||||
|
Ng2Component
|
||||||
|
],
|
||||||
|
imports: [BrowserModule],
|
||||||
|
})
|
||||||
|
class Ng2Module {
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = html(`<div ng-if="!ng1Destroy"><ng2></ng2></div>`);
|
||||||
|
adapter.bootstrap(element, ['ng1']).ready((ref) => {
|
||||||
|
const $rootScope = ref.ng1RootScope as any;
|
||||||
|
|
||||||
|
$rootScope.ng1Destroy = false;
|
||||||
|
tick();
|
||||||
|
$rootScope.$digest();
|
||||||
|
|
||||||
|
expect($onDestroySpyA).not.toHaveBeenCalled();
|
||||||
|
expect($onDestroySpyB).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
$rootScope.ng1Destroy = true;
|
||||||
|
tick();
|
||||||
|
$rootScope.$digest();
|
||||||
|
|
||||||
|
expect($onDestroySpyA).toHaveBeenCalled();
|
||||||
|
expect($onDestroySpyB).toHaveBeenCalled();
|
||||||
|
|
||||||
|
$onDestroySpyA.calls.reset();
|
||||||
|
$onDestroySpyB.calls.reset();
|
||||||
|
|
||||||
|
$rootScope.ng1Destroy = false;
|
||||||
|
tick();
|
||||||
|
$rootScope.$digest();
|
||||||
|
|
||||||
|
expect($onDestroySpyA).not.toHaveBeenCalled();
|
||||||
|
expect($onDestroySpyB).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
ng2ComponentInstance.ng2Destroy = true;
|
||||||
|
tick();
|
||||||
|
$rootScope.$digest();
|
||||||
|
|
||||||
|
expect($onDestroySpyA).toHaveBeenCalled();
|
||||||
|
expect($onDestroySpyB).toHaveBeenCalled();
|
||||||
|
|
||||||
|
ref.dispose();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not call `$onDestroy()` on scope', fakeAsync(() => {
|
||||||
|
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
|
||||||
|
const $onDestroySpy = jasmine.createSpy('$onDestroy');
|
||||||
|
let ng2ComponentInstance: Ng2Component;
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ng2',
|
||||||
|
template: `
|
||||||
|
<div *ngIf="!ng2Destroy">
|
||||||
|
<ng1-a></ng1-a> | <ng1-b></ng1-b>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
class Ng2Component {
|
||||||
|
ng2Destroy: boolean = false;
|
||||||
|
constructor() { ng2ComponentInstance = this; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// On browsers that don't support `requestAnimationFrame` (IE 9, Android <= 4.3),
|
||||||
|
// `$animate` will use `setTimeout(..., 16.6)` instead. This timeout will still be on
|
||||||
|
// the queue at the end of the test, causing it to fail.
|
||||||
|
// Mocking animations (via `ngAnimateMock`) avoids the issue.
|
||||||
|
angular.module('ng1', ['ngAnimateMock'])
|
||||||
|
.directive('ng1A', () => ({
|
||||||
|
template: '',
|
||||||
|
scope: {},
|
||||||
|
bindToController: true,
|
||||||
|
controllerAs: '$ctrl',
|
||||||
|
controller: function($scope: angular.IScope) {
|
||||||
|
Object.getPrototypeOf($scope).$onDestroy = $onDestroySpy;
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.directive('ng1B', () => ({
|
||||||
|
template: '',
|
||||||
|
scope: {},
|
||||||
|
bindToController: false,
|
||||||
|
controllerAs: '$ctrl',
|
||||||
|
controller: function($scope: angular.IScope) {
|
||||||
|
$scope['$onDestroy'] = $onDestroySpy;
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.directive('ng2', adapter.downgradeNg2Component(Ng2Component));
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
adapter.upgradeNg1Component('ng1A'), adapter.upgradeNg1Component('ng1B'),
|
||||||
|
Ng2Component
|
||||||
|
],
|
||||||
|
imports: [BrowserModule],
|
||||||
|
})
|
||||||
|
class Ng2Module {
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = html(`<div ng-if="!ng1Destroy"><ng2></ng2></div>`);
|
||||||
|
adapter.bootstrap(element, ['ng1']).ready((ref) => {
|
||||||
|
const $rootScope = ref.ng1RootScope as any;
|
||||||
|
|
||||||
|
$rootScope.ng1Destroy = false;
|
||||||
|
tick();
|
||||||
|
$rootScope.$digest();
|
||||||
|
|
||||||
|
$rootScope.ng1Destroy = true;
|
||||||
|
tick();
|
||||||
|
$rootScope.$digest();
|
||||||
|
|
||||||
|
$rootScope.ng1Destroy = false;
|
||||||
|
tick();
|
||||||
|
$rootScope.$digest();
|
||||||
|
|
||||||
|
ng2ComponentInstance.ng2Destroy = true;
|
||||||
|
tick();
|
||||||
|
$rootScope.$digest();
|
||||||
|
|
||||||
|
expect($onDestroySpy).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
ref.dispose();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
it('should bind input properties (<) of components', async(() => {
|
it('should bind input properties (<) of components', async(() => {
|
||||||
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
|
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
|
||||||
|
Reference in New Issue
Block a user