828 lines
32 KiB
TypeScript
828 lines
32 KiB
TypeScript
/**
|
|
* @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 {ChangeDetectionStrategy, ChangeDetectorRef, Compiler, Component, ComponentFactoryResolver, Directive, ElementRef, EventEmitter, Injector, Input, NgModule, NgModuleRef, OnChanges, OnDestroy, Output, SimpleChanges, destroyPlatform} from '@angular/core';
|
|
import {async, fakeAsync, tick} from '@angular/core/testing';
|
|
import {BrowserModule} from '@angular/platform-browser';
|
|
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
|
import {fixmeIvy} from '@angular/private/testing';
|
|
import {UpgradeComponent, UpgradeModule, downgradeComponent} from '@angular/upgrade/static';
|
|
import * as angular from '@angular/upgrade/static/src/common/angular1';
|
|
|
|
import {$apply, bootstrap, html, multiTrim, withEachNg1Version} from '../test_helpers';
|
|
|
|
withEachNg1Version(() => {
|
|
describe('downgrade ng2 component', () => {
|
|
|
|
beforeEach(() => destroyPlatform());
|
|
afterEach(() => destroyPlatform());
|
|
|
|
fixmeIvy('FW-716: Error: [$rootScope:inprog] $digest already in progress')
|
|
.it('should bind properties, events', async(() => {
|
|
const ng1Module = angular.module('ng1', []).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(`
|
|
<div>
|
|
<ng2 literal="Text" interpolate="Hello {{name}}"
|
|
bind-one-way-a="dataA" [one-way-b]="dataB"
|
|
bindon-two-way-a="modelA" [(two-way-b)]="modelB"
|
|
on-event-a='eventA=$event' (event-b)="eventB=$event"></ng2>
|
|
| modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}};
|
|
</div>`);
|
|
|
|
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 properties to onpush components', async(() => {
|
|
const ng1Module = angular.module('ng1', []).run(
|
|
($rootScope: angular.IScope) => { $rootScope['dataB'] = 'B'; });
|
|
|
|
@Component({
|
|
selector: 'ng2',
|
|
inputs: ['oneWayB'],
|
|
template: 'oneWayB: {{oneWayB}}',
|
|
changeDetection: ChangeDetectionStrategy.OnPush
|
|
})
|
|
|
|
class Ng2Component {
|
|
ngOnChangesCount = 0;
|
|
oneWayB = '?';
|
|
}
|
|
|
|
ng1Module.directive('ng2', downgradeComponent({
|
|
component: Ng2Component,
|
|
}));
|
|
|
|
@NgModule({
|
|
declarations: [Ng2Component],
|
|
entryComponents: [Ng2Component],
|
|
imports: [BrowserModule, UpgradeModule]
|
|
})
|
|
class Ng2Module {
|
|
ngDoBootstrap() {}
|
|
}
|
|
|
|
const element = html(`
|
|
<div>
|
|
<ng2 [one-way-b]="dataB"></ng2>
|
|
</div>`);
|
|
|
|
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
|
|
expect(multiTrim(document.body.textContent)).toEqual('oneWayB: B');
|
|
$apply(upgrade, 'dataB= "everyone"');
|
|
expect(multiTrim(document.body.textContent)).toEqual('oneWayB: everyone');
|
|
});
|
|
}));
|
|
|
|
fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly')
|
|
.it('should support two-way binding and event listener', async(() => {
|
|
const listenerSpy = jasmine.createSpy('$rootScope.listener');
|
|
const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => {
|
|
$rootScope['value'] = 'world';
|
|
$rootScope['listener'] = listenerSpy;
|
|
});
|
|
|
|
@Component({selector: 'ng2', template: `model: {{model}};`})
|
|
class Ng2Component implements OnChanges {
|
|
ngOnChangesCount = 0;
|
|
@Input() model = '?';
|
|
@Output() modelChange = new EventEmitter();
|
|
|
|
ngOnChanges(changes: SimpleChanges) {
|
|
switch (this.ngOnChangesCount++) {
|
|
case 0:
|
|
expect(changes.model.currentValue).toBe('world');
|
|
this.modelChange.emit('newC');
|
|
break;
|
|
case 1:
|
|
expect(changes.model.currentValue).toBe('newC');
|
|
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(`
|
|
<div>
|
|
<ng2 [(model)]="value" (model-change)="listener($event)"></ng2>
|
|
| value: {{value}}
|
|
</div>
|
|
`);
|
|
|
|
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
|
|
expect(multiTrim(element.textContent)).toEqual('model: newC; | value: newC');
|
|
expect(listenerSpy).toHaveBeenCalledWith('newC');
|
|
});
|
|
}));
|
|
|
|
it('should run change-detection on every digest (by default)', async(() => {
|
|
let ng2Component: Ng2Component;
|
|
|
|
@Component({selector: 'ng2', template: '{{ value1 }} | {{ value2 }}'})
|
|
class Ng2Component {
|
|
@Input() value1 = -1;
|
|
@Input() value2 = -1;
|
|
|
|
constructor() { ng2Component = this; }
|
|
}
|
|
|
|
@NgModule({
|
|
imports: [BrowserModule, UpgradeModule],
|
|
declarations: [Ng2Component],
|
|
entryComponents: [Ng2Component]
|
|
})
|
|
class Ng2Module {
|
|
ngDoBootstrap() {}
|
|
}
|
|
|
|
const ng1Module = angular.module('ng1', [])
|
|
.directive('ng2', downgradeComponent({component: Ng2Component}))
|
|
.run(($rootScope: angular.IRootScopeService) => {
|
|
$rootScope.value1 = 0;
|
|
$rootScope.value2 = 0;
|
|
});
|
|
|
|
const element = html('<ng2 [value1]="value1" value2="{{ value2 }}"></ng2>');
|
|
|
|
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
|
|
const $rootScope = upgrade.$injector.get('$rootScope') as angular.IRootScopeService;
|
|
|
|
expect(element.textContent).toBe('0 | 0');
|
|
|
|
// Digest should invoke CD
|
|
$rootScope.$digest();
|
|
$rootScope.$digest();
|
|
expect(element.textContent).toBe('0 | 0');
|
|
|
|
// Internal changes should be detected on digest
|
|
ng2Component.value1 = 1;
|
|
ng2Component.value2 = 2;
|
|
$rootScope.$digest();
|
|
expect(element.textContent).toBe('1 | 2');
|
|
|
|
// Digest should propagate change in prop-bound input
|
|
$rootScope.$apply('value1 = 3');
|
|
expect(element.textContent).toBe('3 | 2');
|
|
|
|
// Digest should propagate change in attr-bound input
|
|
ng2Component.value1 = 4;
|
|
$rootScope.$apply('value2 = 5');
|
|
expect(element.textContent).toBe('4 | 5');
|
|
|
|
// Digest should propagate changes that happened before the digest
|
|
$rootScope.value1 = 6;
|
|
expect(element.textContent).toBe('4 | 5');
|
|
|
|
$rootScope.$digest();
|
|
expect(element.textContent).toBe('6 | 5');
|
|
});
|
|
}));
|
|
|
|
it('should not run change-detection on every digest when opted out', async(() => {
|
|
let ng2Component: Ng2Component;
|
|
|
|
@Component({selector: 'ng2', template: '{{ value1 }} | {{ value2 }}'})
|
|
class Ng2Component {
|
|
@Input() value1 = -1;
|
|
@Input() value2 = -1;
|
|
|
|
constructor() { ng2Component = this; }
|
|
}
|
|
|
|
@NgModule({
|
|
imports: [BrowserModule, UpgradeModule],
|
|
declarations: [Ng2Component],
|
|
entryComponents: [Ng2Component]
|
|
})
|
|
class Ng2Module {
|
|
ngDoBootstrap() {}
|
|
}
|
|
|
|
const ng1Module =
|
|
angular.module('ng1', [])
|
|
.directive(
|
|
'ng2', downgradeComponent({component: Ng2Component, propagateDigest: false}))
|
|
.run(($rootScope: angular.IRootScopeService) => {
|
|
$rootScope.value1 = 0;
|
|
$rootScope.value2 = 0;
|
|
});
|
|
|
|
const element = html('<ng2 [value1]="value1" value2="{{ value2 }}"></ng2>');
|
|
|
|
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
|
|
const $rootScope = upgrade.$injector.get('$rootScope') as angular.IRootScopeService;
|
|
|
|
expect(element.textContent).toBe('0 | 0');
|
|
|
|
// Digest should not invoke CD
|
|
$rootScope.$digest();
|
|
$rootScope.$digest();
|
|
expect(element.textContent).toBe('0 | 0');
|
|
|
|
// Digest should not invoke CD, even if component values have changed (internally)
|
|
ng2Component.value1 = 1;
|
|
ng2Component.value2 = 2;
|
|
$rootScope.$digest();
|
|
expect(element.textContent).toBe('0 | 0');
|
|
|
|
// Digest should invoke CD, if prop-bound input has changed
|
|
$rootScope.$apply('value1 = 3');
|
|
expect(element.textContent).toBe('3 | 2');
|
|
|
|
// Digest should invoke CD, if attr-bound input has changed
|
|
ng2Component.value1 = 4;
|
|
$rootScope.$apply('value2 = 5');
|
|
expect(element.textContent).toBe('4 | 5');
|
|
|
|
// Digest should invoke CD, if input has changed before the digest
|
|
$rootScope.value1 = 6;
|
|
$rootScope.$digest();
|
|
expect(element.textContent).toBe('6 | 5');
|
|
});
|
|
}));
|
|
|
|
it('should still run normal Angular change-detection regardless of `propagateDigest`',
|
|
fakeAsync(() => {
|
|
let ng2Component: Ng2Component;
|
|
|
|
@Component({selector: 'ng2', template: '{{ value }}'})
|
|
class Ng2Component {
|
|
value = 'foo';
|
|
constructor() { setTimeout(() => this.value = 'bar', 1000); }
|
|
}
|
|
|
|
@NgModule({
|
|
imports: [BrowserModule, UpgradeModule],
|
|
declarations: [Ng2Component],
|
|
entryComponents: [Ng2Component]
|
|
})
|
|
class Ng2Module {
|
|
ngDoBootstrap() {}
|
|
}
|
|
|
|
const ng1Module =
|
|
angular.module('ng1', [])
|
|
.directive(
|
|
'ng2A', downgradeComponent({component: Ng2Component, propagateDigest: true}))
|
|
.directive(
|
|
'ng2B', downgradeComponent({component: Ng2Component, propagateDigest: false}));
|
|
|
|
const element = html('<ng2-a></ng2-a> | <ng2-b></ng2-b>');
|
|
|
|
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
|
|
expect(element.textContent).toBe('foo | foo');
|
|
|
|
tick(1000);
|
|
expect(element.textContent).toBe('bar | bar');
|
|
});
|
|
}));
|
|
|
|
fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly')
|
|
.it('should initialize inputs in time for `ngOnChanges`', async(() => {
|
|
@Component({
|
|
selector: 'ng2',
|
|
template: `
|
|
ngOnChangesCount: {{ ngOnChangesCount }} |
|
|
firstChangesCount: {{ firstChangesCount }} |
|
|
initialValue: {{ initialValue }}`
|
|
})
|
|
class Ng2Component implements OnChanges {
|
|
ngOnChangesCount = 0;
|
|
firstChangesCount = 0;
|
|
// TODO(issue/24571): remove '!'.
|
|
initialValue !: string;
|
|
// TODO(issue/24571): remove '!'.
|
|
@Input() foo !: string;
|
|
|
|
ngOnChanges(changes: SimpleChanges) {
|
|
this.ngOnChangesCount++;
|
|
|
|
if (this.ngOnChangesCount === 1) {
|
|
this.initialValue = this.foo;
|
|
}
|
|
|
|
if (changes['foo'] && changes['foo'].isFirstChange()) {
|
|
this.firstChangesCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
@NgModule({
|
|
imports: [BrowserModule, UpgradeModule],
|
|
declarations: [Ng2Component],
|
|
entryComponents: [Ng2Component]
|
|
})
|
|
class Ng2Module {
|
|
ngDoBootstrap() {}
|
|
}
|
|
|
|
const ng1Module = angular.module('ng1', []).directive(
|
|
'ng2', downgradeComponent({component: Ng2Component}));
|
|
|
|
const element = html(`
|
|
<ng2 [foo]="'foo'"></ng2>
|
|
<ng2 foo="bar"></ng2>
|
|
<ng2 [foo]="'baz'" ng-if="true"></ng2>
|
|
<ng2 foo="qux" ng-if="true"></ng2>
|
|
`);
|
|
|
|
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
|
|
const nodes = element.querySelectorAll('ng2');
|
|
const expectedTextWith = (value: string) =>
|
|
`ngOnChangesCount: 1 | firstChangesCount: 1 | initialValue: ${value}`;
|
|
|
|
expect(multiTrim(nodes[0].textContent)).toBe(expectedTextWith('foo'));
|
|
expect(multiTrim(nodes[1].textContent)).toBe(expectedTextWith('bar'));
|
|
expect(multiTrim(nodes[2].textContent)).toBe(expectedTextWith('baz'));
|
|
expect(multiTrim(nodes[3].textContent)).toBe(expectedTextWith('qux'));
|
|
});
|
|
}));
|
|
|
|
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: '<span>{{_value}}</span>'})
|
|
class Ng2 {
|
|
private _value: any = '';
|
|
private _onChangeCallback: (_: any) => void = () => {};
|
|
private _onTouchedCallback: () => void = () => {};
|
|
constructor() { ng2Instance = this; }
|
|
writeValue(value: any) { this._value = value; }
|
|
registerOnChange(fn: any) { this._onChangeCallback = fn; }
|
|
registerOnTouched(fn: any) { this._onTouchedCallback = fn; }
|
|
doTouch() { this._onTouchedCallback(); }
|
|
doChange(newValue: string) {
|
|
this._value = newValue;
|
|
this._onChangeCallback(newValue);
|
|
}
|
|
}
|
|
|
|
ng1Module.directive('ng2', downgradeComponent({component: Ng2}));
|
|
|
|
const element = html(`<div><ng2 ng-model="modelA"></ng2> | {{modelA}}</div>`);
|
|
|
|
@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');
|
|
|
|
const downgradedElement = <Element>document.body.querySelector('ng2');
|
|
expect(downgradedElement.classList.contains('ng-touched')).toBe(false);
|
|
|
|
ng2Instance.doTouch();
|
|
$rootScope.$apply();
|
|
expect(downgradedElement.classList.contains('ng-touched')).toBe(true);
|
|
});
|
|
}));
|
|
|
|
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: '<div ng-if="!destroyIt"><ng2></ng2></div>'}; })
|
|
.directive('ng2', downgradeComponent({component: Ng2Component}));
|
|
const element = html('<ng1></ng1>');
|
|
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 properly run cleanup with multiple levels of nesting', async(() => {
|
|
let destroyed = false;
|
|
|
|
@Component({
|
|
selector: 'ng2-outer',
|
|
template: '<div *ngIf="!destroyIt"><ng1></ng1></div>',
|
|
})
|
|
class Ng2OuterComponent {
|
|
@Input() destroyIt = false;
|
|
}
|
|
|
|
@Component({selector: 'ng2-inner', template: 'test'})
|
|
class Ng2InnerComponent implements OnDestroy {
|
|
ngOnDestroy() { destroyed = true; }
|
|
}
|
|
|
|
@Directive({selector: 'ng1'})
|
|
class Ng1ComponentFacade extends UpgradeComponent {
|
|
constructor(elementRef: ElementRef, injector: Injector) {
|
|
super('ng1', elementRef, injector);
|
|
}
|
|
}
|
|
|
|
@NgModule({
|
|
imports: [BrowserModule, UpgradeModule],
|
|
declarations: [Ng1ComponentFacade, Ng2InnerComponent, Ng2OuterComponent],
|
|
entryComponents: [Ng2InnerComponent, Ng2OuterComponent],
|
|
})
|
|
class Ng2Module {
|
|
ngDoBootstrap() {}
|
|
}
|
|
|
|
const ng1Module =
|
|
angular.module('ng1', [])
|
|
.directive('ng1', () => ({template: '<ng2-inner></ng2-inner>'}))
|
|
.directive('ng2Inner', downgradeComponent({component: Ng2InnerComponent}))
|
|
.directive('ng2Outer', downgradeComponent({component: Ng2OuterComponent}));
|
|
|
|
const element = html('<ng2-outer [destroy-it]="destroyIt"></ng2-outer>');
|
|
|
|
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
|
|
expect(element.textContent).toBe('test');
|
|
expect(destroyed).toBe(false);
|
|
|
|
$apply(upgrade, 'destroyIt = true');
|
|
|
|
expect(element.textContent).toBe('');
|
|
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('<ng2></ng2>');
|
|
const template = compiled($scope);
|
|
$element.append !(template);
|
|
}
|
|
};
|
|
}
|
|
])
|
|
.directive('ng2', downgradeComponent({component: Ng2Component}));
|
|
|
|
const element = html('<ng1></ng1>');
|
|
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('<works-component></works-component>');
|
|
|
|
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: '<span itWorks></span>!'})
|
|
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('<root-component></root-component>');
|
|
|
|
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
|
|
expect(multiTrim(document.body.textContent)).toBe('It works!');
|
|
});
|
|
}));
|
|
|
|
fixmeIvy('FW-714: ng1 projected content is not being rendered')
|
|
.it('should respect hierarchical dependency injection for ng2', async(() => {
|
|
@Component({selector: 'parent', template: 'parent(<ng-content></ng-content>)'})
|
|
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('<parent><child></child></parent>');
|
|
|
|
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
|
|
expect(multiTrim(document.body.textContent)).toBe('parent(child)');
|
|
});
|
|
}));
|
|
|
|
fixmeIvy(
|
|
'FW-717: Injector on lazy loaded components are not the same as their NgModule\'s injector')
|
|
.it('should work with ng2 lazy loaded components', async(() => {
|
|
let componentInjector: Injector;
|
|
|
|
@Component({selector: 'ng2', template: ''})
|
|
class Ng2Component {
|
|
constructor(injector: Injector) { componentInjector = injector; }
|
|
}
|
|
|
|
@NgModule({
|
|
declarations: [Ng2Component],
|
|
entryComponents: [Ng2Component],
|
|
imports: [BrowserModule, UpgradeModule],
|
|
})
|
|
class Ng2Module {
|
|
ngDoBootstrap() {}
|
|
}
|
|
|
|
@Component({template: ''})
|
|
class LazyLoadedComponent {
|
|
constructor(public module: NgModuleRef<any>) {}
|
|
}
|
|
|
|
@NgModule({
|
|
declarations: [LazyLoadedComponent],
|
|
entryComponents: [LazyLoadedComponent],
|
|
})
|
|
class LazyLoadedModule {
|
|
}
|
|
|
|
const ng1Module = angular.module('ng1', []).directive(
|
|
'ng2', downgradeComponent({component: Ng2Component}));
|
|
|
|
const element = html('<ng2></ng2>');
|
|
|
|
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
|
|
const modInjector = upgrade.injector;
|
|
// Emulate the router lazy loading a module and creating a component
|
|
const compiler = modInjector.get(Compiler);
|
|
const modFactory = compiler.compileModuleSync(LazyLoadedModule);
|
|
const childMod = modFactory.create(modInjector);
|
|
const cmpFactory = childMod.componentFactoryResolver.resolveComponentFactory(
|
|
LazyLoadedComponent) !;
|
|
const lazyCmp = cmpFactory.create(componentInjector);
|
|
|
|
expect(lazyCmp.instance.module.injector === childMod.injector).toBe(true);
|
|
});
|
|
|
|
}));
|
|
|
|
it('should throw if `downgradedModule` is specified', async(() => {
|
|
@Component({selector: 'ng2', template: ''})
|
|
class Ng2Component {
|
|
}
|
|
|
|
@NgModule({
|
|
declarations: [Ng2Component],
|
|
entryComponents: [Ng2Component],
|
|
imports: [BrowserModule, UpgradeModule],
|
|
})
|
|
class Ng2Module {
|
|
ngDoBootstrap() {}
|
|
}
|
|
|
|
|
|
const ng1Module = angular.module('ng1', []).directive(
|
|
'ng2', downgradeComponent({component: Ng2Component, downgradedModule: 'foo'}));
|
|
|
|
const element = html('<ng2></ng2>');
|
|
|
|
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module)
|
|
.then(
|
|
() => { throw new Error('Expected bootstraping to fail.'); },
|
|
err =>
|
|
expect(err.message)
|
|
.toBe(
|
|
'Error while instantiating component \'Ng2Component\': \'downgradedModule\' ' +
|
|
'unexpectedly specified.\n' +
|
|
'You should not specify a value for \'downgradedModule\', unless you are ' +
|
|
'downgrading more than one Angular module (via \'downgradeModule()\').'));
|
|
}));
|
|
});
|
|
});
|