
Previously, if the two injectors are not the same, jasmine tried to display an error message, but it got stuck in an infinite loop trying to render the injectors that were different. PR Close #27454
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()\').'));
|
|
}));
|
|
});
|
|
});
|