fix(ngUpgrade): to work with @NgModule

We changed the bootstrap order:
1. create NgZone
2. bootstrap ng1 inside NgZone and upgrade ng1 components to ng2 components.
3. bootstrap ng2 with NgZone

Note: Previous footgun behavior was: bootstrap ng2 first to extract NgZone, so that ng1 bootstrap can happen in NgZone. This meant that if ng2 bootstrap eagerly compiled a component which contained ng1 components, then we did not have complete metadata.
This commit is contained in:
Igor Minar
2016-08-05 13:32:04 -07:00
committed by Misko Hevery
parent 37f138e83d
commit d21331e902
7 changed files with 179 additions and 85 deletions

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Compiler, CompilerFactory, ComponentFactory, ComponentResolver, Injector, NgModule, NgModuleRef, NgZone, PlatformRef, Provider, ReflectiveInjector, Testability, Type, provide} from '@angular/core';
import {BaseException, Compiler, ComponentFactory, Injector, NgModule, NgModuleRef, NgZone, Provider, Testability, Type} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
@ -59,11 +59,11 @@ var upgradeCount: number = 0;
* ### Example
*
* ```
* var adapter = new UpgradeAdapter();
* var adapter = new UpgradeAdapter(forwardRef(() => MyNg2Module));
* var module = angular.module('myExample', []);
* module.directive('ng2', adapter.downgradeNg2Component(Ng2));
* module.directive('ng2Comp', adapter.downgradeNg2Component(Ng2));
*
* module.directive('ng1', function() {
* module.directive('ng1Hello', function() {
* return {
* scope: { title: '=' },
* template: 'ng1[Hello {{title}}!](<span ng-transclude></span>)'
@ -72,20 +72,28 @@ var upgradeCount: number = 0;
*
*
* @Component({
* selector: 'ng2',
* selector: 'ng2-comp',
* inputs: ['name'],
* template: 'ng2[<ng1 [title]="name">transclude</ng1>](<ng-content></ng-content>)',
* directives: [adapter.upgradeNg1Component('ng1')]
* template: 'ng2[<ng1-hello [title]="name">transclude</ng1-hello>](<ng-content></ng-content>)',
* directives:
* })
* class Ng2 {
* class Ng2Component {
* }
*
* document.body.innerHTML = '<ng2 name="World">project</ng2>';
* @NgModule({
* declarations: [Ng2Component, adapter.upgradeNg1Component('ng1Hello')],
* imports: [BrowserModule]
* })
* class MyNg2Module {}
*
*
* document.body.innerHTML = '<ng2-comp name="World">project</ng2-comp>';
*
* adapter.bootstrap(document.body, ['myExample']).ready(function() {
* expect(document.body.textContent).toEqual(
* "ng2[ng1[Hello World!](transclude)](project)");
* });
*
* ```
*
* @experimental
@ -95,14 +103,26 @@ export class UpgradeAdapter {
private idPrefix: string = `NG2_UPGRADE_${upgradeCount++}_`;
/* @internal */
private upgradedComponents: Type[] = [];
/* @internal */
private downgradedComponents: {[name: string]: UpgradeNg1ComponentAdapterBuilder} = {};
/**
* An internal map of ng1 components which need to up upgraded to ng2.
*
* We can't upgrade until injector is instantiated and we can retrieve the component metadata.
* For this reason we keep a list of components to upgrade until ng1 injector is bootstrapped.
*
* @internal
*/
private ng1ComponentsToBeUpgraded: {[name: string]: UpgradeNg1ComponentAdapterBuilder} = {};
/* @internal */
private providers: Array<Type|Provider|any[]|any> = [];
// the ng2AppModule param should be required once the deprecated @Component.directives prop is
// removed
constructor(private ng2AppModule?: Type) {}
constructor(private ng2AppModule?: Type) {
if (arguments.length && !ng2AppModule) {
throw new BaseException(
'UpgradeAdapter constructor called with undefined instead of a ng module type');
}
}
/**
* Allows Angular v2 Component to be used from AngularJS v1.
@ -132,7 +152,7 @@ export class UpgradeAdapter {
* ### Example
*
* ```
* var adapter = new UpgradeAdapter();
* var adapter = new UpgradeAdapter(forwardRef(() => MyNg2Module));
* var module = angular.module('myExample', []);
* module.directive('greet', adapter.downgradeNg2Component(Greeter));
*
@ -145,6 +165,12 @@ export class UpgradeAdapter {
* @Input() name: string;
* }
*
* @NgModule({
* declarations: [Greeter],
* imports: [BrowserModule]
* })
* class MyNg2Module {}
*
* document.body.innerHTML =
* 'ng1 template: <greet salutation="Hello" [name]="world">text</greet>';
*
@ -204,7 +230,7 @@ export class UpgradeAdapter {
* ### Example
*
* ```
* var adapter = new UpgradeAdapter();
* var adapter = new UpgradeAdapter(forwardRef(() => MyNg2Module));
* var module = angular.module('myExample', []);
*
* module.directive('greet', function() {
@ -219,11 +245,16 @@ export class UpgradeAdapter {
* @Component({
* selector: 'ng2',
* template: 'ng2 template: <greet salutation="Hello" [name]="world">text</greet>'
* directives: [adapter.upgradeNg1Component('greet')]
* })
* class Ng2 {
* }
*
* @NgModule({
* declarations: [Ng2, adapter.upgradeNg1Component('greet')],
* imports: [BrowserModule]
* })
* class MyNg2Module {}
*
* document.body.innerHTML = '<ng2></ng2>';
*
* adapter.bootstrap(document.body, ['myExample']).ready(function() {
@ -232,10 +263,11 @@ export class UpgradeAdapter {
* ```
*/
upgradeNg1Component(name: string): Type {
if ((<any>this.downgradedComponents).hasOwnProperty(name)) {
return this.downgradedComponents[name].type;
if ((<any>this.ng1ComponentsToBeUpgraded).hasOwnProperty(name)) {
return this.ng1ComponentsToBeUpgraded[name].type;
} else {
return (this.downgradedComponents[name] = new UpgradeNg1ComponentAdapterBuilder(name)).type;
return (this.ng1ComponentsToBeUpgraded[name] = new UpgradeNg1ComponentAdapterBuilder(name))
.type;
}
}
@ -264,12 +296,17 @@ export class UpgradeAdapter {
* @Component({
* selector: 'ng2',
* inputs: ['name'],
* template: 'ng2[<ng1 [title]="name">transclude</ng1>](<ng-content></ng-content>)',
* directives: [adapter.upgradeNg1Component('ng1')]
* template: 'ng2[<ng1 [title]="name">transclude</ng1>](<ng-content></ng-content>)'
* })
* class Ng2 {
* }
*
* @NgModule({
* declarations: [Ng2, adapter.upgradeNg1Component('ng1')],
* imports: [BrowserModule]
* })
* class MyNg2Module {}
*
* document.body.innerHTML = '<ng2 name="World">project</ng2>';
*
* adapter.bootstrap(document.body, ['myExample']).ready(function() {
@ -280,55 +317,35 @@ export class UpgradeAdapter {
*/
bootstrap(element: Element, modules?: any[], config?: angular.IAngularBootstrapConfig):
UpgradeAdapterRef {
const ngZone =
new NgZone({enableLongStackTrace: Zone.hasOwnProperty('longStackTraceZoneSpec')});
var upgrade = new UpgradeAdapterRef();
var ng1Injector: angular.IInjectorService = null;
var platformRef: PlatformRef = platformBrowserDynamic();
var providers = [
{provide: NG1_INJECTOR, useFactory: () => ng1Injector},
{provide: NG1_COMPILE, useFactory: () => ng1Injector.get(NG1_COMPILE)}, this.providers
];
@NgModule({providers: providers, imports: [BrowserModule]})
class DynamicModule {
ngDoBootstrap() {}
}
platformRef.bootstrapModule(<any>this.ng2AppModule || DynamicModule).then((moduleRef) => {
ng1Injector = this._afterNg2ModuleBootstrap(moduleRef, upgrade, element, modules, config);
});
return upgrade;
}
private _afterNg2ModuleBootstrap(
moduleRef: NgModuleRef<any>, upgrade: UpgradeAdapterRef, element: Element, modules?: any[],
config?: angular.IAngularBootstrapConfig): angular.IInjectorService {
const boundCompiler: Compiler = moduleRef.injector.get(Compiler);
var ng1Injector: angular.IInjectorService = null;
var injector: Injector = moduleRef.injector;
var ngZone: NgZone = injector.get(NgZone);
var moduleRef: NgModuleRef<any> = null;
var delayApplyExps: Function[] = [];
var original$applyFn: Function;
var rootScopePrototype: any;
var rootScope: angular.IRootScopeService;
var componentFactoryRefMap: ComponentFactoryRefMap = {};
var ng1Module = angular.module(this.idPrefix, modules);
var ng1BootstrapPromise: Promise<any> = null;
var ng1compilePromise: Promise<any> = null;
ng1Module.value(NG2_INJECTOR, injector)
var ng1BootstrapPromise: Promise<any>;
var ng1compilePromise: Promise<any>;
ng1Module.factory(NG2_INJECTOR, () => moduleRef.injector.get(Injector))
.value(NG2_ZONE, ngZone)
.value(NG2_COMPILER, boundCompiler)
.factory(NG2_COMPILER, () => moduleRef.injector.get(Compiler))
.value(NG2_COMPONENT_FACTORY_REF_MAP, componentFactoryRefMap)
.config([
'$provide', '$injector',
(provide: any /** TODO #???? */, ng1Injector: any /** TODO #???? */) => {
(provide: any /** TODO #???? */, ng1Injector: angular.IInjectorService) => {
provide.decorator(NG1_ROOT_SCOPE, [
'$delegate',
function(rootScopeDelegate: angular.IRootScopeService) {
// Capture the root apply so that we can delay first call to $apply until we
// bootstrap Angular 2 and then we replay and restore the $apply.
rootScopePrototype = rootScopeDelegate.constructor.prototype;
if (rootScopePrototype.hasOwnProperty('$apply')) {
original$applyFn = rootScopePrototype.$apply;
rootScopePrototype.$apply = (exp: any /** TODO #???? */) =>
delayApplyExps.push(exp);
rootScopePrototype.$apply = (exp: any) => delayApplyExps.push(exp);
} else {
throw new Error('Failed to find \'$apply\' on \'$rootScope\'!');
}
@ -339,12 +356,12 @@ export class UpgradeAdapter {
provide.decorator(NG1_TESTABILITY, [
'$delegate',
function(testabilityDelegate: angular.ITestabilityService) {
var ng2Testability: Testability = injector.get(Testability);
var origonalWhenStable: Function = testabilityDelegate.whenStable;
var originalWhenStable: Function = testabilityDelegate.whenStable;
var newWhenStable = (callback: Function): void => {
var whenStableContext: any = this;
origonalWhenStable.call(this, function() {
originalWhenStable.call(this, function() {
var ng2Testability: Testability = moduleRef.injector.get(Testability);
if (ng2Testability.isStable()) {
callback.apply(this, arguments);
} else {
@ -366,12 +383,31 @@ export class UpgradeAdapter {
'$injector', '$rootScope',
(injector: angular.IInjectorService, rootScope: angular.IRootScopeService) => {
ng1Injector = injector;
ngZone.onMicrotaskEmpty.subscribe({
next: (_: any /** TODO #???? */) =>
ngZone.runOutsideAngular(() => rootScope.$evalAsync())
});
UpgradeNg1ComponentAdapterBuilder.resolve(this.downgradedComponents, injector)
.then(resolve, reject);
UpgradeNg1ComponentAdapterBuilder.resolve(this.ng1ComponentsToBeUpgraded, injector)
.then(() => {
// At this point we have ng1 injector and we have lifted ng1 components into ng2, we
// now can bootstrap ng2.
var DynamicNgUpgradeModule =
NgModule({
providers: [
{provide: NG1_INJECTOR, useFactory: () => ng1Injector},
{provide: NG1_COMPILE, useFactory: () => ng1Injector.get(NG1_COMPILE)},
this.providers
],
imports: this.ng2AppModule ? [this.ng2AppModule] : [BrowserModule]
}).Class({constructor: function() {}, ngDoBootstrap: function() {}});
(platformBrowserDynamic() as any)
._bootstrapModuleWithZone(DynamicNgUpgradeModule, undefined, ngZone)
.then((ref: NgModuleRef<any>) => {
moduleRef = ref;
angular.element(element).data(
controllerKey(NG2_INJECTOR), moduleRef.injector);
ngZone.onMicrotaskEmpty.subscribe({
next: (_: any) => ngZone.runOutsideAngular(() => rootScope.$evalAsync())
});
})
.then(resolve, reject);
});
}
]);
});
@ -380,7 +416,6 @@ export class UpgradeAdapter {
var windowAngular = (window as any /** TODO #???? */)['angular'];
windowAngular.resumeBootstrap = undefined;
angular.element(element).data(controllerKey(NG2_INJECTOR), injector);
ngZone.run(() => { angular.bootstrap(element, [this.idPrefix], config); });
ng1BootstrapPromise = new Promise((resolve, reject) => {
if (windowAngular.resumeBootstrap) {
@ -396,9 +431,12 @@ export class UpgradeAdapter {
});
Promise.all([ng1BootstrapPromise, ng1compilePromise])
.then(() => { return this.compileNg2Components(boundCompiler, componentFactoryRefMap); })
.then(() => {
ngZone.run(() => {
return this.compileNg2Components(
moduleRef.injector.get(Compiler), componentFactoryRefMap);
})
.then(() => {
moduleRef.injector.get(NgZone).run(() => {
if (rootScopePrototype) {
rootScopePrototype.$apply = original$applyFn; // restore original $apply
while (delayApplyExps.length) {
@ -409,7 +447,7 @@ export class UpgradeAdapter {
}
});
}, onError);
return ng1Injector;
return upgrade;
}
/**
@ -528,7 +566,7 @@ export class UpgradeAdapter {
var promises: Array<Promise<ComponentFactory<any>>> = [];
var types = this.upgradedComponents;
for (var i = 0; i < types.length; i++) {
promises.push(compiler.compileComponentAsync(<any>types[i]));
promises.push(compiler.compileComponentAsync(<any>types[i], this.ng2AppModule));
}
return Promise.all(promises).then((componentFactories: Array<ComponentFactory<any>>) => {
var types = this.upgradedComponents;