/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {Component, destroyPlatform, Directive, ElementRef, EventEmitter, Inject, Injector, Input, NgModule, NO_ERRORS_SCHEMA, Output, SimpleChanges} from '@angular/core'; import {fakeAsync, tick, waitForAsync} from '@angular/core/testing'; import {BrowserModule} from '@angular/platform-browser'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import * as angular from '../../../src/common/src/angular1'; import {$EXCEPTION_HANDLER, $SCOPE} from '../../../src/common/src/constants'; import {html, multiTrim, withEachNg1Version} from '../../../src/common/test/helpers/common_test_helpers'; import {downgradeComponent, UpgradeComponent, UpgradeModule} from '../../index'; import {$digest, bootstrap} from './static_test_helpers'; withEachNg1Version(() => { describe('upgrade ng1 component', () => { beforeEach(() => destroyPlatform()); afterEach(() => destroyPlatform()); describe('template/templateUrl', () => { it('should support `template` (string)', waitForAsync(() => { // Define `ng1Component` const ng1Component: angular.IComponent = {template: 'Hello, Angular!'}; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { 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((upgrade) => { expect(multiTrim(element.textContent)).toBe('Hello, Angular!'); }); })); it('should support `template` (function)', waitForAsync(() => { // Define `ng1Component` const ng1Component: angular.IComponent = {template: () => 'Hello, Angular!'}; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { 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('Hello, Angular!'); }); })); it('should pass $element to `template` function and not $attrs', waitForAsync(() => { // Define `ng1Component` const ng1Component: angular.IComponent = { template: ($attrs: angular.IAttributes, $element: angular.IAugmentedJQuery) => { expect($attrs).toBeUndefined(); expect($element).toBeDefined(); return 'Hello, Angular!'; } }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { 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('Hello, Angular!'); }); })); it('should support `templateUrl` (string) fetched from `$templateCache`', waitForAsync(() => { // Define `ng1Component` const ng1Component: angular.IComponent = {templateUrl: 'ng1.component.html'}; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { 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})) .run( ($templateCache: angular.ITemplateCacheService) => $templateCache.put('ng1.component.html', 'Hello, Angular!')); // 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('Hello, Angular!'); }); })); it('should support `templateUrl` (function) fetched from `$templateCache`', waitForAsync(() => { // Define `ng1Component` const ng1Component: angular.IComponent = {templateUrl: () => 'ng1.component.html'}; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { 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})) .run( ($templateCache: angular.ITemplateCacheService) => $templateCache.put('ng1.component.html', 'Hello, Angular!')); // 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('Hello, Angular!'); }); })); it('should pass $element to `templateUrl` function and not $attrs', waitForAsync(() => { // Define `ng1Component` const ng1Component: angular.IComponent = { templateUrl: ($attrs: angular.IAttributes, $element: angular.IAugmentedJQuery) => { expect($attrs).toBeUndefined(); expect($element).toBeDefined(); return 'ng1.component.html'; } }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { 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})) .run( ($templateCache: angular.ITemplateCacheService) => $templateCache.put('ng1.component.html', 'Hello, Angular!')); // 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('Hello, Angular!'); }); })); // NOT SUPPORTED YET xit('should support `templateUrl` (string) fetched from the server', fakeAsync(() => { // Define `ng1Component` const ng1Component: angular.IComponent = {templateUrl: 'ng1.component.html'}; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { 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})) .value( '$httpBackend', (method: string, url: string, post?: any, callback?: Function) => setTimeout( () => callback!(200, `${method}:${url}`.toLowerCase()), 1000)); // Define `Ng2Module` @NgModule({ declarations: [Ng1ComponentFacade, Ng2Component], entryComponents: [Ng2Component], imports: [BrowserModule, UpgradeModule] }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const element = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => { tick(500); expect(multiTrim(element.textContent)).toBe(''); tick(500); expect(multiTrim(element.textContent)).toBe('get:ng1.component.html'); }); })); // NOT SUPPORTED YET xit('should support `templateUrl` (function) fetched from the server', fakeAsync(() => { // Define `ng1Component` const ng1Component: angular.IComponent = {templateUrl: () => 'ng1.component.html'}; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { 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})) .value( '$httpBackend', (method: string, url: string, post?: any, callback?: Function) => setTimeout( () => callback!(200, `${method}:${url}`.toLowerCase()), 1000)); // Define `Ng2Module` @NgModule({ declarations: [Ng1ComponentFacade, Ng2Component], entryComponents: [Ng2Component], imports: [BrowserModule, UpgradeModule] }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const element = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => { tick(500); expect(multiTrim(element.textContent)).toBe(''); tick(500); expect(multiTrim(element.textContent)).toBe('get:ng1.component.html'); }); })); it('should support empty templates', waitForAsync(() => { // Define `ng1Component`s const ng1ComponentA: angular.IComponent = {template: ''}; const ng1ComponentB: angular.IComponent = {template: () => ''}; const ng1ComponentC: angular.IComponent = {templateUrl: 'ng1.component.html'}; const ng1ComponentD: angular.IComponent = {templateUrl: () => 'ng1.component.html'}; // Define `Ng1ComponentFacade`s @Directive({selector: 'ng1A'}) class Ng1ComponentAFacade extends UpgradeComponent { constructor(e: ElementRef, i: Injector) { super('ng1A', e, i); } } @Directive({selector: 'ng1B'}) class Ng1ComponentBFacade extends UpgradeComponent { constructor(e: ElementRef, i: Injector) { super('ng1B', e, i); } } @Directive({selector: 'ng1C'}) class Ng1ComponentCFacade extends UpgradeComponent { constructor(e: ElementRef, i: Injector) { super('ng1C', e, i); } } @Directive({selector: 'ng1D'}) class Ng1ComponentDFacade extends UpgradeComponent { constructor(e: ElementRef, i: Injector) { super('ng1D', e, i); } } // Define `Ng2Component` @Component({ selector: 'ng2', template: ` Ignore this Ignore this Ignore this Ignore this ` }) class Ng2Component { } // Define `ng1Module` const ng1Module = angular.module_('ng1Module', []) .component('ng1A', ng1ComponentA) .component('ng1B', ng1ComponentB) .component('ng1C', ng1ComponentC) .component('ng1D', ng1ComponentD) .directive('ng2', downgradeComponent({component: Ng2Component})) .run( ($templateCache: angular.ITemplateCacheService) => $templateCache.put('ng1.component.html', '')); // Define `Ng2Module` @NgModule({ declarations: [ Ng1ComponentAFacade, Ng1ComponentBFacade, Ng1ComponentCFacade, Ng1ComponentDFacade, Ng2Component ], entryComponents: [Ng2Component], imports: [BrowserModule, UpgradeModule], schemas: [NO_ERRORS_SCHEMA] }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const element = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => { expect(multiTrim(element.textContent)).toBe(''); }); })); }); describe('bindings', () => { it('should support `@` bindings', fakeAsync(() => { let ng2ComponentInstance: Ng2Component; // Define `ng1Component` const ng1Component: angular.IComponent = { template: 'Inside: {{ $ctrl.inputA }}, {{ $ctrl.inputB }}', bindings: {inputA: '@inputAttrA', inputB: '@'} }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { // TODO(issue/24571): remove '!'. @Input('inputAttrA') inputA!: string; // TODO(issue/24571): remove '!'. @Input() inputB!: string; constructor(elementRef: ElementRef, injector: Injector) { super('ng1', elementRef, injector); } } // Define `Ng2Component` @Component({ selector: 'ng2', template: ` | Outside: {{ dataA }}, {{ dataB }} ` }) class Ng2Component { dataA = 'foo'; dataB = 'bar'; constructor() { ng2ComponentInstance = this; } } // 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(adapter => { const ng1 = element.querySelector('ng1')!; const ng1Controller = angular.element(ng1).controller!('ng1'); expect(multiTrim(element.textContent)).toBe('Inside: foo, bar | Outside: foo, bar'); ng1Controller.inputA = 'baz'; ng1Controller.inputB = 'qux'; tick(); expect(multiTrim(element.textContent)).toBe('Inside: baz, qux | Outside: foo, bar'); ng2ComponentInstance.dataA = 'foo2'; ng2ComponentInstance.dataB = 'bar2'; $digest(adapter); tick(); expect(multiTrim(element.textContent)) .toBe('Inside: foo2, bar2 | Outside: foo2, bar2'); }); })); it('should support `<` bindings', fakeAsync(() => { let ng2ComponentInstance: Ng2Component; // Define `ng1Component` const ng1Component: angular.IComponent = { template: 'Inside: {{ $ctrl.inputA.value }}, {{ $ctrl.inputB.value }}', bindings: {inputA: ' | Outside: {{ dataA.value }}, {{ dataB.value }} ` }) class Ng2Component { dataA = {value: 'foo'}; dataB = {value: 'bar'}; constructor() { ng2ComponentInstance = this; } } // 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(adapter => { const ng1 = element.querySelector('ng1')!; const ng1Controller = angular.element(ng1).controller!('ng1'); expect(multiTrim(element.textContent)).toBe('Inside: foo, bar | Outside: foo, bar'); ng1Controller.inputA = {value: 'baz'}; ng1Controller.inputB = {value: 'qux'}; tick(); expect(multiTrim(element.textContent)).toBe('Inside: baz, qux | Outside: foo, bar'); ng2ComponentInstance.dataA = {value: 'foo2'}; ng2ComponentInstance.dataB = {value: 'bar2'}; $digest(adapter); tick(); expect(multiTrim(element.textContent)) .toBe('Inside: foo2, bar2 | Outside: foo2, bar2'); }); })); it('should support `=` bindings', fakeAsync(() => { let ng2ComponentInstance: Ng2Component; // Define `ng1Component` const ng1Component: angular.IComponent = { template: 'Inside: {{ $ctrl.inputA.value }}, {{ $ctrl.inputB.value }}', bindings: {inputA: '=inputAttrA', inputB: '='} }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { // TODO(issue/24571): remove '!'. @Input('inputAttrA') inputA!: string; // TODO(issue/24571): remove '!'. @Output('inputAttrAChange') inputAChange!: EventEmitter; // TODO(issue/24571): remove '!'. @Input() inputB!: string; // TODO(issue/24571): remove '!'. @Output() inputBChange!: EventEmitter; constructor(elementRef: ElementRef, injector: Injector) { super('ng1', elementRef, injector); } } // Define `Ng2Component` @Component({ selector: 'ng2', template: ` | Outside: {{ dataA.value }}, {{ dataB.value }} ` }) class Ng2Component { dataA = {value: 'foo'}; dataB = {value: 'bar'}; constructor() { ng2ComponentInstance = this; } } // 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(adapter => { const ng1 = element.querySelector('ng1')!; const ng1Controller = angular.element(ng1).controller!('ng1'); expect(multiTrim(element.textContent)).toBe('Inside: foo, bar | Outside: foo, bar'); ng1Controller.inputA = {value: 'baz'}; ng1Controller.inputB = {value: 'qux'}; tick(); expect(multiTrim(element.textContent)).toBe('Inside: baz, qux | Outside: baz, qux'); ng2ComponentInstance.dataA = {value: 'foo2'}; ng2ComponentInstance.dataB = {value: 'bar2'}; $digest(adapter); tick(); expect(multiTrim(element.textContent)) .toBe('Inside: foo2, bar2 | Outside: foo2, bar2'); }); })); it('should support `&` bindings', fakeAsync(() => { // Define `ng1Component` const ng1Component: angular.IComponent = { template: 'Inside: -', bindings: {outputA: '&outputAttrA', outputB: '&'} }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { // TODO(issue/24571): remove '!'. @Output('outputAttrA') outputA!: EventEmitter; // TODO(issue/24571): remove '!'. @Output() outputB!: EventEmitter; constructor(elementRef: ElementRef, injector: Injector) { super('ng1', elementRef, injector); } } // Define `Ng2Component` @Component({ selector: 'ng2', template: ` | Outside: {{ dataA }}, {{ dataB }} ` }) class Ng2Component { dataA = 'foo'; dataB = 'bar'; } // 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(() => { const ng1 = element.querySelector('ng1')!; const ng1Controller = angular.element(ng1).controller!('ng1'); expect(multiTrim(element.textContent)).toBe('Inside: - | Outside: foo, bar'); ng1Controller.outputA('baz'); ng1Controller.outputB('qux'); tick(); expect(multiTrim(element.textContent)).toBe('Inside: - | Outside: baz, qux'); }); })); it('should bind properties, events', fakeAsync(() => { // Define `ng1Component` const ng1Component: angular.IComponent = { template: ` Hello {{ $ctrl.fullName }}; A: {{ $ctrl.modelA }}; B: {{ $ctrl.modelB }}; C: {{ $ctrl.modelC }} `, bindings: {fullName: '@', modelA: ' { if (v === 'Savkin') { this.modelB = 'SAVKIN'; this.event('WORKS'); // Should not update because `modelA: '; @Input() modelC: any; // TODO(issue/24571): remove '!'. @Output() modelCChange!: EventEmitter; // TODO(issue/24571): remove '!'. @Output() event!: EventEmitter; constructor(elementRef: ElementRef, injector: Injector) { super('ng1', elementRef, injector); } } // Define `Ng2Component` @Component({ selector: 'ng2', template: ` | | {{ event }} - {{ last }}, {{ first }}, {{ city }} ` }) class Ng2Component { first = 'Victor'; last = 'Savkin'; city = 'SF'; event = '?'; } // 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( 'Hello Savkin, Victor, SF; A: VICTOR; B: SAVKIN; C: sf | ' + 'Hello TEST; A: First; B: Last; C: City | ' + 'WORKS - SAVKIN, Victor, SF'); // Detect changes tick(); expect(multiTrim(element.textContent)) .toBe( 'Hello SAVKIN, Victor, SF; A: VICTOR; B: SAVKIN; C: sf | ' + 'Hello TEST; A: First; B: Last; C: City | ' + 'WORKS - SAVKIN, Victor, SF'); }); })); it('should bind optional properties', fakeAsync(() => { // Define `ng1Component` const ng1Component: angular.IComponent = { template: 'Inside: {{ $ctrl.inputA.value }}, {{ $ctrl.inputB }}', bindings: {inputA: '=?inputAttrA', inputB: '=?', outputA: '&?outputAttrA', outputB: '&?'} }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { // TODO(issue/24571): remove '!'. @Input('inputAttrA') inputA!: string; // TODO(issue/24571): remove '!'. @Output('inputAttrAChange') inputAChange!: EventEmitter; // TODO(issue/24571): remove '!'. @Input() inputB!: string; // TODO(issue/24571): remove '!'. @Output() inputBChange!: EventEmitter; // TODO(issue/24571): remove '!'. @Output('outputAttrA') outputA!: EventEmitter; // TODO(issue/24571): remove '!'. @Output() outputB!: EventEmitter; constructor(elementRef: ElementRef, injector: Injector) { super('ng1', elementRef, injector); } } // Define `Ng2Component` @Component({ selector: 'ng2', template: ` | | | | Outside: {{ dataA.value }}, {{ dataB.value }} ` }) class Ng2Component { dataA = {value: 'foo'}; dataB = {value: 'bar'}; updateDataB(value: any) { this.dataB.value = value; } } // 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(adapter => { const ng1s = element.querySelectorAll('ng1')!; const ng1Controller0 = angular.element(ng1s[0]).controller!('ng1'); const ng1Controller1 = angular.element(ng1s[1]).controller!('ng1'); const ng1Controller2 = angular.element(ng1s[2]).controller!('ng1'); expect(multiTrim(element.textContent)) .toBe( 'Inside: foo, bar | Inside: , Bar | Inside: , | Inside: , | Outside: foo, bar'); ng1Controller0.inputA.value = 'baz'; ng1Controller0.inputB = 'qux'; tick(); expect(multiTrim(element.textContent)) .toBe( 'Inside: baz, qux | Inside: , Bar | Inside: , | Inside: , | Outside: baz, qux'); ng1Controller1.outputA({value: 'foo again'}); ng1Controller2.outputB('bar again'); $digest(adapter); tick(); expect(ng1Controller0.inputA).toEqual({value: 'foo again'}); expect(ng1Controller0.inputB).toEqual('bar again'); expect(multiTrim(element.textContent)) .toBe( 'Inside: foo again, bar again | Inside: , Bar | Inside: , | Inside: , | ' + 'Outside: foo again, bar again'); }); })); it('should bind properties, events to scope when bindToController is not used', fakeAsync(() => { // Define `ng1Directive` const ng1Directive: angular.IDirective = { template: '{{ someText }} - Data: {{ inputA }} - Length: {{ inputA.length }}', scope: {inputA: '=', outputA: '&'}, controller: function(this: any, $scope: angular.IScope) { $scope['someText'] = 'ng1'; this.$scope = $scope; } }; // Define `Ng1ComponentFacade` @Directive({selector: '[ng1]'}) class Ng1ComponentFacade extends UpgradeComponent { // TODO(issue/24571): remove '!'. @Input() inputA!: string; // TODO(issue/24571): remove '!'. @Output() inputAChange!: EventEmitter; // TODO(issue/24571): remove '!'. @Output() outputA!: EventEmitter; constructor(elementRef: ElementRef, injector: Injector) { super('ng1', elementRef, injector); } } // Define `Ng2Component` @Component({ selector: 'ng2', template: `
| {{ someText }} - Data: {{ dataA }} - Length: {{ dataA.length }} ` }) class Ng2Component { someText = 'ng2'; dataA = [1, 2, 3]; } // Define `ng1Module` const ng1Module = angular.module_('ng1Module', []) .directive('ng1', () => ng1Directive) .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(adapter => { const ng1 = element.querySelector('[ng1]')!; const ng1Controller = angular.element(ng1).controller!('ng1'); expect(multiTrim(element.textContent)) .toBe('ng1 - Data: [1,2,3] - Length: 3 | ng2 - Data: 1,2,3 - Length: 3'); ng1Controller.$scope.inputA = [4, 5]; tick(); expect(multiTrim(element.textContent)) .toBe('ng1 - Data: [4,5] - Length: 2 | ng2 - Data: 4,5 - Length: 2'); ng1Controller.$scope.outputA(6); $digest(adapter); tick(); expect(ng1Controller.$scope.inputA).toEqual([4, 5, 6]); expect(multiTrim(element.textContent)) .toBe('ng1 - Data: [4,5,6] - Length: 3 | ng2 - Data: 4,5,6 - Length: 3'); }); })); }); describe('compiling', () => { it('should compile the ng1 template in the correct DOM context', waitForAsync(() => { let grandParentNodeName: string; // Define `ng1Component` const ng1ComponentA: angular.IComponent = {template: 'ng1A()'}; const ng1DirectiveB: angular.IDirective = { compile: tElem => { grandParentNodeName = tElem.parent!().parent!()[0].nodeName; return {}; } }; // Define `Ng1ComponentAFacade` @Directive({selector: 'ng1A'}) class Ng1ComponentAFacade extends UpgradeComponent { constructor(elementRef: ElementRef, injector: Injector) { super('ng1A', elementRef, injector); } } // Define `Ng2ComponentX` @Component({selector: 'ng2-x', template: 'ng2X()'}) class Ng2ComponentX { } // Define `ng1Module` const ng1Module = angular.module_('ng1', []) .component('ng1A', ng1ComponentA) .directive('ng1B', () => ng1DirectiveB) .directive('ng2X', downgradeComponent({component: Ng2ComponentX})); // Define `Ng2Module` @NgModule({ imports: [BrowserModule, UpgradeModule], declarations: [Ng1ComponentAFacade, Ng2ComponentX], entryComponents: [Ng2ComponentX], }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const element = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => { expect(grandParentNodeName).toBe('NG2-X'); }); })); }); describe('linking', () => { it('should run the pre-linking after instantiating the controller', waitForAsync(() => { const log: string[] = []; // Define `ng1Directive` const ng1Directive: angular.IDirective = { template: '', link: {pre: () => log.push('ng1-pre')}, controller: class { constructor() { log.push('ng1-ctrl'); } } }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { constructor(elementRef: ElementRef, injector: Injector) { super('ng1', elementRef, injector); } } // Define `Ng2Component` @Component({selector: 'ng2', template: ''}) class Ng2Component { } // Define `ng1Module` const ng1Module = angular.module_('ng1', []) .directive('ng1', () => ng1Directive) .directive('ng2', downgradeComponent({component: Ng2Component})); // Define `Ng2Module` @NgModule({ imports: [BrowserModule, UpgradeModule], declarations: [Ng1ComponentFacade, Ng2Component], entryComponents: [Ng2Component], }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const element = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => { expect(log).toEqual(['ng1-ctrl', 'ng1-pre']); }); })); it('should run the pre-linking function before linking', waitForAsync(() => { const log: string[] = []; // Define `ng1Directive` const ng1DirectiveA: angular.IDirective = { template: '', link: {pre: () => log.push('ng1A-pre')} }; const ng1DirectiveB: angular.IDirective = {link: () => log.push('ng1B-post')}; // Define `Ng1ComponentAFacade` @Directive({selector: 'ng1A'}) class Ng1ComponentAFacade extends UpgradeComponent { constructor(elementRef: ElementRef, injector: Injector) { super('ng1A', elementRef, injector); } } // Define `Ng2Component` @Component({selector: 'ng2', template: ''}) class Ng2Component { } // Define `ng1Module` const ng1Module = angular.module_('ng1', []) .directive('ng1A', () => ng1DirectiveA) .directive('ng1B', () => ng1DirectiveB) .directive('ng2', downgradeComponent({component: Ng2Component})); // Define `Ng2Module` @NgModule({ imports: [BrowserModule, UpgradeModule], declarations: [Ng1ComponentAFacade, Ng2Component], entryComponents: [Ng2Component], }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const element = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => { expect(log).toEqual(['ng1A-pre', 'ng1B-post']); }); })); it('should run the post-linking function after linking (link: object)', waitForAsync(() => { const log: string[] = []; // Define `ng1Directive` const ng1DirectiveA: angular.IDirective = { template: '', link: {post: () => log.push('ng1A-post')} }; const ng1DirectiveB: angular.IDirective = {link: () => log.push('ng1B-post')}; // Define `Ng1ComponentAFacade` @Directive({selector: 'ng1A'}) class Ng1ComponentAFacade extends UpgradeComponent { constructor(elementRef: ElementRef, injector: Injector) { super('ng1A', elementRef, injector); } } // Define `Ng2Component` @Component({selector: 'ng2', template: ''}) class Ng2Component { } // Define `ng1Module` const ng1Module = angular.module_('ng1', []) .directive('ng1A', () => ng1DirectiveA) .directive('ng1B', () => ng1DirectiveB) .directive('ng2', downgradeComponent({component: Ng2Component})); // Define `Ng2Module` @NgModule({ imports: [BrowserModule, UpgradeModule], declarations: [Ng1ComponentAFacade, Ng2Component], entryComponents: [Ng2Component], }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const element = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => { expect(log).toEqual(['ng1B-post', 'ng1A-post']); }); })); it('should run the post-linking function after linking (link: function)', waitForAsync(() => { const log: string[] = []; // Define `ng1Directive` const ng1DirectiveA: angular.IDirective = { template: '', link: () => log.push('ng1A-post') }; const ng1DirectiveB: angular.IDirective = {link: () => log.push('ng1B-post')}; // Define `Ng1ComponentAFacade` @Directive({selector: 'ng1A'}) class Ng1ComponentAFacade extends UpgradeComponent { constructor(elementRef: ElementRef, injector: Injector) { super('ng1A', elementRef, injector); } } // Define `Ng2Component` @Component({selector: 'ng2', template: ''}) class Ng2Component { } // Define `ng1Module` const ng1Module = angular.module_('ng1', []) .directive('ng1A', () => ng1DirectiveA) .directive('ng1B', () => ng1DirectiveB) .directive('ng2', downgradeComponent({component: Ng2Component})); // Define `Ng2Module` @NgModule({ imports: [BrowserModule, UpgradeModule], declarations: [Ng1ComponentAFacade, Ng2Component], entryComponents: [Ng2Component], }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const element = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => { expect(log).toEqual(['ng1B-post', 'ng1A-post']); }); })); it('should run the post-linking function before `$postLink`', waitForAsync(() => { const log: string[] = []; // Define `ng1Directive` const ng1Directive: angular.IDirective = { template: '', link: () => log.push('ng1-post'), controller: class { $postLink() { log.push('ng1-$post'); } } }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { constructor(elementRef: ElementRef, injector: Injector) { super('ng1', elementRef, injector); } } // Define `Ng2Component` @Component({selector: 'ng2', template: ''}) class Ng2Component { } // Define `ng1Module` const ng1Module = angular.module_('ng1', []) .directive('ng1', () => ng1Directive) .directive('ng2', downgradeComponent({component: Ng2Component})); // Define `Ng2Module` @NgModule({ imports: [BrowserModule, UpgradeModule], declarations: [Ng1ComponentFacade, Ng2Component], entryComponents: [Ng2Component], }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const element = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => { expect(log).toEqual(['ng1-post', 'ng1-$post']); }); })); }); describe('controller', () => { it('should support `controllerAs`', waitForAsync(() => { // Define `ng1Directive` const ng1Directive: angular.IDirective = { template: '{{ vm.scope }}; {{ vm.isClass }}; {{ vm.hasElement }}; {{ vm.isPublished() }}', scope: true, controllerAs: 'vm', controller: class { hasElement: string; // TODO(issue/24571): remove '!'. isClass!: string; scope: string; constructor(public $element: angular.IAugmentedJQuery, $scope: angular.IScope) { this.hasElement = $element[0].nodeName; this.scope = $scope.$parent.$parent === $scope.$root ? 'scope' : 'wrong-scope'; this.verifyIAmAClass(); } isPublished() { return this.$element.controller!('ng1') === this ? 'published' : 'not-published'; } verifyIAmAClass() { this.isClass = 'isClass'; } } }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { constructor(elementRef: ElementRef, injector: Injector) { super('ng1', elementRef, injector); } } // Define `Ng2Component` @Component({selector: 'ng2', template: ''}) class Ng2Component { } // Define `ng1Module` const ng1Module = angular.module_('ng1Module', []) .directive('ng1', () => ng1Directive) .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('scope; isClass; NG1; published'); }); })); it('should support `bindToController` (boolean)', waitForAsync(() => { // Define `ng1Directive` const ng1DirectiveA: angular.IDirective = { template: 'Scope: {{ title }}; Controller: {{ $ctrl.title }}', scope: {title: '@'}, bindToController: false, controllerAs: '$ctrl', controller: class {} }; const ng1DirectiveB: angular.IDirective = { template: 'Scope: {{ title }}; Controller: {{ $ctrl.title }}', scope: {title: '@'}, bindToController: true, controllerAs: '$ctrl', controller: class {} }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1A'}) class Ng1ComponentAFacade extends UpgradeComponent { // TODO(issue/24571): remove '!'. @Input() title!: string; constructor(elementRef: ElementRef, injector: Injector) { super('ng1A', elementRef, injector); } } @Directive({selector: 'ng1B'}) class Ng1ComponentBFacade extends UpgradeComponent { // TODO(issue/24571): remove '!'. @Input() title!: string; 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], schemas: [NO_ERRORS_SCHEMA] }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const element = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => { expect(multiTrim(element.textContent)) .toBe('Scope: WORKS; Controller: | Scope: ; Controller: WORKS'); }); })); it('should support `bindToController` (object)', waitForAsync(() => { // Define `ng1Directive` const ng1Directive: angular.IDirective = { template: '{{ $ctrl.title }}', scope: {}, bindToController: {title: '@'}, controllerAs: '$ctrl', controller: class {} }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { // TODO(issue/24571): remove '!'. @Input() title!: string; constructor(elementRef: ElementRef, injector: Injector) { super('ng1', elementRef, injector); } } // Define `Ng2Component` @Component({selector: 'ng2', template: ''}) class Ng2Component { dataA = 'foo'; dataB = 'bar'; } // Define `ng1Module` const ng1Module = angular.module_('ng1Module', []) .directive('ng1', () => ng1Directive) .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('WORKS'); }); })); it('should support `controller` as string', waitForAsync(() => { // Define `ng1Directive` const ng1Directive: angular.IDirective = { template: '{{ $ctrl.title }} {{ $ctrl.text }}', scope: {title: '@'}, bindToController: true, controller: 'Ng1Controller as $ctrl' }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { // TODO(issue/24571): remove '!'. @Input() title!: string; constructor(elementRef: ElementRef, injector: Injector) { super('ng1', elementRef, injector); } } // Define `Ng2Component` @Component({selector: 'ng2', template: ''}) class Ng2Component { } // Define `ng1Module` const ng1Module = angular.module_('ng1Module', []) .controller( 'Ng1Controller', class { text = 'GREAT'; }) .directive('ng1', () => ng1Directive) .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('WORKS GREAT'); }); })); it('should insert the compiled content before instantiating the controller', waitForAsync(() => { let compiledContent: string; let getCurrentContent: () => string; // Define `ng1Component` const ng1Component: angular.IComponent = { template: 'Hello, {{ $ctrl.name }}!', controller: class { name = 'world'; constructor($element: angular.IAugmentedJQuery) { getCurrentContent = () => $element.text!(); compiledContent = getCurrentContent(); } } }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { 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({ imports: [BrowserModule, UpgradeModule], declarations: [Ng1ComponentFacade, Ng2Component], entryComponents: [Ng2Component] }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const element = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => { expect(multiTrim(compiledContent)).toBe('Hello, {{ $ctrl.name }}!'); expect(multiTrim(getCurrentContent())).toBe('Hello, world!'); }); })); }); describe('require', () => { // NOT YET SUPPORTED xdescribe('in pre-/post-link', () => { it('should resolve to its own controller if falsy', waitForAsync(() => { // Define `ng1Directive` const ng1Directive: angular.IDirective = { template: 'Pre: {{ pre }} | Post: {{ post }}', controller: class { value = 'foo'; }, link: { pre: function(scope: any, elem: any, attrs: any, ctrl: any) { scope['pre'] = ctrl.value; }, post: function(scope: any, elem: any, attrs: any, ctrl: any) { scope['post'] = ctrl.value; } } }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { constructor(elementRef: ElementRef, injector: Injector) { super('ng1', elementRef, injector); } } // Define `Ng2Component` @Component({selector: 'ng2', template: ''}) class Ng2Component { } // Define `ng1Module` const ng1Module = angular.module_('ng1Module', []) .directive('ng1', () => ng1Directive) .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(document.body.textContent)).toBe('Pre: foo | Post: foo'); }); })); // TODO: Add more tests }); describe('in controller', () => { it('should be available to children', waitForAsync(() => { // Define `ng1Component` const ng1ComponentA: angular.IComponent = { template: '', controller: class { value = 'ng1A'; } }; const ng1ComponentB: angular.IComponent = { template: 'Required: {{ $ctrl.required.value }}', require: {required: '^^ng1A'} }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1A'}) class Ng1ComponentAFacade extends UpgradeComponent { constructor(elementRef: ElementRef, injector: Injector) { super('ng1A', elementRef, injector); } } // Define `Ng2Component` @Component({selector: 'ng2', template: ''}) class Ng2Component { } // Define `ng1Module` const ng1Module = angular.module_('ng1Module', []) .component('ng1A', ng1ComponentA) .component('ng1B', ng1ComponentB) .directive('ng2', downgradeComponent({component: Ng2Component})); // Define `Ng2Module` @NgModule({ declarations: [Ng1ComponentAFacade, Ng2Component], entryComponents: [Ng2Component], imports: [BrowserModule, UpgradeModule] }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const element = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => { expect(multiTrim(element.textContent)).toBe('Required: ng1A'); }); })); it('should throw if required controller cannot be found', waitForAsync(() => { // Define `ng1Component` const ng1ComponentA: angular.IComponent = {require: {foo: 'iDoNotExist'}}; const ng1ComponentB: angular.IComponent = {require: {foo: '^iDoNotExist'}}; const ng1ComponentC: angular.IComponent = {require: {foo: '^^iDoNotExist'}}; // 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); } } @Directive({selector: 'ng1C'}) class Ng1ComponentCFacade extends UpgradeComponent { constructor(elementRef: ElementRef, injector: Injector) { super('ng1C', elementRef, injector); } } // Define `Ng2Component` @Component({selector: 'ng2-a', template: ''}) class Ng2ComponentA { } @Component({selector: 'ng2-b', template: ''}) class Ng2ComponentB { } @Component({selector: 'ng2-c', template: ''}) class Ng2ComponentC { } // Define `ng1Module` const mockExceptionHandler = jasmine.createSpy($EXCEPTION_HANDLER); const ng1Module = angular.module_('ng1Module', []) .component('ng1A', ng1ComponentA) .component('ng1B', ng1ComponentB) .component('ng1C', ng1ComponentC) .directive('ng2A', downgradeComponent({component: Ng2ComponentA})) .directive('ng2B', downgradeComponent({component: Ng2ComponentB})) .directive('ng2C', downgradeComponent({component: Ng2ComponentC})) .value($EXCEPTION_HANDLER, mockExceptionHandler); // Define `Ng2Module` @NgModule({ declarations: [ Ng1ComponentAFacade, Ng1ComponentBFacade, Ng1ComponentCFacade, Ng2ComponentA, Ng2ComponentB, Ng2ComponentC ], entryComponents: [Ng2ComponentA, Ng2ComponentB, Ng2ComponentC], imports: [BrowserModule, UpgradeModule] }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const elementA = html(``); const elementB = html(``); const elementC = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, elementA, ng1Module).then(() => { expect(mockExceptionHandler) .toHaveBeenCalledWith(new Error( 'Unable to find required \'iDoNotExist\' in upgraded directive \'ng1A\'.')); }); bootstrap(platformBrowserDynamic(), Ng2Module, elementB, ng1Module).then(() => { expect(mockExceptionHandler) .toHaveBeenCalledWith(new Error( 'Unable to find required \'^iDoNotExist\' in upgraded directive \'ng1B\'.')); }); bootstrap(platformBrowserDynamic(), Ng2Module, elementC, ng1Module).then(() => { expect(mockExceptionHandler) .toHaveBeenCalledWith(new Error( 'Unable to find required \'^^iDoNotExist\' in upgraded directive \'ng1C\'.')); }); })); it('should not throw if missing required controller is optional', waitForAsync(() => { // Define `ng1Component` const ng1Component: angular.IComponent = { require: { foo: '?iDoNotExist', bar: '^?iDoNotExist', baz: '?^^iDoNotExist', } }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { constructor(elementRef: ElementRef, injector: Injector) { super('ng1', elementRef, injector); } } // Define `Ng2Component` @Component({selector: 'ng2', template: ''}) class Ng2Component { } // Define `ng1Module` const mockExceptionHandler = jasmine.createSpy($EXCEPTION_HANDLER); const ng1Module = angular.module_('ng1Module', []) .component('ng1', ng1Component) .directive('ng2', downgradeComponent({component: Ng2Component})) .value($EXCEPTION_HANDLER, mockExceptionHandler); // 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(mockExceptionHandler).not.toHaveBeenCalled(); }); })); it('should assign resolved values to the controller instance (if `require` is not object)', waitForAsync(() => { // Define `ng1Component` const ng1ComponentA: angular.IComponent = { template: 'ng1A(
)', controller: class { value = 'A'; } }; const ng1ComponentB: angular.IComponent = { template: `ng1B({{ $ctrl.getProps() }})`, require: '^ng1A', controller: class { getProps() { // If all goes well, there should be no keys on `this` return Object.keys(this).join(', '); } } }; const ng1ComponentC: angular.IComponent = { template: `ng1C({{ $ctrl.getProps() }})`, require: ['?ng1A', '^ng1A', '^^ng1A', 'ng1C', '^ng1C', '?^^ng1C'], controller: class { getProps() { // If all goes well, there should be no keys on `this` return Object.keys(this).join(', '); } } }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1B'}) class Ng1ComponentBFacade extends UpgradeComponent { constructor(elementRef: ElementRef, injector: Injector) { super('ng1B', elementRef, injector); } } @Directive({selector: 'ng1C'}) class Ng1ComponentCFacade extends UpgradeComponent { constructor(elementRef: ElementRef, injector: Injector) { super('ng1C', elementRef, injector); } } // Define `Ng2Component` @Component( {selector: 'ng2', template: 'ng2(
|
)'}) class Ng2Component { } // Define `ng1Module` const ng1Module = angular.module_('ng1Module', []) .component('ng1A', ng1ComponentA) .component('ng1B', ng1ComponentB) .component('ng1C', ng1ComponentC) .directive('ng2', downgradeComponent({component: Ng2Component})); // Define `Ng2Module` @NgModule({ declarations: [Ng1ComponentBFacade, Ng1ComponentCFacade, Ng2Component], entryComponents: [Ng2Component], imports: [BrowserModule, UpgradeModule] }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const element = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => { expect(multiTrim(element.textContent)).toBe('ng1A(ng2(ng1B() | ng1C()))'); }); })); it('should assign resolved values to the controller instance (if `require` is object)', waitForAsync(() => { // Define `ng1Component` const ng1ComponentA: angular.IComponent = { template: 'ng1A(
)', controller: class { value = 'A'; } }; const ng1ComponentB: angular.IComponent = { template: `ng1B( ng1A: {{ $ctrl.ng1ASelf.value }} | ^ng1A: {{ $ctrl.ng1ASelfUp.value }} | ^^ng1A: {{ $ctrl.ng1AParentUp.value }} | ng1B: {{ $ctrl.ng1BSelf.value }} | ^ng1B: {{ $ctrl.ng1BSelfUp.value }} | ^^ng1B: {{ $ctrl.ng1BParentUp.value }} )`, require: { ng1ASelf: '?ng1A', ng1ASelfUp: '^ng1A', ng1AParentUp: '^^ng1A', ng1BSelf: 'ng1B', ng1BSelfUp: '^ng1B', ng1BParentUp: '?^^ng1B', }, controller: class { value = 'B'; } }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1B'}) class Ng1ComponentBFacade extends UpgradeComponent { constructor(elementRef: ElementRef, injector: Injector) { super('ng1B', elementRef, injector); } } // Define `Ng2Component` @Component({selector: 'ng2', template: 'ng2(
)'}) class Ng2Component { } // Define `ng1Module` const ng1Module = angular.module_('ng1Module', []) .component('ng1A', ng1ComponentA) .component('ng1B', ng1ComponentB) .directive('ng2', downgradeComponent({component: Ng2Component})); // Define `Ng2Module` @NgModule({ declarations: [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( 'ng1A(ng2(ng1B( ng1A: | ^ng1A: A | ^^ng1A: A | ng1B: B | ^ng1B: B | ^^ng1B: )))'); }); })); it('should assign to controller before calling `$onInit()`', waitForAsync(() => { // Define `ng1Component` const ng1ComponentA: angular.IComponent = { template: '', controller: class { value = 'ng1A'; } }; const ng1ComponentB: angular.IComponent = { template: '$onInit: {{ $ctrl.onInitValue }}', require: {required: '^^ng1A'}, controller: class { $onInit() { const self = this as any; self.onInitValue = self.required.value; } } }; // Define `Ng1ComponentFacade` @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', []) .component('ng1A', ng1ComponentA) .component('ng1B', ng1ComponentB) .directive('ng2', downgradeComponent({component: Ng2Component})); // Define `Ng2Module` @NgModule({ declarations: [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('$onInit: ng1A'); }); })); it('should use the key as name if the required controller name is omitted', waitForAsync(() => { // Define `ng1Component` const ng1ComponentA: angular.IComponent = { template: '', controller: class { value = 'A'; } }; const ng1ComponentB: angular.IComponent = { template: '', controller: class { value = 'B'; } }; const ng1ComponentC: angular.IComponent = { template: 'ng1A: {{ $ctrl.ng1A.value }} | ng1B: {{ $ctrl.ng1B.value }} | ng1C: {{ $ctrl.ng1C.value }}', require: { ng1A: '^^', ng1B: '?^', ng1C: '', }, controller: class { value = 'C'; } }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1C'}) class Ng1ComponentCFacade extends UpgradeComponent { constructor(elementRef: ElementRef, injector: Injector) { super('ng1C', elementRef, injector); } } // Define `Ng2Component` @Component({selector: 'ng2', template: ''}) class Ng2Component { } // Define `ng1Module` const ng1Module = angular.module_('ng1Module', []) .component('ng1A', ng1ComponentA) .component('ng1B', ng1ComponentB) .component('ng1C', ng1ComponentC) .directive('ng2', downgradeComponent({component: Ng2Component})); // Define `Ng2Module` @NgModule({ declarations: [Ng1ComponentCFacade, Ng2Component], entryComponents: [Ng2Component], imports: [BrowserModule, UpgradeModule] }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const element = html(''); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => { expect(multiTrim(element.textContent)).toBe('ng1A: A | ng1B: B | ng1C: C'); }); })); }); }); describe('transclusion', () => { it('should support single-slot transclusion', waitForAsync(() => { let ng2ComponentAInstance: Ng2ComponentA; let ng2ComponentBInstance: Ng2ComponentB; // Define `ng1Component` const ng1Component: angular.IComponent = {template: 'ng1(
)', transclude: true}; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { constructor(elementRef: ElementRef, injector: Injector) { super('ng1', elementRef, injector); } } // Define `Ng2Component` @Component({ selector: 'ng2A', template: 'ng2A({{ value }} | )' }) class Ng2ComponentA { value = 'foo'; showB = false; constructor() { ng2ComponentAInstance = this; } } @Component({selector: 'ng2B', template: 'ng2B({{ value }})'}) class Ng2ComponentB { value = 'bar'; constructor() { ng2ComponentBInstance = this; } } // Define `ng1Module` const ng1Module = angular.module_('ng1Module', []) .component('ng1', ng1Component) .directive('ng2A', downgradeComponent({component: Ng2ComponentA})); // Define `Ng2Module` @NgModule({ imports: [BrowserModule, UpgradeModule], declarations: [Ng1ComponentFacade, Ng2ComponentA, Ng2ComponentB], entryComponents: [Ng2ComponentA] }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const element = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => { expect(multiTrim(element.textContent)).toBe('ng2A(ng1(foo | ))'); ng2ComponentAInstance.value = 'baz'; ng2ComponentAInstance.showB = true; $digest(adapter); expect(multiTrim(element.textContent)).toBe('ng2A(ng1(baz | ng2B(bar)))'); ng2ComponentBInstance.value = 'qux'; $digest(adapter); expect(multiTrim(element.textContent)).toBe('ng2A(ng1(baz | ng2B(qux)))'); }); })); it('should support single-slot transclusion with fallback content', waitForAsync(() => { let ng1ControllerInstances: any[] = []; let ng2ComponentInstance: Ng2Component; // Define `ng1Component` const ng1Component: angular.IComponent = { template: 'ng1(
{{ $ctrl.value }}
)', transclude: true, controller: class { value = 'from-ng1'; constructor() { ng1ControllerInstances.push(this); } } }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { constructor(elementRef: ElementRef, injector: Injector) { super('ng1', elementRef, injector); } } // Define `Ng2Component` @Component({selector: 'ng2', template: 'ng2({{ value }} | )'}) class Ng2Component { value = 'from-ng2'; constructor() { ng2ComponentInstance = this; } } // Define `ng1Module` const ng1Module = angular.module_('ng1Module', []) .component('ng1', ng1Component) .directive('ng2', downgradeComponent({component: Ng2Component})); // Define `Ng2Module` @NgModule({ imports: [BrowserModule, UpgradeModule], declarations: [Ng1ComponentFacade, Ng2Component], entryComponents: [Ng2Component] }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const element = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => { expect(multiTrim(element.textContent)).toBe('ng2(ng1(from-ng2) | ng1(from-ng1))'); ng1ControllerInstances.forEach(ctrl => ctrl.value = 'ng1-foo'); ng2ComponentInstance.value = 'ng2-bar'; $digest(adapter); expect(multiTrim(element.textContent)).toBe('ng2(ng1(ng2-bar) | ng1(ng1-foo))'); }); })); it('should support multi-slot transclusion', waitForAsync(() => { let ng2ComponentInstance: Ng2Component; // Define `ng1Component` const ng1Component: angular.IComponent = { template: 'ng1(x(
) | y(
))', transclude: {slotX: 'contentX', slotY: 'contentY'} }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { constructor(elementRef: ElementRef, injector: Injector) { super('ng1', elementRef, injector); } } // Define `Ng2Component` @Component({ selector: 'ng2', template: ` ng2( {{ x }}1 {{ y }}1 {{ x }}2 {{ y }}2 )` }) class Ng2Component { x = 'foo'; y = 'bar'; constructor() { ng2ComponentInstance = this; } } // Define `ng1Module` const ng1Module = angular.module_('ng1Module', []) .component('ng1', ng1Component) .directive('ng2', downgradeComponent({component: Ng2Component})); // Define `Ng2Module` @NgModule({ imports: [BrowserModule, UpgradeModule], declarations: [Ng1ComponentFacade, Ng2Component], entryComponents: [Ng2Component], schemas: [NO_ERRORS_SCHEMA] }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const element = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => { expect(multiTrim(element.textContent, true)).toBe('ng2(ng1(x(foo1foo2)|y(bar1bar2)))'); ng2ComponentInstance.x = 'baz'; ng2ComponentInstance.y = 'qux'; $digest(adapter); expect(multiTrim(element.textContent, true)).toBe('ng2(ng1(x(baz1baz2)|y(qux1qux2)))'); }); })); it('should support default slot (with fallback content)', waitForAsync(() => { let ng1ControllerInstances: any[] = []; let ng2ComponentInstance: Ng2Component; // Define `ng1Component` const ng1Component: angular.IComponent = { template: 'ng1(default(
fallback-{{ $ctrl.value }}
))', transclude: {slotX: 'contentX', slotY: 'contentY'}, controller: class { value = 'ng1'; constructor() { ng1ControllerInstances.push(this); } } }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { constructor(elementRef: ElementRef, injector: Injector) { super('ng1', elementRef, injector); } } // Define `Ng2Component` @Component({ selector: 'ng2', template: ` ng2( ({{ x }}) ignored x {{ x }}-{{ y }} ignored y ({{ y }}) | ignored xignored y )` }) class Ng2Component { x = 'foo'; y = 'bar'; constructor() { ng2ComponentInstance = this; } } // Define `ng1Module` const ng1Module = angular.module_('ng1Module', []) .component('ng1', ng1Component) .directive('ng2', downgradeComponent({component: Ng2Component})); // Define `Ng2Module` @NgModule({ imports: [BrowserModule, UpgradeModule], declarations: [Ng1ComponentFacade, Ng2Component], entryComponents: [Ng2Component], schemas: [NO_ERRORS_SCHEMA] }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const element = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => { expect(multiTrim(element.textContent, true)) .toBe('ng2(ng1(default((foo)foo-bar(bar)))|ng1(default(fallback-ng1)))'); ng1ControllerInstances.forEach(ctrl => ctrl.value = 'ng1-plus'); ng2ComponentInstance.x = 'baz'; ng2ComponentInstance.y = 'qux'; $digest(adapter); expect(multiTrim(element.textContent, true)) .toBe('ng2(ng1(default((baz)baz-qux(qux)))|ng1(default(fallback-ng1-plus)))'); }); })); it('should support optional transclusion slots (with fallback content)', waitForAsync(() => { let ng1ControllerInstances: any[] = []; let ng2ComponentInstance: Ng2Component; // Define `ng1Component` const ng1Component: angular.IComponent = { template: ` ng1( x(
{{ $ctrl.x }}
) | y(
{{ $ctrl.y }}
) )`, transclude: {slotX: '?contentX', slotY: '?contentY'}, controller: class { x = 'ng1X'; y = 'ng1Y'; constructor() { ng1ControllerInstances.push(this); } } }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { constructor(elementRef: ElementRef, injector: Injector) { super('ng1', elementRef, injector); } } // Define `Ng2Component` @Component({ selector: 'ng2', template: ` ng2( {{ x }} | {{ y }} )` }) class Ng2Component { x = 'ng2X'; y = 'ng2Y'; constructor() { ng2ComponentInstance = this; } } // Define `ng1Module` const ng1Module = angular.module_('ng1Module', []) .component('ng1', ng1Component) .directive('ng2', downgradeComponent({component: Ng2Component})); // Define `Ng2Module` @NgModule({ imports: [BrowserModule, UpgradeModule], declarations: [Ng1ComponentFacade, Ng2Component], entryComponents: [Ng2Component], schemas: [NO_ERRORS_SCHEMA] }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const element = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => { expect(multiTrim(element.textContent, true)) .toBe('ng2(ng1(x(ng2X)|y(ng1Y))|ng1(x(ng1X)|y(ng2Y)))'); ng1ControllerInstances.forEach(ctrl => { ctrl.x = 'ng1X-foo'; ctrl.y = 'ng1Y-bar'; }); ng2ComponentInstance.x = 'ng2X-baz'; ng2ComponentInstance.y = 'ng2Y-qux'; $digest(adapter); expect(multiTrim(element.textContent, true)) .toBe('ng2(ng1(x(ng2X-baz)|y(ng1Y-bar))|ng1(x(ng1X-foo)|y(ng2Y-qux)))'); }); })); it('should throw if a non-optional slot is not filled', waitForAsync(() => { let errorMessage: string; // Define `ng1Component` const ng1Component: angular.IComponent = { template: '', transclude: {slotX: '?contentX', slotY: 'contentY'} }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { constructor(elementRef: ElementRef, injector: Injector) { super('ng1', elementRef, injector); } } // Define `Ng2Component` @Component({selector: 'ng2', template: ''}) class Ng2Component { } // Define `ng1Module` const ng1Module = angular.module_('ng1Module', []) .value($EXCEPTION_HANDLER, (error: Error) => errorMessage = error.message) .component('ng1', ng1Component) .directive('ng2', downgradeComponent({component: Ng2Component})); // Define `Ng2Module` @NgModule({ imports: [BrowserModule, UpgradeModule], declarations: [Ng1ComponentFacade, Ng2Component], entryComponents: [Ng2Component] }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const element = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => { expect(errorMessage) .toContain('Required transclusion slot \'slotY\' on directive: ng1'); }); })); it('should support structural directives in transcluded content', waitForAsync(() => { let ng2ComponentInstance: Ng2Component; // Define `ng1Component` const ng1Component: angular.IComponent = { template: 'ng1(x(
) | default(
))', transclude: {slotX: 'contentX'} }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { constructor(elementRef: ElementRef, injector: Injector) { super('ng1', elementRef, injector); } } // Define `Ng2Component` @Component({ selector: 'ng2', template: ` ng2(
{{ x }}1
{{ y }}1
{{ x }}2
{{ y }}2
)` }) class Ng2Component { x = 'foo'; y = 'bar'; show = true; constructor() { ng2ComponentInstance = this; } } // Define `ng1Module` const ng1Module = angular.module_('ng1Module', []) .component('ng1', ng1Component) .directive('ng2', downgradeComponent({component: Ng2Component})); // Define `Ng2Module` @NgModule({ imports: [BrowserModule, UpgradeModule], declarations: [Ng1ComponentFacade, Ng2Component], entryComponents: [Ng2Component], schemas: [NO_ERRORS_SCHEMA] }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const element = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => { expect(multiTrim(element.textContent, true)).toBe('ng2(ng1(x(foo1)|default(bar2)))'); ng2ComponentInstance.x = 'baz'; ng2ComponentInstance.y = 'qux'; ng2ComponentInstance.show = false; $digest(adapter); expect(multiTrim(element.textContent, true)).toBe('ng2(ng1(x(baz2)|default(qux1)))'); ng2ComponentInstance.show = true; $digest(adapter); expect(multiTrim(element.textContent, true)).toBe('ng2(ng1(x(baz1)|default(qux2)))'); }); })); }); describe('lifecycle hooks', () => { 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 { $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})) .run(($rootScope: angular.IRootScopeService) => { Object.getPrototypeOf($rootScope)['$onChanges'] = scopeOnChanges; }); // 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', waitForAsync(() => { // Define `ng1Directive` const ng1DirectiveA: angular.IDirective = { template: 'Called: {{ called }}', bindToController: false, controller: class { constructor(private $scope: angular.IScope) { $scope['called'] = 'no'; } $onInit() { 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)['$onInit'] = () => $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 `$onInit()` on scope', waitForAsync(() => { // Define `ng1Directive` const ng1DirectiveA: angular.IDirective = { template: 'Called: {{ called }}', bindToController: false, controller: class { constructor($scope: angular.IScope) { $scope['called'] = 'no'; $scope['$onInit'] = () => $scope['called'] = 'yes'; Object.getPrototypeOf($scope)['$onInit'] = () => $scope['called'] = 'yes'; } } }; const ng1DirectiveB: angular.IDirective = { template: 'Called: {{ called }}', bindToController: true, controller: class { constructor($scope: angular.IScope) { $scope['called'] = 'no'; $scope['$onInit'] = () => $scope['called'] = 'yes'; Object.getPrototypeOf($scope)['$onInit'] = () => $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 `$postLink()` on controller', waitForAsync(() => { // Define `ng1Directive` const ng1DirectiveA: angular.IDirective = { template: 'Called: {{ called }}', bindToController: false, controller: class { constructor(private $scope: angular.IScope) { $scope['called'] = 'no'; } $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', waitForAsync(() => { // 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 `$doCheck()` on controller', waitForAsync(() => { const controllerDoCheckA = jasmine.createSpy('controllerDoCheckA'); const controllerDoCheckB = jasmine.createSpy('controllerDoCheckB'); // Define `ng1Directive` const ng1DirectiveA: angular.IDirective = { template: 'ng1A', bindToController: false, controller: class { $doCheck() { controllerDoCheckA(); } } }; const ng1DirectiveB: angular.IDirective = { template: 'ng1B', bindToController: true, controller: class { constructor() { (this as any)['$doCheck'] = controllerDoCheckB; } } }; // 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(adapter => { // Get to a stable `$digest` state. $digest(adapter); // Initial change. // (Do not use a specific number due to differences between AngularJS 1.5/1.6.) expect(controllerDoCheckA.calls.count()).toBeGreaterThan(0); expect(controllerDoCheckB.calls.count()).toBeGreaterThan(0); controllerDoCheckA.calls.reset(); controllerDoCheckB.calls.reset(); // Run a `$digest` $digest(adapter); expect(controllerDoCheckA.calls.count()).toBe(1); expect(controllerDoCheckB.calls.count()).toBe(1); // Run another `$digest` $digest(adapter); expect(controllerDoCheckA.calls.count()).toBe(2); expect(controllerDoCheckB.calls.count()).toBe(2); }); })); it('should not call `$doCheck()` on scope', waitForAsync(() => { const scopeDoCheck = jasmine.createSpy('scopeDoCheck'); // Define `ng1Directive` const ng1DirectiveA: angular.IDirective = { template: 'ng1A', bindToController: false, controller: class { constructor(private $scope: angular.IScope) { $scope['$doCheck'] = scopeDoCheck; } } }; const ng1DirectiveB: angular.IDirective = { template: 'ng1B', bindToController: true, controller: class { constructor(private $scope: angular.IScope) { $scope['$doCheck'] = scopeDoCheck; } } }; // 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(adapter => { // Initial change expect(scopeDoCheck).not.toHaveBeenCalled(); // Run a `$digest` $digest(adapter); expect(scopeDoCheck).not.toHaveBeenCalled(); // Run another `$digest` $digest(adapter); expect(scopeDoCheck).not.toHaveBeenCalled(); }); })); it('should call `$onDestroy()` on controller', waitForAsync(() => { const controllerOnDestroyA = jasmine.createSpy('controllerOnDestroyA'); const controllerOnDestroyB = jasmine.createSpy('controllerOnDestroyB'); // 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 { // TODO(issue/24571): remove '!'. @Input() show!: boolean; } // 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 => { 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', waitForAsync(() => { const scopeOnDestroy = jasmine.createSpy('scopeOnDestroy'); // 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 { // TODO(issue/24571): remove '!'. @Input() show!: boolean; } // 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 => { 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()` > `$doCheck()` > `$postLink()`', waitForAsync(() => { // Define `ng1Component` const ng1Component: angular.IComponent = { // `$doCheck()` will keep getting called as long as the interpolated value keeps // changing (by appending `> $doCheck`). Only care about the first 4 values. template: '{{ $ctrl.calls.slice(0, 4).join(" > ") }}', bindings: {value: '<'}, controller: class { calls: string[] = []; $onChanges() { this.calls.push('$onChanges'); } $onInit() { this.calls.push('$onInit'); } $doCheck() { this.calls.push('$doCheck'); } $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 > $doCheck > $postLink'); }); })); }); describe('destroying the upgraded component', () => { it('should destroy `$componentScope`', waitForAsync(() => { 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).toHaveBeenCalledTimes(1); }); })); it('should emit `$destroy` on `$element` and descendants', waitForAsync(() => { const elementDestroyListener = jasmine.createSpy('elementDestroyListener'); const descendantDestroyListener = jasmine.createSpy('descendantDestroyListener'); let ng2ComponentAInstance: Ng2ComponentA; // Define `ng1Component` const ng1Component: angular.IComponent = { controller: class { constructor($element: angular.IAugmentedJQuery) { $element.on!('$destroy', elementDestroyListener); $element.contents!().on!('$destroy', descendantDestroyListener); } }, template: '
' }; // 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(elementDestroyListener).not.toHaveBeenCalled(); expect(descendantDestroyListener).not.toHaveBeenCalled(); ng2ComponentAInstance.destroyIt = true; $digest(adapter); expect(elementDestroyListener).toHaveBeenCalledTimes(1); expect(descendantDestroyListener).toHaveBeenCalledTimes(1); }); })); it('should clear data on `$element` and descendants`', waitForAsync(() => { let ng1ComponentElement: angular.IAugmentedJQuery; let ng2ComponentAInstance: Ng2ComponentA; // Define `ng1Component` const ng1Component: angular.IComponent = { controller: class { constructor($element: angular.IAugmentedJQuery) { $element.data!('test', 1); $element.contents!().data!('test', 2); ng1ComponentElement = $element; } }, template: '
' }; // 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(ng1ComponentElement.data!('test')).toBe(1); expect(ng1ComponentElement.contents!().data!('test')).toBe(2); ng2ComponentAInstance.destroyIt = true; $digest(adapter); expect(ng1ComponentElement.data!('test')).toBeUndefined(); expect(ng1ComponentElement.contents!().data!('test')).toBeUndefined(); }); })); it('should clear dom listeners on `$element` and descendants`', waitForAsync(() => { const elementClickListener = jasmine.createSpy('elementClickListener'); const descendantClickListener = jasmine.createSpy('descendantClickListener'); let ng1DescendantElement: angular.IAugmentedJQuery; let ng2ComponentAInstance: Ng2ComponentA; // Define `ng1Component` const ng1Component: angular.IComponent = { controller: class { constructor($element: angular.IAugmentedJQuery) { ng1DescendantElement = $element.contents!(); $element.on!('click', elementClickListener); ng1DescendantElement.on!('click', descendantClickListener); } }, template: '
' }; // 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 => { (ng1DescendantElement[0] as HTMLElement).click(); expect(elementClickListener).toHaveBeenCalledTimes(1); expect(descendantClickListener).toHaveBeenCalledTimes(1); ng2ComponentAInstance.destroyIt = true; $digest(adapter); (ng1DescendantElement[0] as HTMLElement).click(); expect(elementClickListener).toHaveBeenCalledTimes(1); expect(descendantClickListener).toHaveBeenCalledTimes(1); }); })); it('should clean up `$doCheck()` watchers from the parent scope', waitForAsync(() => { let ng2Component: Ng2Component; // Define `ng1Component` const ng1Component: angular.IComponent = { template: 'ng1', controller: class { $doCheck() {} } }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { constructor(elementRef: ElementRef, injector: Injector) { super('ng1', elementRef, injector); } } // Define `Ng2Component` @Component({selector: 'ng2', template: ''}) class Ng2Component { doShow: boolean = false; constructor(@Inject($SCOPE) public $scope: angular.IScope) { ng2Component = this; } } // Define `ng1Module` const ng1Module = angular.module_('ng1Module', []) .component('ng1', ng1Component) .directive('ng2', downgradeComponent({component: Ng2Component})); // Define `Ng2Module` @NgModule({ imports: [BrowserModule, UpgradeModule], declarations: [Ng1ComponentFacade, Ng2Component], entryComponents: [Ng2Component] }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const element = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => { const getWatcherCount: () => number = () => (ng2Component.$scope as any).$$watchers.length; const baseWatcherCount = getWatcherCount(); expect(multiTrim(document.body.textContent)).toBe(''); ng2Component.doShow = true; $digest(adapter); expect(multiTrim(document.body.textContent)).toBe('ng1'); expect(getWatcherCount()).toBe(baseWatcherCount + 1); ng2Component.doShow = false; $digest(adapter); expect(multiTrim(document.body.textContent)).toBe(''); expect(getWatcherCount()).toBe(baseWatcherCount); ng2Component.doShow = true; $digest(adapter); expect(multiTrim(document.body.textContent)).toBe('ng1'); expect(getWatcherCount()).toBe(baseWatcherCount + 1); }); })); }); it('should support ng2 > ng1 > ng2 (no inputs/outputs)', waitForAsync(() => { // Define `ng1Component` const ng1Component: angular.IComponent = {template: 'ng1X()'}; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1X'}) class Ng1ComponentFacade extends UpgradeComponent { constructor(elementRef: ElementRef, injector: Injector) { super('ng1X', elementRef, injector); } } // Define `Ng2Component` @Component({selector: 'ng2-a', template: 'ng2A()'}) class Ng2ComponentA { } @Component({selector: 'ng2-b', template: 'ng2B'}) class Ng2ComponentB { } // Define `ng1Module` const ng1Module = angular.module_('ng1', []) .component('ng1X', ng1Component) .directive('ng2A', downgradeComponent({component: Ng2ComponentA})) .directive('ng2B', downgradeComponent({component: Ng2ComponentB})); // Define `Ng2Module` @NgModule({ declarations: [Ng1ComponentFacade, Ng2ComponentA, Ng2ComponentB], entryComponents: [Ng2ComponentA, Ng2ComponentB], imports: [BrowserModule, UpgradeModule], schemas: [NO_ERRORS_SCHEMA], }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const element = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => { expect(multiTrim(document.body.textContent)).toBe('ng2A(ng1X(ng2B))'); }); })); it('should support ng2 > ng1 > ng2 (with inputs/outputs)', fakeAsync(() => { let ng2ComponentAInstance: Ng2ComponentA; let ng2ComponentBInstance: Ng2ComponentB; let ng1ControllerXInstance: Ng1ControllerX; // Define `ng1Component` class Ng1ControllerX { // TODO(issue/24571): remove '!'. ng1XInputA!: string; ng1XInputB: any; ng1XInputC: any; constructor() { ng1ControllerXInstance = this; } } const ng1Component: angular.IComponent = { template: ` ng1X({{ $ctrl.ng1XInputA }}, {{ $ctrl.ng1XInputB.value }}, {{ $ctrl.ng1XInputC.value }}) | `, bindings: { ng1XInputA: '@', ng1XInputB: '<', ng1XInputC: '=', ng1XOutputA: '&', ng1XOutputB: '&' }, controller: Ng1ControllerX }; // Define `Ng1ComponentFacade` @Directive({selector: 'ng1X'}) class Ng1ComponentXFacade extends UpgradeComponent { // TODO(issue/24571): remove '!'. @Input() ng1XInputA!: string; @Input() ng1XInputB: any; @Input() ng1XInputC: any; // TODO(issue/24571): remove '!'. @Output() ng1XInputCChange!: EventEmitter; // TODO(issue/24571): remove '!'. @Output() ng1XOutputA!: EventEmitter; // TODO(issue/24571): remove '!'. @Output() ng1XOutputB!: EventEmitter; constructor(elementRef: ElementRef, injector: Injector) { super('ng1X', elementRef, injector); } } // Define `Ng2Component` @Component({ selector: 'ng2-a', template: ` ng2A({{ ng2ADataA.value }}, {{ ng2ADataB.value }}, {{ ng2ADataC.value }}) | ` }) class Ng2ComponentA { ng2ADataA = {value: 'foo'}; ng2ADataB = {value: 'bar'}; ng2ADataC = {value: 'baz'}; constructor() { ng2ComponentAInstance = this; } } @Component({selector: 'ng2-b', template: 'ng2B({{ ng2BInputA }}, {{ ng2BInputC }})'}) class Ng2ComponentB { @Input('ng2BInput1') ng2BInputA: any; @Input() ng2BInputC: any; @Output() ng2BOutputC = new EventEmitter(); constructor() { ng2ComponentBInstance = this; } } // Define `ng1Module` const ng1Module = angular.module_('ng1', []) .component('ng1X', ng1Component) .directive('ng2A', downgradeComponent({component: Ng2ComponentA})) .directive('ng2B', downgradeComponent({component: Ng2ComponentB})); // Define `Ng2Module` @NgModule({ declarations: [Ng1ComponentXFacade, Ng2ComponentA, Ng2ComponentB], entryComponents: [Ng2ComponentA, Ng2ComponentB], imports: [BrowserModule, UpgradeModule], schemas: [NO_ERRORS_SCHEMA], }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const element = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => { // Initial value propagation. // (ng2A > ng1X > ng2B) expect(multiTrim(document.body.textContent)) .toBe('ng2A(foo, bar, baz) | ng1X(foo, bar, baz) | ng2B(foo, baz)'); // Update `ng2BInputA`/`ng2BInputC`. // (Should not propagate upwards.) ng2ComponentBInstance.ng2BInputA = 'foo2'; ng2ComponentBInstance.ng2BInputC = 'baz2'; $digest(adapter); tick(); expect(multiTrim(document.body.textContent)) .toBe('ng2A(foo, bar, baz) | ng1X(foo, bar, baz) | ng2B(foo2, baz2)'); // Emit from `ng2BOutputC`. // (Should propagate all the way up to `ng1ADataC` and back all the way down to // `ng2BInputC`.) ng2ComponentBInstance.ng2BOutputC.emit('baz3'); $digest(adapter); tick(); expect(multiTrim(document.body.textContent)) .toBe('ng2A(foo, bar, baz3) | ng1X(foo, bar, baz3) | ng2B(foo2, baz3)'); // Update `ng1XInputA`/`ng1XInputB`. // (Should not propagate upwards, only downwards.) ng1ControllerXInstance.ng1XInputA = 'foo4'; ng1ControllerXInstance.ng1XInputB = {value: 'bar4'}; $digest(adapter); tick(); expect(multiTrim(document.body.textContent)) .toBe('ng2A(foo, bar, baz3) | ng1X(foo4, bar4, baz3) | ng2B(foo4, baz3)'); // Update `ng1XInputC`. // (Should propagate upwards and downwards.) ng1ControllerXInstance.ng1XInputC = {value: 'baz5'}; $digest(adapter); tick(); expect(multiTrim(document.body.textContent)) .toBe('ng2A(foo, bar, baz5) | ng1X(foo4, bar4, baz5) | ng2B(foo4, baz5)'); // Update a property on `ng1XInputC`. // (Should propagate upwards and downwards.) ng1ControllerXInstance.ng1XInputC.value = 'baz6'; $digest(adapter); tick(); expect(multiTrim(document.body.textContent)) .toBe('ng2A(foo, bar, baz6) | ng1X(foo4, bar4, baz6) | ng2B(foo4, baz6)'); // Emit from `ng1XOutputA`. // (Should propagate upwards to `ng1ADataA` and back all the way down to // `ng2BInputA`.) (ng1ControllerXInstance as any).ng1XOutputA({value: 'foo7'}); $digest(adapter); tick(); expect(multiTrim(document.body.textContent)) .toBe('ng2A(foo7, bar, baz6) | ng1X(foo7, bar4, baz6) | ng2B(foo7, baz6)'); // Emit from `ng1XOutputB`. // (Should propagate upwards to `ng1ADataB`, but not downwards, // since `ng1XInputB` has been re-assigned (i.e. `ng2ADataB !== ng1XInputB`).) (ng1ControllerXInstance as any).ng1XOutputB('bar8'); $digest(adapter); tick(); expect(multiTrim(document.body.textContent)) .toBe('ng2A(foo7, bar8, baz6) | ng1X(foo7, bar4, baz6) | ng2B(foo7, baz6)'); // Update `ng2ADataA`/`ng2ADataB`/`ng2ADataC`. // (Should propagate everywhere.) ng2ComponentAInstance.ng2ADataA = {value: 'foo9'}; ng2ComponentAInstance.ng2ADataB = {value: 'bar9'}; ng2ComponentAInstance.ng2ADataC = {value: 'baz9'}; $digest(adapter); tick(); expect(multiTrim(document.body.textContent)) .toBe('ng2A(foo9, bar9, baz9) | ng1X(foo9, bar9, baz9) | ng2B(foo9, baz9)'); }); })); it('should support ng2 > ng1 > ng2 > ng1 (with `require`)', waitForAsync(() => { // Define `ng1Component` const ng1ComponentA: angular.IComponent = { template: 'ng1A()', controller: class { value = 'ng1A'; } }; const ng1ComponentB: angular.IComponent = { template: 'ng1B(^^ng1A: {{ $ctrl.ng1A.value }} | ?^^ng1B: {{ $ctrl.ng1B.value }} | ^ng1B: {{ $ctrl.ng1BSelf.value }})', require: {ng1A: '^^', ng1B: '?^^', ng1BSelf: '^ng1B'}, controller: class { value = 'ng1B'; } }; // 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-a', template: 'ng2A()'}) class Ng2ComponentA { } @Component({selector: 'ng2-b', template: 'ng2B()'}) class Ng2ComponentB { } // Define `ng1Module` const ng1Module = angular.module_('ng1', []) .component('ng1A', ng1ComponentA) .component('ng1B', ng1ComponentB) .directive('ng2A', downgradeComponent({component: Ng2ComponentA})) .directive('ng2B', downgradeComponent({component: Ng2ComponentB})); // Define `Ng2Module` @NgModule({ declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2ComponentA, Ng2ComponentB], entryComponents: [Ng2ComponentA, Ng2ComponentB], imports: [BrowserModule, UpgradeModule], schemas: [NO_ERRORS_SCHEMA], }) class Ng2Module { ngDoBootstrap() {} } // Bootstrap const element = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => { expect(multiTrim(document.body.textContent)) .toBe('ng2A(ng1A(ng2B(ng1B(^^ng1A: ng1A | ?^^ng1B: | ^ng1B: ng1B))))'); }); })); }); });