feat(UpgradeComponent): add/improve support for lifecycle hooks
Add support for the `$postDigest()` and `$onDestroy()` lifecycle hooks. Better align the behavior of the `$onChanges()` and `$onInit()` lifecycle hooks with Angular 1.x: - Call `$onInit()` before pre-linking. - Always instantiate the controller before calling `$onChanges()`.
This commit is contained in:
parent
f0cdb428f5
commit
469010ea8e
|
@ -6,7 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {DoCheck, ElementRef, EventEmitter, Injector, OnChanges, OnInit, SimpleChanges} from '@angular/core';
|
||||
import {DoCheck, ElementRef, EventEmitter, Injector, OnChanges, OnDestroy, OnInit, SimpleChanges} from '@angular/core';
|
||||
|
||||
import * as angular from '../angular_js';
|
||||
import {looseIdentical} from '../facade/lang';
|
||||
|
@ -34,13 +34,17 @@ interface IBindingDestination {
|
|||
}
|
||||
|
||||
interface IControllerInstance extends IBindingDestination {
|
||||
$onDestroy?: () => void;
|
||||
$onInit?: () => void;
|
||||
$postLink?: () => void;
|
||||
}
|
||||
|
||||
type LifecycleHook = '$onChanges' | '$onDestroy' | '$onInit' | '$postLink';
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
export class UpgradeComponent implements OnInit, OnChanges, DoCheck {
|
||||
export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
|
||||
private $injector: angular.IInjectorService;
|
||||
private $compile: angular.ICompileService;
|
||||
private $templateCache: angular.ITemplateCacheService;
|
||||
|
@ -80,33 +84,27 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck {
|
|||
this.$componentScope = $parentScope.$new(!!this.directive.scope);
|
||||
|
||||
const controllerType = this.directive.controller;
|
||||
// QUESTION: shouldn't we be building the controller in any case?
|
||||
if (this.directive.bindToController) {
|
||||
if (controllerType) {
|
||||
this.bindingDestination = this.controllerInstance = this.buildController(
|
||||
controllerType, this.$componentScope, this.$element, this.directive.controllerAs);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Upgraded directive '${name}' specifies 'bindToController' but no controller.`);
|
||||
}
|
||||
} else {
|
||||
this.bindingDestination = this.$componentScope;
|
||||
const bindToController = this.directive.bindToController;
|
||||
if (controllerType) {
|
||||
this.controllerInstance = this.buildController(
|
||||
controllerType, this.$componentScope, this.$element, this.directive.controllerAs);
|
||||
} else if (bindToController) {
|
||||
throw new Error(
|
||||
`Upgraded directive '${name}' specifies 'bindToController' but no controller.`);
|
||||
}
|
||||
|
||||
this.bindingDestination = bindToController ? this.controllerInstance : this.$componentScope;
|
||||
|
||||
this.setupOutputs();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
// QUESTION: why not just use $compile instead of reproducing parts of it
|
||||
if (!this.directive.bindToController && this.directive.controller) {
|
||||
this.controllerInstance = this.buildController(
|
||||
this.directive.controller, this.$componentScope, this.$element,
|
||||
this.directive.controllerAs);
|
||||
}
|
||||
const attrs: angular.IAttributes = NOT_SUPPORTED;
|
||||
const transcludeFn: angular.ITranscludeFunction = NOT_SUPPORTED;
|
||||
const linkController = this.resolveRequired(this.$element, this.directive.require);
|
||||
|
||||
this.callLifecycleHook('$onInit', this.controllerInstance);
|
||||
|
||||
const link = this.directive.link;
|
||||
const preLink = (typeof link == 'object') && (link as angular.IDirectivePrePost).pre;
|
||||
const postLink = (typeof link == 'object') ? (link as angular.IDirectivePrePost).post : link;
|
||||
|
@ -131,19 +129,15 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck {
|
|||
postLink(this.$componentScope, this.$element, attrs, linkController, transcludeFn);
|
||||
}
|
||||
|
||||
if (this.controllerInstance && this.controllerInstance.$onInit) {
|
||||
this.controllerInstance.$onInit();
|
||||
}
|
||||
this.callLifecycleHook('$postLink', this.controllerInstance);
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
// Forward input changes to `bindingDestination`
|
||||
Object.keys(changes).forEach(
|
||||
propName => { this.bindingDestination[propName] = changes[propName].currentValue; });
|
||||
propName => this.bindingDestination[propName] = changes[propName].currentValue);
|
||||
|
||||
if (this.bindingDestination.$onChanges) {
|
||||
this.bindingDestination.$onChanges(changes);
|
||||
}
|
||||
this.callLifecycleHook('$onChanges', this.bindingDestination, changes);
|
||||
}
|
||||
|
||||
ngDoCheck() {
|
||||
|
@ -165,6 +159,17 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck {
|
|||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.callLifecycleHook('$onDestroy', this.controllerInstance);
|
||||
this.$componentScope.$destroy();
|
||||
}
|
||||
|
||||
private callLifecycleHook(method: LifecycleHook, context: IBindingDestination, arg?: any) {
|
||||
if (context && typeof context[method] === 'function') {
|
||||
context[method](arg);
|
||||
}
|
||||
}
|
||||
|
||||
private getDirective(name: string): angular.IDirective {
|
||||
const directives: angular.IDirective[] = this.$injector.get(name + 'Directive');
|
||||
if (directives.length > 1) {
|
||||
|
@ -254,6 +259,7 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck {
|
|||
private buildController(
|
||||
controllerType: angular.IController, $scope: angular.IScope,
|
||||
$element: angular.IAugmentedJQuery, controllerAs: string) {
|
||||
// TODO: Document that we do not pre-assign bindings on the controller instance
|
||||
var locals = {$scope, $element};
|
||||
var controller = this.$controller(controllerType, locals, null, controllerAs);
|
||||
$element.data(controllerKey(this.directive.name), controller);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Component, Directive, ElementRef, EventEmitter, Injector, Input, NO_ERRORS_SCHEMA, NgModule, Output, destroyPlatform} from '@angular/core';
|
||||
import {Component, Directive, ElementRef, EventEmitter, Injector, Input, NO_ERRORS_SCHEMA, NgModule, Output, SimpleChanges, destroyPlatform} from '@angular/core';
|
||||
import {async, fakeAsync, tick} from '@angular/core/testing';
|
||||
import {BrowserModule} from '@angular/platform-browser';
|
||||
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||
|
@ -1381,8 +1381,307 @@ export function main() {
|
|||
});
|
||||
|
||||
describe('lifecycle hooks', () => {
|
||||
xit('should call `$onChanges()` on controller', () => {});
|
||||
xit('should call `$onChanges()` on scope', () => {});
|
||||
it('should call `$onChanges()` on binding destination (prototype)', fakeAsync(() => {
|
||||
const scopeOnChanges = jasmine.createSpy('scopeOnChanges');
|
||||
const controllerOnChangesA = jasmine.createSpy('controllerOnChangesA');
|
||||
const controllerOnChangesB = jasmine.createSpy('controllerOnChangesB');
|
||||
let ng2ComponentInstance: Ng2Component;
|
||||
|
||||
// Define `ng1Directive`
|
||||
const ng1DirectiveA: angular.IDirective = {
|
||||
template: '',
|
||||
scope: {inputA: '<'},
|
||||
bindToController: false,
|
||||
controllerAs: '$ctrl',
|
||||
controller: class {
|
||||
$onChanges(changes: SimpleChanges) {
|
||||
controllerOnChangesA(changes);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ng1DirectiveB: angular.IDirective = {
|
||||
template: '',
|
||||
scope: {inputB: '<'},
|
||||
bindToController: true,
|
||||
controllerAs: '$ctrl',
|
||||
controller: class {
|
||||
constructor($scope: angular.IScope) {
|
||||
Object.getPrototypeOf($scope)['$onChanges'] = scopeOnChanges;
|
||||
}
|
||||
|
||||
$onChanges(changes: SimpleChanges) {
|
||||
controllerOnChangesB(changes);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Define `Ng1ComponentFacade`
|
||||
@Directive({selector: 'ng1A'})
|
||||
class Ng1ComponentAFacade extends UpgradeComponent {
|
||||
@Input() inputA: any;
|
||||
|
||||
constructor(elementRef: ElementRef, injector: Injector) {
|
||||
super('ng1A', elementRef, injector);
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({selector: 'ng1B'})
|
||||
class Ng1ComponentBFacade extends UpgradeComponent {
|
||||
@Input() inputB: any;
|
||||
|
||||
constructor(elementRef: ElementRef, injector: Injector) {
|
||||
super('ng1B', elementRef, injector);
|
||||
}
|
||||
}
|
||||
|
||||
// Define `Ng2Component`
|
||||
@Component({
|
||||
selector: 'ng2',
|
||||
template: '<ng1A [inputA]="data"></ng1A> | <ng1B [inputB]="data"></ng1B>'
|
||||
})
|
||||
class Ng2Component {
|
||||
data = {foo: 'bar'};
|
||||
|
||||
constructor() { ng2ComponentInstance = this; }
|
||||
}
|
||||
|
||||
// Define `ng1Module`
|
||||
const ng1Module = angular.module('ng1Module', [])
|
||||
.directive('ng1A', () => ng1DirectiveA)
|
||||
.directive('ng1B', () => ng1DirectiveB)
|
||||
.directive('ng2', downgradeComponent({component: Ng2Component}));
|
||||
|
||||
// Define `Ng2Module`
|
||||
@NgModule({
|
||||
declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component],
|
||||
entryComponents: [Ng2Component],
|
||||
imports: [BrowserModule, UpgradeModule]
|
||||
})
|
||||
class Ng2Module {
|
||||
ngDoBootstrap() {}
|
||||
}
|
||||
|
||||
// Bootstrap
|
||||
const element = html(`<ng2></ng2>`);
|
||||
|
||||
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
|
||||
// Initial change
|
||||
expect(scopeOnChanges.calls.count()).toBe(1);
|
||||
expect(controllerOnChangesA).not.toHaveBeenCalled();
|
||||
expect(controllerOnChangesB.calls.count()).toBe(1);
|
||||
|
||||
expect(scopeOnChanges.calls.argsFor(0)[0]).toEqual({inputA: jasmine.any(Object)});
|
||||
expect(scopeOnChanges.calls.argsFor(0)[0].inputA.currentValue).toEqual({foo: 'bar'});
|
||||
expect(scopeOnChanges.calls.argsFor(0)[0].inputA.isFirstChange()).toBe(true);
|
||||
expect(controllerOnChangesB.calls.argsFor(0)[0].inputB.currentValue).toEqual({
|
||||
foo: 'bar'
|
||||
});
|
||||
expect(controllerOnChangesB.calls.argsFor(0)[0].inputB.isFirstChange()).toBe(true);
|
||||
|
||||
// Change: Re-assign `data`
|
||||
ng2ComponentInstance.data = {foo: 'baz'};
|
||||
digest(adapter);
|
||||
tick();
|
||||
|
||||
expect(scopeOnChanges.calls.count()).toBe(2);
|
||||
expect(controllerOnChangesA).not.toHaveBeenCalled();
|
||||
expect(controllerOnChangesB.calls.count()).toBe(2);
|
||||
|
||||
expect(scopeOnChanges.calls.argsFor(1)[0]).toEqual({inputA: jasmine.any(Object)});
|
||||
expect(scopeOnChanges.calls.argsFor(1)[0].inputA.previousValue).toEqual({foo: 'bar'});
|
||||
expect(scopeOnChanges.calls.argsFor(1)[0].inputA.currentValue).toEqual({foo: 'baz'});
|
||||
expect(scopeOnChanges.calls.argsFor(1)[0].inputA.isFirstChange()).toBe(false);
|
||||
expect(controllerOnChangesB.calls.argsFor(1)[0].inputB.previousValue).toEqual({
|
||||
foo: 'bar'
|
||||
});
|
||||
expect(controllerOnChangesB.calls.argsFor(1)[0].inputB.currentValue).toEqual({
|
||||
foo: 'baz'
|
||||
});
|
||||
expect(controllerOnChangesB.calls.argsFor(1)[0].inputB.isFirstChange()).toBe(false);
|
||||
|
||||
// No change: Update internal property
|
||||
ng2ComponentInstance.data.foo = 'qux';
|
||||
digest(adapter);
|
||||
tick();
|
||||
|
||||
expect(scopeOnChanges.calls.count()).toBe(2);
|
||||
expect(controllerOnChangesA).not.toHaveBeenCalled();
|
||||
expect(controllerOnChangesB.calls.count()).toBe(2);
|
||||
|
||||
// Change: Re-assign `data` (even if it looks the same)
|
||||
ng2ComponentInstance.data = {foo: 'qux'};
|
||||
digest(adapter);
|
||||
tick();
|
||||
|
||||
expect(scopeOnChanges.calls.count()).toBe(3);
|
||||
expect(controllerOnChangesA).not.toHaveBeenCalled();
|
||||
expect(controllerOnChangesB.calls.count()).toBe(3);
|
||||
|
||||
expect(scopeOnChanges.calls.argsFor(2)[0]).toEqual({inputA: jasmine.any(Object)});
|
||||
expect(scopeOnChanges.calls.argsFor(2)[0].inputA.previousValue).toEqual({foo: 'qux'});
|
||||
expect(scopeOnChanges.calls.argsFor(2)[0].inputA.currentValue).toEqual({foo: 'qux'});
|
||||
expect(scopeOnChanges.calls.argsFor(2)[0].inputA.isFirstChange()).toBe(false);
|
||||
expect(controllerOnChangesB.calls.argsFor(2)[0].inputB.previousValue).toEqual({
|
||||
foo: 'qux'
|
||||
});
|
||||
expect(controllerOnChangesB.calls.argsFor(2)[0].inputB.currentValue).toEqual({
|
||||
foo: 'qux'
|
||||
});
|
||||
expect(controllerOnChangesB.calls.argsFor(2)[0].inputB.isFirstChange()).toBe(false);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should call `$onChanges()` on binding destination (instance)', fakeAsync(() => {
|
||||
const scopeOnChangesA = jasmine.createSpy('scopeOnChangesA');
|
||||
const scopeOnChangesB = jasmine.createSpy('scopeOnChangesB');
|
||||
const controllerOnChangesA = jasmine.createSpy('controllerOnChangesA');
|
||||
const controllerOnChangesB = jasmine.createSpy('controllerOnChangesB');
|
||||
let ng2ComponentInstance: Ng2Component;
|
||||
|
||||
// Define `ng1Directive`
|
||||
const ng1DirectiveA: angular.IDirective = {
|
||||
template: '',
|
||||
scope: {inputA: '<'},
|
||||
bindToController: false,
|
||||
controllerAs: '$ctrl',
|
||||
controller: class {
|
||||
constructor($scope: angular.IScope) {
|
||||
$scope['$onChanges'] = scopeOnChangesA;
|
||||
(this as any).$onChanges = controllerOnChangesA;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ng1DirectiveB: angular.IDirective = {
|
||||
template: '',
|
||||
scope: {inputB: '<'},
|
||||
bindToController: true,
|
||||
controllerAs: '$ctrl',
|
||||
controller: class {
|
||||
constructor($scope: angular.IScope) {
|
||||
$scope['$onChanges'] = scopeOnChangesB;
|
||||
(this as any).$onChanges = controllerOnChangesB;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Define `Ng1ComponentFacade`
|
||||
@Directive({selector: 'ng1A'})
|
||||
class Ng1ComponentAFacade extends UpgradeComponent {
|
||||
@Input() inputA: any;
|
||||
|
||||
constructor(elementRef: ElementRef, injector: Injector) {
|
||||
super('ng1A', elementRef, injector);
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({selector: 'ng1B'})
|
||||
class Ng1ComponentBFacade extends UpgradeComponent {
|
||||
@Input() inputB: any;
|
||||
|
||||
constructor(elementRef: ElementRef, injector: Injector) {
|
||||
super('ng1B', elementRef, injector);
|
||||
}
|
||||
}
|
||||
|
||||
// Define `Ng2Component`
|
||||
@Component({
|
||||
selector: 'ng2',
|
||||
template: '<ng1A [inputA]="data"></ng1A> | <ng1B [inputB]="data"></ng1B>'
|
||||
})
|
||||
class Ng2Component {
|
||||
data = {foo: 'bar'};
|
||||
|
||||
constructor() { ng2ComponentInstance = this; }
|
||||
}
|
||||
|
||||
// Define `ng1Module`
|
||||
const ng1Module = angular.module('ng1Module', [])
|
||||
.directive('ng1A', () => ng1DirectiveA)
|
||||
.directive('ng1B', () => ng1DirectiveB)
|
||||
.directive('ng2', downgradeComponent({component: Ng2Component}));
|
||||
|
||||
// Define `Ng2Module`
|
||||
@NgModule({
|
||||
declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component],
|
||||
entryComponents: [Ng2Component],
|
||||
imports: [BrowserModule, UpgradeModule]
|
||||
})
|
||||
class Ng2Module {
|
||||
ngDoBootstrap() {}
|
||||
}
|
||||
|
||||
// Bootstrap
|
||||
const element = html(`<ng2></ng2>`);
|
||||
|
||||
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
|
||||
// Initial change
|
||||
expect(scopeOnChangesA.calls.count()).toBe(1);
|
||||
expect(scopeOnChangesB).not.toHaveBeenCalled();
|
||||
expect(controllerOnChangesA).not.toHaveBeenCalled();
|
||||
expect(controllerOnChangesB.calls.count()).toBe(1);
|
||||
|
||||
expect(scopeOnChangesA.calls.argsFor(0)[0].inputA.currentValue).toEqual({foo: 'bar'});
|
||||
expect(scopeOnChangesA.calls.argsFor(0)[0].inputA.isFirstChange()).toBe(true);
|
||||
expect(controllerOnChangesB.calls.argsFor(0)[0].inputB.currentValue).toEqual({
|
||||
foo: 'bar'
|
||||
});
|
||||
expect(controllerOnChangesB.calls.argsFor(0)[0].inputB.isFirstChange()).toBe(true);
|
||||
|
||||
// Change: Re-assign `data`
|
||||
ng2ComponentInstance.data = {foo: 'baz'};
|
||||
digest(adapter);
|
||||
tick();
|
||||
|
||||
expect(scopeOnChangesA.calls.count()).toBe(2);
|
||||
expect(scopeOnChangesB).not.toHaveBeenCalled();
|
||||
expect(controllerOnChangesA).not.toHaveBeenCalled();
|
||||
expect(controllerOnChangesB.calls.count()).toBe(2);
|
||||
|
||||
expect(scopeOnChangesA.calls.argsFor(1)[0].inputA.previousValue).toEqual({foo: 'bar'});
|
||||
expect(scopeOnChangesA.calls.argsFor(1)[0].inputA.currentValue).toEqual({foo: 'baz'});
|
||||
expect(scopeOnChangesA.calls.argsFor(1)[0].inputA.isFirstChange()).toBe(false);
|
||||
expect(controllerOnChangesB.calls.argsFor(1)[0].inputB.previousValue).toEqual({
|
||||
foo: 'bar'
|
||||
});
|
||||
expect(controllerOnChangesB.calls.argsFor(1)[0].inputB.currentValue).toEqual({
|
||||
foo: 'baz'
|
||||
});
|
||||
expect(controllerOnChangesB.calls.argsFor(1)[0].inputB.isFirstChange()).toBe(false);
|
||||
|
||||
// No change: Update internal property
|
||||
ng2ComponentInstance.data.foo = 'qux';
|
||||
digest(adapter);
|
||||
tick();
|
||||
|
||||
expect(scopeOnChangesA.calls.count()).toBe(2);
|
||||
expect(scopeOnChangesB).not.toHaveBeenCalled();
|
||||
expect(controllerOnChangesA).not.toHaveBeenCalled();
|
||||
expect(controllerOnChangesB.calls.count()).toBe(2);
|
||||
|
||||
// Change: Re-assign `data` (even if it looks the same)
|
||||
ng2ComponentInstance.data = {foo: 'qux'};
|
||||
digest(adapter);
|
||||
tick();
|
||||
|
||||
expect(scopeOnChangesA.calls.count()).toBe(3);
|
||||
expect(scopeOnChangesB).not.toHaveBeenCalled();
|
||||
expect(controllerOnChangesA).not.toHaveBeenCalled();
|
||||
expect(controllerOnChangesB.calls.count()).toBe(3);
|
||||
|
||||
expect(scopeOnChangesA.calls.argsFor(2)[0].inputA.previousValue).toEqual({foo: 'qux'});
|
||||
expect(scopeOnChangesA.calls.argsFor(2)[0].inputA.currentValue).toEqual({foo: 'qux'});
|
||||
expect(scopeOnChangesA.calls.argsFor(2)[0].inputA.isFirstChange()).toBe(false);
|
||||
expect(controllerOnChangesB.calls.argsFor(2)[0].inputB.previousValue).toEqual({
|
||||
foo: 'qux'
|
||||
});
|
||||
expect(controllerOnChangesB.calls.argsFor(2)[0].inputB.currentValue).toEqual({
|
||||
foo: 'qux'
|
||||
});
|
||||
expect(controllerOnChangesB.calls.argsFor(2)[0].inputB.isFirstChange()).toBe(false);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should call `$onInit()` on controller', async(() => {
|
||||
// Define `ng1Directive`
|
||||
|
@ -1400,9 +1699,10 @@ export function main() {
|
|||
template: 'Called: {{ called }}',
|
||||
bindToController: true,
|
||||
controller: class {
|
||||
constructor(private $scope: angular.IScope) { $scope['called'] = 'no'; }
|
||||
|
||||
$onInit() { this.$scope['called'] = 'yes'; }
|
||||
constructor($scope: angular.IScope) {
|
||||
$scope['called'] = 'no';
|
||||
(this as any)['$onInit'] = () => $scope['called'] = 'yes';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1445,11 +1745,8 @@ export function main() {
|
|||
// Bootstrap
|
||||
const element = html(`<ng2></ng2>`);
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(Ng2Module).then(ref => {
|
||||
var adapter = ref.injector.get(UpgradeModule) as UpgradeModule;
|
||||
adapter.bootstrap(element, [ng1Module.name]);
|
||||
|
||||
expect(multiTrim(document.body.textContent)).toBe('Called: yes | Called: yes');
|
||||
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
|
||||
expect(multiTrim(element.textContent)).toBe('Called: yes | Called: yes');
|
||||
});
|
||||
}));
|
||||
|
||||
|
@ -1462,6 +1759,7 @@ export function main() {
|
|||
constructor($scope: angular.IScope) {
|
||||
$scope['called'] = 'no';
|
||||
$scope['$onInit'] = () => $scope['called'] = 'yes';
|
||||
Object.getPrototypeOf($scope)['$onInit'] = () => $scope['called'] = 'yes';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1473,6 +1771,7 @@ export function main() {
|
|||
constructor($scope: angular.IScope) {
|
||||
$scope['called'] = 'no';
|
||||
$scope['$onInit'] = () => $scope['called'] = 'yes';
|
||||
Object.getPrototypeOf($scope)['$onInit'] = () => $scope['called'] = 'yes';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1516,21 +1815,461 @@ export function main() {
|
|||
// Bootstrap
|
||||
const element = html(`<ng2></ng2>`);
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(Ng2Module).then(ref => {
|
||||
var adapter = ref.injector.get(UpgradeModule) as UpgradeModule;
|
||||
adapter.bootstrap(element, [ng1Module.name]);
|
||||
|
||||
expect(multiTrim(document.body.textContent)).toBe('Called: no | Called: no');
|
||||
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
|
||||
expect(multiTrim(element.textContent)).toBe('Called: no | Called: no');
|
||||
});
|
||||
}));
|
||||
|
||||
xit('should call `$onPostDigest()` on controller', () => {});
|
||||
xit('should not call `$onPostDigest()` on scope', () => {});
|
||||
it('should call `$postLink()` on controller', async(() => {
|
||||
// Define `ng1Directive`
|
||||
const ng1DirectiveA: angular.IDirective = {
|
||||
template: 'Called: {{ called }}',
|
||||
bindToController: false,
|
||||
controller: class {
|
||||
constructor(private $scope: angular.IScope) { $scope['called'] = 'no'; }
|
||||
|
||||
xit('should call `$onDestroy()` on controller', () => {});
|
||||
xit('should not call `$onDestroy()` on scope', () => {});
|
||||
$postLink() { this.$scope['called'] = 'yes'; }
|
||||
}
|
||||
};
|
||||
|
||||
const ng1DirectiveB: angular.IDirective = {
|
||||
template: 'Called: {{ called }}',
|
||||
bindToController: true,
|
||||
controller: class {
|
||||
constructor($scope: angular.IScope) {
|
||||
$scope['called'] = 'no';
|
||||
(this as any)['$postLink'] = () => $scope['called'] = 'yes';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Define `Ng1ComponentFacade`
|
||||
@Directive({selector: 'ng1A'})
|
||||
class Ng1ComponentAFacade extends UpgradeComponent {
|
||||
constructor(elementRef: ElementRef, injector: Injector) {
|
||||
super('ng1A', elementRef, injector);
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({selector: 'ng1B'})
|
||||
class Ng1ComponentBFacade extends UpgradeComponent {
|
||||
constructor(elementRef: ElementRef, injector: Injector) {
|
||||
super('ng1B', elementRef, injector);
|
||||
}
|
||||
}
|
||||
|
||||
// Define `Ng2Component`
|
||||
@Component({selector: 'ng2', template: '<ng1A></ng1A> | <ng1B></ng1B>'})
|
||||
class Ng2Component {
|
||||
}
|
||||
|
||||
// Define `ng1Module`
|
||||
const ng1Module = angular.module('ng1Module', [])
|
||||
.directive('ng1A', () => ng1DirectiveA)
|
||||
.directive('ng1B', () => ng1DirectiveB)
|
||||
.directive('ng2', downgradeComponent({component: Ng2Component}));
|
||||
|
||||
// Define `Ng2Module`
|
||||
@NgModule({
|
||||
declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component],
|
||||
entryComponents: [Ng2Component],
|
||||
imports: [BrowserModule, UpgradeModule]
|
||||
})
|
||||
class Ng2Module {
|
||||
ngDoBootstrap() {}
|
||||
}
|
||||
|
||||
// Bootstrap
|
||||
const element = html(`<ng2></ng2>`);
|
||||
|
||||
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
|
||||
expect(multiTrim(element.textContent)).toBe('Called: yes | Called: yes');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should not call `$postLink()` on scope', async(() => {
|
||||
// Define `ng1Directive`
|
||||
const ng1DirectiveA: angular.IDirective = {
|
||||
template: 'Called: {{ called }}',
|
||||
bindToController: false,
|
||||
controller: class {
|
||||
constructor($scope: angular.IScope) {
|
||||
$scope['called'] = 'no';
|
||||
$scope['$postLink'] = () => $scope['called'] = 'yes';
|
||||
Object.getPrototypeOf($scope)['$postLink'] = () => $scope['called'] = 'yes';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ng1DirectiveB: angular.IDirective = {
|
||||
template: 'Called: {{ called }}',
|
||||
bindToController: true,
|
||||
controller: class {
|
||||
constructor($scope: angular.IScope) {
|
||||
$scope['called'] = 'no';
|
||||
$scope['$postLink'] = () => $scope['called'] = 'yes';
|
||||
Object.getPrototypeOf($scope)['$postLink'] = () => $scope['called'] = 'yes';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Define `Ng1ComponentFacade`
|
||||
@Directive({selector: 'ng1A'})
|
||||
class Ng1ComponentAFacade extends UpgradeComponent {
|
||||
constructor(elementRef: ElementRef, injector: Injector) {
|
||||
super('ng1A', elementRef, injector);
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({selector: 'ng1B'})
|
||||
class Ng1ComponentBFacade extends UpgradeComponent {
|
||||
constructor(elementRef: ElementRef, injector: Injector) {
|
||||
super('ng1B', elementRef, injector);
|
||||
}
|
||||
}
|
||||
|
||||
// Define `Ng2Component`
|
||||
@Component({selector: 'ng2', template: '<ng1A></ng1A> | <ng1B></ng1B>'})
|
||||
class Ng2Component {
|
||||
}
|
||||
|
||||
// Define `ng1Module`
|
||||
const ng1Module = angular.module('ng1Module', [])
|
||||
.directive('ng1A', () => ng1DirectiveA)
|
||||
.directive('ng1B', () => ng1DirectiveB)
|
||||
.directive('ng2', downgradeComponent({component: Ng2Component}));
|
||||
|
||||
// Define `Ng2Module`
|
||||
@NgModule({
|
||||
declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component],
|
||||
entryComponents: [Ng2Component],
|
||||
imports: [BrowserModule, UpgradeModule]
|
||||
})
|
||||
class Ng2Module {
|
||||
ngDoBootstrap() {}
|
||||
}
|
||||
|
||||
// Bootstrap
|
||||
const element = html(`<ng2></ng2>`);
|
||||
|
||||
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
|
||||
expect(multiTrim(element.textContent)).toBe('Called: no | Called: no');
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
it('should call `$onDestroy()` on controller', async(() => {
|
||||
const controllerOnDestroyA = jasmine.createSpy('controllerOnDestroyA');
|
||||
const controllerOnDestroyB = jasmine.createSpy('controllerOnDestroyB');
|
||||
let ng2ComponentInstance: Ng2Component;
|
||||
|
||||
// Define `ng1Directive`
|
||||
const ng1DirectiveA: angular.IDirective = {
|
||||
template: 'ng1A',
|
||||
scope: {},
|
||||
bindToController: false,
|
||||
controllerAs: '$ctrl',
|
||||
controller: class {$onDestroy() { controllerOnDestroyA(); }}
|
||||
};
|
||||
|
||||
const ng1DirectiveB: angular.IDirective = {
|
||||
template: 'ng1B',
|
||||
scope: {},
|
||||
bindToController: true,
|
||||
controllerAs: '$ctrl',
|
||||
controller: class {
|
||||
constructor() {
|
||||
(this as any)['$onDestroy'] = controllerOnDestroyB;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Define `Ng1ComponentFacade`
|
||||
@Directive({selector: 'ng1A'})
|
||||
class Ng1ComponentAFacade extends UpgradeComponent {
|
||||
constructor(elementRef: ElementRef, injector: Injector) {
|
||||
super('ng1A', elementRef, injector);
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({selector: 'ng1B'})
|
||||
class Ng1ComponentBFacade extends UpgradeComponent {
|
||||
constructor(elementRef: ElementRef, injector: Injector) {
|
||||
super('ng1B', elementRef, injector);
|
||||
}
|
||||
}
|
||||
|
||||
// Define `Ng2Component`
|
||||
@Component(
|
||||
{selector: 'ng2', template: '<div *ngIf="show"><ng1A></ng1A> | <ng1B></ng1B></div>'})
|
||||
class Ng2Component {
|
||||
@Input() show: boolean;
|
||||
}
|
||||
|
||||
// Define `ng1Module`
|
||||
const ng1Module =
|
||||
angular.module('ng1Module', [])
|
||||
.directive('ng1A', () => ng1DirectiveA)
|
||||
.directive('ng1B', () => ng1DirectiveB)
|
||||
.directive(
|
||||
'ng2', downgradeComponent({component: Ng2Component, inputs: ['show']}));
|
||||
|
||||
// Define `Ng2Module`
|
||||
@NgModule({
|
||||
declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component],
|
||||
entryComponents: [Ng2Component],
|
||||
imports: [BrowserModule, UpgradeModule]
|
||||
})
|
||||
class Ng2Module {
|
||||
ngDoBootstrap() {}
|
||||
}
|
||||
|
||||
// Bootstrap
|
||||
const element = html('<ng2 [show]="!destroyFromNg2" ng-if="!destroyFromNg1"></ng2>');
|
||||
|
||||
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
|
||||
const $rootScope = adapter.$injector.get('$rootScope') as angular.IRootScopeService;
|
||||
|
||||
expect(multiTrim(document.body.textContent)).toBe('ng1A | ng1B');
|
||||
expect(controllerOnDestroyA).not.toHaveBeenCalled();
|
||||
expect(controllerOnDestroyB).not.toHaveBeenCalled();
|
||||
|
||||
$rootScope.$apply('destroyFromNg1 = true');
|
||||
|
||||
expect(multiTrim(document.body.textContent)).toBe('');
|
||||
expect(controllerOnDestroyA).toHaveBeenCalled();
|
||||
expect(controllerOnDestroyB).toHaveBeenCalled();
|
||||
|
||||
controllerOnDestroyA.calls.reset();
|
||||
controllerOnDestroyB.calls.reset();
|
||||
$rootScope.$apply('destroyFromNg1 = false');
|
||||
|
||||
expect(multiTrim(document.body.textContent)).toBe('ng1A | ng1B');
|
||||
expect(controllerOnDestroyA).not.toHaveBeenCalled();
|
||||
expect(controllerOnDestroyB).not.toHaveBeenCalled();
|
||||
|
||||
$rootScope.$apply('destroyFromNg2 = true');
|
||||
|
||||
expect(multiTrim(document.body.textContent)).toBe('');
|
||||
expect(controllerOnDestroyA).toHaveBeenCalled();
|
||||
expect(controllerOnDestroyB).toHaveBeenCalled();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should not call `$onDestroy()` on scope', async(() => {
|
||||
const scopeOnDestroy = jasmine.createSpy('scopeOnDestroy');
|
||||
let ng2ComponentInstance: Ng2Component;
|
||||
|
||||
// Define `ng1Directive`
|
||||
const ng1DirectiveA: angular.IDirective = {
|
||||
template: 'ng1A',
|
||||
scope: {},
|
||||
bindToController: false,
|
||||
controllerAs: '$ctrl',
|
||||
controller: class {
|
||||
constructor($scope: angular.IScope) {
|
||||
$scope['$onDestroy'] = scopeOnDestroy;
|
||||
Object.getPrototypeOf($scope)['$onDestroy'] = scopeOnDestroy;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ng1DirectiveB: angular.IDirective = {
|
||||
template: 'ng1B',
|
||||
scope: {},
|
||||
bindToController: true,
|
||||
controllerAs: '$ctrl',
|
||||
controller: class {
|
||||
constructor($scope: angular.IScope) {
|
||||
$scope['$onDestroy'] = scopeOnDestroy;
|
||||
Object.getPrototypeOf($scope)['$onDestroy'] = scopeOnDestroy;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Define `Ng1ComponentFacade`
|
||||
@Directive({selector: 'ng1A'})
|
||||
class Ng1ComponentAFacade extends UpgradeComponent {
|
||||
constructor(elementRef: ElementRef, injector: Injector) {
|
||||
super('ng1A', elementRef, injector);
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({selector: 'ng1B'})
|
||||
class Ng1ComponentBFacade extends UpgradeComponent {
|
||||
constructor(elementRef: ElementRef, injector: Injector) {
|
||||
super('ng1B', elementRef, injector);
|
||||
}
|
||||
}
|
||||
|
||||
// Define `Ng2Component`
|
||||
@Component(
|
||||
{selector: 'ng2', template: '<div *ngIf="show"><ng1A></ng1A> | <ng1B></ng1B></div>'})
|
||||
class Ng2Component {
|
||||
@Input() show: boolean;
|
||||
}
|
||||
|
||||
// Define `ng1Module`
|
||||
const ng1Module =
|
||||
angular.module('ng1Module', [])
|
||||
.directive('ng1A', () => ng1DirectiveA)
|
||||
.directive('ng1B', () => ng1DirectiveB)
|
||||
.directive(
|
||||
'ng2', downgradeComponent({component: Ng2Component, inputs: ['show']}));
|
||||
|
||||
// Define `Ng2Module`
|
||||
@NgModule({
|
||||
declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component],
|
||||
entryComponents: [Ng2Component],
|
||||
imports: [BrowserModule, UpgradeModule]
|
||||
})
|
||||
class Ng2Module {
|
||||
ngDoBootstrap() {}
|
||||
}
|
||||
|
||||
// Bootstrap
|
||||
const element = html('<ng2 [show]="!destroyFromNg2" ng-if="!destroyFromNg1"></ng2>');
|
||||
|
||||
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
|
||||
const $rootScope = adapter.$injector.get('$rootScope') as angular.IRootScopeService;
|
||||
|
||||
expect(multiTrim(document.body.textContent)).toBe('ng1A | ng1B');
|
||||
expect(scopeOnDestroy).not.toHaveBeenCalled();
|
||||
|
||||
$rootScope.$apply('destroyFromNg1 = true');
|
||||
|
||||
expect(multiTrim(document.body.textContent)).toBe('');
|
||||
expect(scopeOnDestroy).not.toHaveBeenCalled();
|
||||
|
||||
$rootScope.$apply('destroyFromNg1 = false');
|
||||
|
||||
expect(multiTrim(document.body.textContent)).toBe('ng1A | ng1B');
|
||||
expect(scopeOnDestroy).not.toHaveBeenCalled();
|
||||
|
||||
$rootScope.$apply('destroyFromNg2 = true');
|
||||
|
||||
expect(multiTrim(document.body.textContent)).toBe('');
|
||||
expect(scopeOnDestroy).not.toHaveBeenCalled();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should be called in order `$onChanges()` > `$onInit()` > `$postLink()`', async(() => {
|
||||
// Define `ng1Component`
|
||||
const ng1Component: angular.IComponent = {
|
||||
template: '{{ $ctrl.calls.join(" > ") }}',
|
||||
bindings: {value: '<'},
|
||||
controller: class {
|
||||
calls: string[] = [];
|
||||
|
||||
$onChanges() { this.calls.push('$onChanges'); } $onInit() {
|
||||
this.calls.push('$onInit');
|
||||
} $postLink() { this.calls.push('$postLink'); }
|
||||
}
|
||||
};
|
||||
|
||||
// Define `Ng1ComponentFacade`
|
||||
@Directive({selector: 'ng1'})
|
||||
class Ng1ComponentFacade extends UpgradeComponent {
|
||||
@Input() value: any;
|
||||
|
||||
constructor(elementRef: ElementRef, injector: Injector) {
|
||||
super('ng1', elementRef, injector);
|
||||
}
|
||||
}
|
||||
|
||||
// Define `Ng2Component`
|
||||
@Component({selector: 'ng2', template: '<ng1 value="foo"></ng1>'})
|
||||
class Ng2Component {
|
||||
}
|
||||
|
||||
// Define `ng1Module`
|
||||
const ng1Module = angular.module('ng1Module', [])
|
||||
.component('ng1', ng1Component)
|
||||
.directive('ng2', downgradeComponent({component: Ng2Component}));
|
||||
|
||||
// Define `Ng2Module`
|
||||
@NgModule({
|
||||
declarations: [Ng1ComponentFacade, Ng2Component],
|
||||
entryComponents: [Ng2Component],
|
||||
imports: [BrowserModule, UpgradeModule]
|
||||
})
|
||||
class Ng2Module {
|
||||
ngDoBootstrap() {}
|
||||
}
|
||||
|
||||
// Bootstrap
|
||||
const element = html(`<ng2></ng2>`);
|
||||
|
||||
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
|
||||
expect(multiTrim(element.textContent)).toBe('$onChanges > $onInit > $postLink');
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it('should destroy `$componentScope` when destroying the upgraded component', async(() => {
|
||||
const scopeDestroyListener = jasmine.createSpy('scopeDestroyListener');
|
||||
let ng2ComponentAInstance: Ng2ComponentA;
|
||||
|
||||
// Define `ng1Component`
|
||||
const ng1Component: angular.IComponent = {
|
||||
controller: class {
|
||||
constructor($scope: angular.IScope) {
|
||||
$scope.$on('$destroy', scopeDestroyListener);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Define `Ng1ComponentFacade`
|
||||
@Directive({selector: 'ng1'})
|
||||
class Ng1ComponentFacade extends UpgradeComponent {
|
||||
constructor(elementRef: ElementRef, injector: Injector) {
|
||||
super('ng1', elementRef, injector);
|
||||
}
|
||||
}
|
||||
|
||||
// Define `Ng2Component`
|
||||
@Component({selector: 'ng2A', template: '<ng2B *ngIf="!destroyIt"></ng2B>'})
|
||||
class Ng2ComponentA {
|
||||
destroyIt = false;
|
||||
|
||||
constructor() {
|
||||
ng2ComponentAInstance = this;
|
||||
}
|
||||
}
|
||||
|
||||
@Component({selector: 'ng2B', template: '<ng1></ng1>'})
|
||||
class Ng2ComponentB {
|
||||
}
|
||||
|
||||
// Define `ng1Module`
|
||||
const ng1Module =
|
||||
angular.module('ng1Module', [])
|
||||
.component('ng1', ng1Component)
|
||||
.directive('ng2A', downgradeComponent({component: Ng2ComponentA}));
|
||||
|
||||
// Define `Ng2Module`
|
||||
@NgModule({
|
||||
declarations: [Ng1ComponentFacade, Ng2ComponentA, Ng2ComponentB],
|
||||
entryComponents: [Ng2ComponentA],
|
||||
imports: [BrowserModule, UpgradeModule]
|
||||
})
|
||||
class Ng2Module {
|
||||
ngDoBootstrap() {}
|
||||
}
|
||||
|
||||
// Bootstrap
|
||||
const element = html(`<ng2-a></ng2-a>`);
|
||||
|
||||
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
|
||||
expect(scopeDestroyListener).not.toHaveBeenCalled();
|
||||
|
||||
ng2ComponentAInstance.destroyIt = true;
|
||||
digest(adapter);
|
||||
|
||||
expect(scopeDestroyListener).toHaveBeenCalled();
|
||||
});
|
||||
}));
|
||||
|
||||
// it('should support ng2 > ng1 > ng2', async(() => {
|
||||
// const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
|
||||
// const ng1Module = angular.module('ng1', []);
|
||||
|
|
|
@ -5,10 +5,11 @@ export declare function downgradeComponent(info: ComponentInfo): angular.IInject
|
|||
export declare function downgradeInjectable(token: any): (string | ((i: Injector) => any))[];
|
||||
|
||||
/** @experimental */
|
||||
export declare class UpgradeComponent implements OnInit, OnChanges, DoCheck {
|
||||
export declare class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
|
||||
constructor(name: string, elementRef: ElementRef, injector: Injector);
|
||||
ngDoCheck(): void;
|
||||
ngOnChanges(changes: SimpleChanges): void;
|
||||
ngOnDestroy(): void;
|
||||
ngOnInit(): void;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue