/** * @license * Copyright Google Inc. 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, EventEmitter, NgModule, OnChanges, OnDestroy, SimpleChanges, destroyPlatform} from '@angular/core'; import {async} from '@angular/core/testing'; import {BrowserModule} from '@angular/platform-browser'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import * as angular from '@angular/upgrade/src/common/angular1'; import {UpgradeModule, downgradeComponent} from '@angular/upgrade/static'; import {$apply, bootstrap, html, multiTrim} from '../test_helpers'; export function main() { describe('downgrade ng2 component', () => { beforeEach(() => destroyPlatform()); afterEach(() => destroyPlatform()); it('should bind properties, events', async(() => { const ng1Module = angular.module('ng1', []).value('$exceptionHandler', (err: any) => { throw err; }).run(($rootScope: angular.IScope) => { $rootScope['name'] = 'world'; $rootScope['dataA'] = 'A'; $rootScope['dataB'] = 'B'; $rootScope['modelA'] = 'initModelA'; $rootScope['modelB'] = 'initModelB'; $rootScope['eventA'] = '?'; $rootScope['eventB'] = '?'; }); @Component({ selector: 'ng2', inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'], outputs: [ 'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange', 'twoWayBEmitter: twoWayBChange' ], template: 'ignore: {{ignore}}; ' + 'literal: {{literal}}; interpolate: {{interpolate}}; ' + 'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' + 'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})' }) class Ng2Component implements OnChanges { ngOnChangesCount = 0; ignore = '-'; literal = '?'; interpolate = '?'; oneWayA = '?'; oneWayB = '?'; twoWayA = '?'; twoWayB = '?'; eventA = new EventEmitter(); eventB = new EventEmitter(); twoWayAEmitter = new EventEmitter(); twoWayBEmitter = new EventEmitter(); ngOnChanges(changes: SimpleChanges) { const assert = (prop: string, value: any) => { const propVal = (this as any)[prop]; if (propVal != value) { throw new Error(`Expected: '${prop}' to be '${value}' but was '${propVal}'`); } }; const assertChange = (prop: string, value: any) => { assert(prop, value); if (!changes[prop]) { throw new Error(`Changes record for '${prop}' not found.`); } const actualValue = changes[prop].currentValue; if (actualValue != value) { throw new Error( `Expected changes record for'${prop}' to be '${value}' but was '${actualValue}'`); } }; switch (this.ngOnChangesCount++) { case 0: assert('ignore', '-'); assertChange('literal', 'Text'); assertChange('interpolate', 'Hello world'); assertChange('oneWayA', 'A'); assertChange('oneWayB', 'B'); assertChange('twoWayA', 'initModelA'); assertChange('twoWayB', 'initModelB'); this.twoWayAEmitter.emit('newA'); this.twoWayBEmitter.emit('newB'); this.eventA.emit('aFired'); this.eventB.emit('bFired'); break; case 1: assertChange('twoWayA', 'newA'); assertChange('twoWayB', 'newB'); break; case 2: assertChange('interpolate', 'Hello everyone'); break; default: throw new Error('Called too many times! ' + JSON.stringify(changes)); } }; } ng1Module.directive('ng2', downgradeComponent({ component: Ng2Component, })); @NgModule({ declarations: [Ng2Component], entryComponents: [Ng2Component], imports: [BrowserModule, UpgradeModule] }) class Ng2Module { ngDoBootstrap() {} } const element = html(`
| modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}};
`); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { expect(multiTrim(document.body.textContent)) .toEqual( 'ignore: -; ' + 'literal: Text; interpolate: Hello world; ' + 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (2) | ' + 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); $apply(upgrade, 'name = "everyone"'); expect(multiTrim(document.body.textContent)) .toEqual( 'ignore: -; ' + 'literal: Text; interpolate: Hello everyone; ' + 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | ' + 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); }); })); it('should bind to ng-model', async(() => { const ng1Module = angular.module('ng1', []).run( ($rootScope: angular.IScope) => { $rootScope['modelA'] = 'A'; }); let ng2Instance: Ng2; @Component({selector: 'ng2', template: '{{_value}}'}) class Ng2 { private _value: any = ''; private _onChangeCallback: (_: any) => void = () => {}; constructor() { ng2Instance = this; } writeValue(value: any) { this._value = value; } registerOnChange(fn: any) { this._onChangeCallback = fn; } doChange(newValue: string) { this._value = newValue; this._onChangeCallback(newValue); } } ng1Module.directive('ng2', downgradeComponent({component: Ng2})); const element = html(`
| {{modelA}}
`); @NgModule( {declarations: [Ng2], entryComponents: [Ng2], imports: [BrowserModule, UpgradeModule]}) class Ng2Module { ngDoBootstrap() {} } platformBrowserDynamic().bootstrapModule(Ng2Module).then((ref) => { const adapter = ref.injector.get(UpgradeModule) as UpgradeModule; adapter.bootstrap(element, [ng1Module.name]); const $rootScope = adapter.$injector.get('$rootScope'); expect(multiTrim(document.body.textContent)).toEqual('A | A'); $rootScope.modelA = 'B'; $rootScope.$apply(); expect(multiTrim(document.body.textContent)).toEqual('B | B'); ng2Instance.doChange('C'); expect($rootScope.modelA).toBe('C'); expect(multiTrim(document.body.textContent)).toEqual('C | C'); }); })); it('should properly run cleanup when ng1 directive is destroyed', async(() => { let destroyed = false; @Component({selector: 'ng2', template: 'test'}) class Ng2Component implements OnDestroy { ngOnDestroy() { destroyed = true; } } @NgModule({ declarations: [Ng2Component], entryComponents: [Ng2Component], imports: [BrowserModule, UpgradeModule] }) class Ng2Module { ngDoBootstrap() {} } const ng1Module = angular.module('ng1', []) .directive( 'ng1', () => { return {template: '
'}; }) .directive('ng2', downgradeComponent({component: Ng2Component})); const element = html(''); platformBrowserDynamic().bootstrapModule(Ng2Module).then((ref) => { const adapter = ref.injector.get(UpgradeModule) as UpgradeModule; adapter.bootstrap(element, [ng1Module.name]); expect(element.textContent).toContain('test'); expect(destroyed).toBe(false); const $rootScope = adapter.$injector.get('$rootScope'); $rootScope.$apply('destroyIt = true'); expect(element.textContent).not.toContain('test'); expect(destroyed).toBe(true); }); })); it('should work when compiled outside the dom (by fallback to the root ng2.injector)', async(() => { @Component({selector: 'ng2', template: 'test'}) class Ng2Component { } @NgModule({ declarations: [Ng2Component], entryComponents: [Ng2Component], imports: [BrowserModule, UpgradeModule] }) class Ng2Module { ngDoBootstrap() {} } const ng1Module = angular.module('ng1', []) .directive( 'ng1', [ '$compile', ($compile: angular.ICompileService) => { return { link: function( $scope: angular.IScope, $element: angular.IAugmentedJQuery, $attrs: angular.IAttributes) { // here we compile some HTML that contains a downgraded component // since it is not currently in the DOM it is not able to "require" // an ng2 injector so it should use the `moduleInjector` instead. const compiled = $compile(''); const template = compiled($scope); $element.append(template); } }; } ]) .directive('ng2', downgradeComponent({component: Ng2Component})); const element = html(''); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { // the fact that the body contains the correct text means that the // downgraded component was able to access the moduleInjector // (since there is no other injector in this system) expect(multiTrim(document.body.textContent)).toEqual('test'); }); })); it('should allow attribute selectors for downgraded components', async(() => { @Component({selector: '[itWorks]', template: 'It works'}) class WorksComponent { } @NgModule({ declarations: [WorksComponent], entryComponents: [WorksComponent], imports: [BrowserModule, UpgradeModule] }) class Ng2Module { ngDoBootstrap() {} } const ng1Module = angular.module('ng1', []).directive( 'worksComponent', downgradeComponent({component: WorksComponent})); const element = html(''); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { expect(multiTrim(document.body.textContent)).toBe('It works'); }); })); it('should allow attribute selectors for components in ng2', async(() => { @Component({selector: '[itWorks]', template: 'It works'}) class WorksComponent { } @Component({selector: 'root-component', template: '!'}) class RootComponent { } @NgModule({ declarations: [RootComponent, WorksComponent], entryComponents: [RootComponent], imports: [BrowserModule, UpgradeModule] }) class Ng2Module { ngDoBootstrap() {} } const ng1Module = angular.module('ng1', []).directive( 'rootComponent', downgradeComponent({component: RootComponent})); const element = html(''); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { expect(multiTrim(document.body.textContent)).toBe('It works!'); }); })); it('should respect hierarchical dependency injection for ng2', async(() => { @Component({selector: 'parent', template: 'parent()'}) class ParentComponent { } @Component({selector: 'child', template: 'child'}) class ChildComponent { constructor(parent: ParentComponent) {} } @NgModule({ declarations: [ParentComponent, ChildComponent], entryComponents: [ParentComponent, ChildComponent], imports: [BrowserModule, UpgradeModule] }) class Ng2Module { ngDoBootstrap() {} } const ng1Module = angular.module('ng1', []) .directive('parent', downgradeComponent({component: ParentComponent})) .directive('child', downgradeComponent({component: ChildComponent})); const element = html(''); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => { expect(multiTrim(document.body.textContent)).toBe('parent(child)'); }); })); }); }