diff --git a/modules/@angular/upgrade/src/aot/upgrade_component.ts b/modules/@angular/upgrade/src/aot/upgrade_component.ts
index d7bbac5d40..6eb8274078 100644
--- a/modules/@angular/upgrade/src/aot/upgrade_component.ts
+++ b/modules/@angular/upgrade/src/aot/upgrade_component.ts
@@ -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);
diff --git a/modules/@angular/upgrade/test/aot/integration/upgrade_component_spec.ts b/modules/@angular/upgrade/test/aot/integration/upgrade_component_spec.ts
index a1db845dfa..9228d2ff66 100644
--- a/modules/@angular/upgrade/test/aot/integration/upgrade_component_spec.ts
+++ b/modules/@angular/upgrade/test/aot/integration/upgrade_component_spec.ts
@@ -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: ' | '
+ })
+ 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(``);
+
+ 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: ' | '
+ })
+ 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(``);
+
+ 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(``);
- 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(``);
- 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: ' | '})
+ 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(``);
+
+ 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: ' | '})
+ 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(``);
+
+ 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: '
|
'})
+ 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('');
+
+ 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: ' |
'})
+ 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('');
+
+ 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: ''})
+ 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(``);
+
+ 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: ''})
+ class Ng2ComponentA {
+ destroyIt = false;
+
+ constructor() {
+ ng2ComponentAInstance = this;
+ }
+ }
+
+ @Component({selector: 'ng2B', template: ''})
+ 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(``);
+
+ 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', []);
diff --git a/tools/public_api_guard/upgrade/static.d.ts b/tools/public_api_guard/upgrade/static.d.ts
index 49ff6cdf48..3afecf1bd3 100644
--- a/tools/public_api_guard/upgrade/static.d.ts
+++ b/tools/public_api_guard/upgrade/static.d.ts
@@ -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;
}