2016-10-19 16:41:04 -04:00
|
|
|
/**
|
|
|
|
* @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
|
|
|
|
*/
|
|
|
|
|
2017-03-22 18:22:38 -04:00
|
|
|
import {Compiler, Component, ComponentFactoryResolver, EventEmitter, Injector, NgModule, NgModuleRef, OnChanges, OnDestroy, SimpleChanges, destroyPlatform} from '@angular/core';
|
2016-10-19 16:41:04 -04:00
|
|
|
import {async} from '@angular/core/testing';
|
|
|
|
import {BrowserModule} from '@angular/platform-browser';
|
|
|
|
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
feat(upgrade): return a function (instead of array) from `downgradeInjectable()` (#14037)
This makes it more consistent with the dynamic version of `upgrade` and makes it
possible to share code between the dynamic and static versions.
This commit also refactors the file layout, moving common and dynamic-specific
files to `common/` and `dynamic/` directories respectively and renaming `aot/`
to `static/`.
Some private keys, used as AngularJS DI tokens, have also been renamed, but this
should not affect apps, since these keys are undocumented and not supposed to
be used externally.
BREAKING CHANGE:
Previously, `upgrade/static/downgradeInjectable` returned an array of the form:
```js
['dep1', 'dep2', ..., function factory(dep1, dep2, ...) { ... }]
```
Now it returns a function with an `$inject` property:
```js
factory.$inject = ['dep1', 'dep2', ...];
function factory(dep1, dep2, ...) { ... }
```
It shouldn't affect the behavior of apps, since both forms are equally suitable
to be used for registering AngularJS injectable services, but it is possible
that type-checking might fail or that current code breaks if it relies on the
returned value being an array.
2017-01-13 09:20:28 -05:00
|
|
|
import * as angular from '@angular/upgrade/src/common/angular1';
|
2016-10-20 22:35:35 -04:00
|
|
|
import {UpgradeModule, downgradeComponent} from '@angular/upgrade/static';
|
2016-10-19 16:41:04 -04:00
|
|
|
|
2017-02-04 10:19:09 -05:00
|
|
|
import {$apply, bootstrap, html, multiTrim} from '../test_helpers';
|
2016-10-19 16:41:04 -04:00
|
|
|
|
|
|
|
export function main() {
|
|
|
|
describe('downgrade ng2 component', () => {
|
|
|
|
|
|
|
|
beforeEach(() => destroyPlatform());
|
|
|
|
afterEach(() => destroyPlatform());
|
|
|
|
|
|
|
|
it('should bind properties, events', async(() => {
|
2017-02-04 10:19:09 -05:00
|
|
|
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'] = '?';
|
|
|
|
});
|
2016-10-19 16:41:04 -04:00
|
|
|
|
|
|
|
@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');
|
2017-02-04 10:19:09 -05:00
|
|
|
assertChange('twoWayB', 'newB');
|
2016-10-19 16:41:04 -04:00
|
|
|
break;
|
|
|
|
case 2:
|
2017-02-04 10:19:09 -05:00
|
|
|
assertChange('interpolate', 'Hello everyone');
|
2016-10-19 16:41:04 -04:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Error('Called too many times! ' + JSON.stringify(changes));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-03-14 17:55:37 -04:00
|
|
|
ng1Module.directive('ng2', downgradeComponent({
|
|
|
|
component: Ng2Component,
|
|
|
|
}));
|
2016-10-19 16:41:04 -04:00
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Ng2Component],
|
|
|
|
entryComponents: [Ng2Component],
|
|
|
|
imports: [BrowserModule, UpgradeModule]
|
|
|
|
})
|
|
|
|
class Ng2Module {
|
|
|
|
ngDoBootstrap() {}
|
|
|
|
}
|
|
|
|
|
|
|
|
const element = html(`
|
|
|
|
<div>
|
2017-02-04 10:19:09 -05:00
|
|
|
<ng2 literal="Text" interpolate="Hello {{name}}"
|
2016-10-19 16:41:04 -04:00
|
|
|
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;');
|
2017-02-04 10:19:09 -05:00
|
|
|
|
|
|
|
$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;');
|
2016-10-19 16:41:04 -04:00
|
|
|
});
|
|
|
|
}));
|
|
|
|
|
2017-01-23 14:23:45 -05:00
|
|
|
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 = () => {};
|
|
|
|
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(`<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');
|
|
|
|
});
|
|
|
|
}));
|
|
|
|
|
2016-10-19 16:41:04 -04:00
|
|
|
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 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);
|
2017-03-24 12:59:18 -04:00
|
|
|
$element.append !(template);
|
2016-10-19 16:41:04 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
])
|
|
|
|
.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');
|
|
|
|
});
|
|
|
|
}));
|
|
|
|
|
2017-01-13 17:36:16 -05:00
|
|
|
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');
|
|
|
|
});
|
|
|
|
}));
|
|
|
|
|
2016-10-19 16:41:04 -04:00
|
|
|
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!');
|
|
|
|
});
|
|
|
|
}));
|
2017-01-16 16:52:42 -05:00
|
|
|
|
|
|
|
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)');
|
|
|
|
});
|
|
|
|
}));
|
2017-03-22 18:22:38 -04:00
|
|
|
|
|
|
|
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 =
|
2017-03-24 12:59:18 -04:00
|
|
|
childMod.componentFactoryResolver.resolveComponentFactory(LazyLoadedComponent) !;
|
2017-03-22 18:22:38 -04:00
|
|
|
const lazyCmp = cmpFactory.create(componentInjector);
|
|
|
|
|
|
|
|
expect(lazyCmp.instance.module).toBe(childMod.injector);
|
|
|
|
});
|
|
|
|
|
|
|
|
}));
|
2016-10-19 16:41:04 -04:00
|
|
|
});
|
2016-10-20 22:35:35 -04:00
|
|
|
}
|