feat(upgrade): Allow including ng2/1 components in ng1/2

Closes #3539
This commit is contained in:
Misko Hevery 2015-08-06 13:19:29 -07:00
parent db6d289d82
commit 8427863bab
8 changed files with 331 additions and 3 deletions

View File

@ -1,5 +1,6 @@
import {isPresent} from 'angular2/src/core/facade/lang';
import * as viewModule from './view';
import {ChangeDetectorRef} from '../change_detection/change_detector_ref';
import {RenderViewRef, RenderFragmentRef} from 'angular2/src/core/render/api';
// This is a workaround for privacy in Dart as we don't have library parts
@ -12,7 +13,7 @@ export function internalProtoView(protoViewRef: ProtoViewRef): viewModule.AppPro
return isPresent(protoViewRef) ? protoViewRef._protoView : null;
}
export interface HostViewRef {}
export interface HostViewRef { changeDetectorRef: ChangeDetectorRef; }
/**
* A reference to an Angular View.
@ -66,6 +67,8 @@ export interface HostViewRef {}
* ```
*/
export class ViewRef implements HostViewRef {
private _changeDetectorRef: ChangeDetectorRef = null;
/**
* @private
*/
@ -81,6 +84,19 @@ export class ViewRef implements HostViewRef {
*/
get renderFragment(): RenderFragmentRef { return this._view.renderFragment; }
/**
* Return `ChangeDetectorRef`
*/
get changeDetectorRef(): ChangeDetectorRef {
if (this._changeDetectorRef === null) {
this._changeDetectorRef = this._view.changeDetector.ref;
}
return this._changeDetectorRef;
}
set changeDetectorRef(value: ChangeDetectorRef) {
throw "readonly"; // TODO: https://github.com/Microsoft/TypeScript/issues/12
}
/**
* Set local variable in a view.
*

View File

@ -1089,6 +1089,8 @@ var NG_API = [
'ViewQueryMetadata.token',
'ViewQueryMetadata.varBindings',
'ViewRef',
'ViewRef.changeDetectorRef',
'ViewRef.changeDetectorRef=',
'ViewRef.render',
'ViewRef.renderFragment',
'ViewRef.setLocal()',
@ -1137,6 +1139,8 @@ var NG_API = [
'{DoCheck}',
'{Form}',
'{HostViewRef}',
'{HostViewRef}.changeDetectorRef',
'{HostViewRef}.changeDetectorRef=',
'{IterableDifferFactory}',
'{IterableDiffer}',
'{KeyValueDifferFactory}',

View File

@ -0,0 +1,24 @@
import {Type, ComponentMetadata, DirectiveResolver, DirectiveMetadata} from 'angular2/angular2';
import {stringify} from 'upgrade/src/util';
var COMPONENT_SELECTOR = /^[\w|-]*$/;
var SKEWER_CASE = /-(\w)/g;
var directiveResolver = new DirectiveResolver();
interface Reflect {
getOwnMetadata(name: string, type: Function): any;
defineMetadata(name: string, value: any, cls: Type): void;
}
var Reflect: Reflect = <Reflect>(<any>window).Reflect;
if (!(Reflect && (<any>Reflect)['getOwnMetadata'])) {
throw 'reflect-metadata shim is required when using class decorators';
}
export function getComponentSelector(type: Type): string {
var resolvedMetadata: DirectiveMetadata = directiveResolver.resolve(type);
var selector = resolvedMetadata.selector;
if (!selector.match(COMPONENT_SELECTOR)) {
throw new Error('Only selectors matching element names are supported, got: ' + selector);
}
return selector.replace(SKEWER_CASE, (all, letter: string) => letter.toUpperCase());
}

View File

@ -0,0 +1,172 @@
///<reference path="../typings/angularjs/angular.d.ts"/>
import {
platform,
PlatformRef,
ApplicationRef,
ComponentRef,
bind,
Directive,
Component,
Inject,
View,
Type,
PlatformRef,
ApplicationRef,
ChangeDetectorRef,
AppViewManager,
NgZone,
Injector,
Compiler,
ProtoViewRef,
ElementRef,
HostViewRef,
ViewRef
} from 'angular2/angular2';
import {applicationDomBindings} from 'angular2/src/core/application_common';
import {applicationCommonBindings} from "../../angular2/src/core/application_ref";
import {getComponentSelector} from './metadata';
import {onError} from './util';
export const INJECTOR = 'ng2.Injector';
export const APP_VIEW_MANAGER = 'ng2.AppViewManager';
export const NG2_COMPILER = 'ng2.Compiler';
export const NG2_ZONE = 'ng2.NgZone';
export const PROTO_VIEW_REF_MAP = 'ng2.ProtoViewRefMap';
const NG1_REQUIRE_INJECTOR_REF = '$' + INJECTOR + 'Controller';
const NG1_SCOPE = '$scope';
const NG1_COMPILE = '$compile';
const NG1_INJECTOR = '$injector';
const REQUIRE_INJECTOR = '^' + INJECTOR;
var moduleCount: number = 0;
const CAMEL_CASE = /([A-Z])/g;
export function createUpgradeModule(): UpgradeModule {
var prefix = `NG2_UPGRADE_m${moduleCount++}_`;
return new UpgradeModule(prefix, angular.module(prefix, []));
}
export class UpgradeModule {
componentTypes: Array<Type> = [];
constructor(public idPrefix: string, public ng1Module: angular.IModule) {}
importNg2Component(type: Type): UpgradeModule {
this.componentTypes.push(type);
var selector: string = getComponentSelector(type);
var factory: Function = ng1ComponentDirective(selector, type, `${this.idPrefix}${selector}_c`);
this.ng1Module.directive(selector, <any[]>factory);
return this;
}
exportAsNg2Component(name: string): Type {
return Directive({
selector: name.replace(CAMEL_CASE, (all, next: string) => '-' + next.toLowerCase())
})
.Class({
constructor: [
new Inject(NG1_COMPILE),
new Inject(NG1_SCOPE),
ElementRef,
function(compile: angular.ICompileService, scope: angular.IScope,
elementRef: ElementRef) { compile(elementRef.nativeElement)(scope); }
]
});
}
bootstrap(element: Element, modules?: any[],
config?: angular.IAngularBootstrapConfig): UpgradeRef {
var upgrade = new UpgradeRef();
var ng1Injector: angular.auto.IInjectorService = null;
var bindings = [
applicationCommonBindings(),
applicationDomBindings(),
bind(NG1_INJECTOR).toFactory(() => ng1Injector),
bind(NG1_COMPILE).toFactory(() => ng1Injector.get(NG1_COMPILE))
];
var platformRef: PlatformRef = platform();
var applicationRef: ApplicationRef = platformRef.application(bindings);
var injector: Injector = applicationRef.injector;
var ngZone: NgZone = injector.get(NgZone);
var compiler: Compiler = injector.get(Compiler);
this.compileNg2Components(compiler).then((protoViewRefMap: ProtoViewRefMap) => {
ngZone.run(() => {
this.ng1Module.value(INJECTOR, injector)
.value(NG2_ZONE, ngZone)
.value(NG2_COMPILER, compiler)
.value(PROTO_VIEW_REF_MAP, protoViewRefMap)
.value(APP_VIEW_MANAGER, injector.get(AppViewManager))
.run([
'$injector',
'$rootScope',
(injector: angular.auto.IInjectorService, rootScope: angular.IRootScopeService) => {
ng1Injector = injector;
ngZone.overrideOnTurnDone(() => rootScope.$apply());
}
]);
modules = modules ? [].concat(modules) : [];
modules.push(this.idPrefix);
angular.element(element).data(NG1_REQUIRE_INJECTOR_REF, injector);
angular.bootstrap(element, modules, config);
upgrade.readyFn && upgrade.readyFn();
});
});
return upgrade;
}
private compileNg2Components(compiler: Compiler): Promise<ProtoViewRefMap> {
var promises: Array<Promise<ProtoViewRef>> = [];
var types = this.componentTypes;
for (var i = 0; i < types.length; i++) {
promises.push(compiler.compileInHost(types[i]));
}
return Promise.all(promises).then((protoViews: Array<ProtoViewRef>) => {
var protoViewRefMap: ProtoViewRefMap = {};
var types = this.componentTypes;
for (var i = 0; i < protoViews.length; i++) {
protoViewRefMap[getComponentSelector(types[i])] = protoViews[i];
}
return protoViewRefMap;
}, onError);
}
}
interface ProtoViewRefMap {
[selector: string]: ProtoViewRef
}
function ng1ComponentDirective(selector: string, type: Type, idPrefix: string): Function {
directiveFactory.$inject = [PROTO_VIEW_REF_MAP, APP_VIEW_MANAGER];
function directiveFactory(protoViewRefMap: ProtoViewRefMap, viewManager: AppViewManager):
angular.IDirective {
var protoView: ProtoViewRef = protoViewRefMap[selector];
if (!protoView) throw new Error('Expecting ProtoViewRef for: ' + selector);
var idCount = 0;
return {
restrict: 'E',
require: REQUIRE_INJECTOR,
link: (scope: angular.IScope, element: angular.IAugmentedJQuery, attrs: angular.IAttributes,
parentInjector: any, transclude: angular.ITranscludeFunction): void => {
var id = element[0].id = idPrefix + (idCount++);
var childInjector = parentInjector.resolveAndCreateChild([bind(NG1_SCOPE).toValue(scope)]);
var hostViewRef = viewManager.createRootHostView(protoView, '#' + id, childInjector);
var changeDetector: ChangeDetectorRef = hostViewRef.changeDetectorRef;
scope.$watch(() => changeDetector.detectChanges());
element.bind('$remove', () => viewManager.destroyRootHostView(hostViewRef));
}
};
}
return directiveFactory;
}
export class UpgradeRef {
readyFn: Function;
ready(fn: Function) { this.readyFn = fn; }
}

View File

@ -0,0 +1,12 @@
export function stringify(obj: any): string {
if (typeof obj == 'function') return obj.name || obj.toString();
return '' + obj;
}
export function onError(e: any) {
// TODO: (misko): We seem to not have a stack trace here!
console.log(e, e.stack);
throw e;
}

View File

@ -11,8 +11,58 @@ import {
xit,
} from 'angular2/test_lib';
import {Component, View, Inject} from 'angular2/angular2';
import {createUpgradeModule, UpgradeModule, bootstrapHybrid} from 'upgrade/upgrade';
export function main() {
describe('upgrade integration',
() => { it('should run', () => { expect(angular.version.major).toBe(1); }); });
describe('upgrade: ng1 to ng2', () => {
it('should have angular 1 loaded', () => expect(angular.version.major).toBe(1));
it('should instantiate ng2 in ng1 template', inject([AsyncTestCompleter], (async) => {
var element = html("<div>{{ 'ng1-' }}<ng2>~~</ng2>{{ '-ng1' }}</div>");
var upgradeModule: UpgradeModule = createUpgradeModule();
upgradeModule.importNg2Component(SimpleComponent);
upgradeModule.bootstrap(element).ready(() => {
expect(document.body.textContent).toEqual("ng1-NG2-ng1");
async.done();
});
}));
it('should instantiate ng1 in ng2 template', inject([AsyncTestCompleter], (async) => {
var element = html("<div>{{'ng1('}}<ng2-1></ng2-1>{{')'}}</div>");
ng1inNg2Module.bootstrap(element).ready(() => {
expect(document.body.textContent).toEqual("ng1(ng2(ng1 WORKS!))");
async.done();
});
}));
});
}
@Component({selector: 'ng2'})
@View({template: `{{ 'NG2' }}`})
class SimpleComponent {
}
var ng1inNg2Module: UpgradeModule = createUpgradeModule();
@Component({selector: 'ng2-1'})
@View({
template: `{{ 'ng2(' }}<ng1></ng1>{{ ')' }}`,
directives: [ng1inNg2Module.exportAsNg2Component('ng1')]
})
class Ng2ContainsNg1 {
}
ng1inNg2Module.ng1Module.directive('ng1', () => { return {template: 'ng1 {{ "WORKS" }}!'}; });
ng1inNg2Module.importNg2Component(Ng2ContainsNg1);
function html(html: string): Element {
var body = document.body;
body.innerHTML = html;
if (body.childNodes.length == 1 && body.firstChild instanceof HTMLElement)
return <Element>body.firstChild;
return body;
}

View File

@ -0,0 +1,49 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
describe,
expect,
iit,
inject,
it,
xdescribe,
xit,
} from 'angular2/test_lib';
import {Component, View} from 'angular2/angular2';
import {getComponentSelector} from 'upgrade/src/metadata';
export function main() {
describe('upgrade metadata', () => {
it('should extract component selector',
() => { expect(getComponentSelector(ElementNameComponent)).toEqual('elementNameDashed'); });
describe('errors', () => {
it('should throw on missing selector', () => {
expect(() => getComponentSelector(AttributeNameComponent))
.toThrowErrorWith(
"Only selectors matching element names are supported, got: [attr-name]");
});
it('should throw on non element names', () => {
expect(() => getComponentSelector(NoAnnotationComponent))
.toThrowErrorWith("No Directive annotation found on NoAnnotationComponent");
});
});
});
}
@Component({selector: 'element-name-dashed'})
@View({template: ``})
class ElementNameComponent {
}
@Component({selector: '[attr-name]'})
@View({template: ``})
class AttributeNameComponent {
}
class NoAnnotationComponent {}

View File

@ -0,0 +1 @@
export {createUpgradeModule, UpgradeModule} from './src/upgrade_module';