diff --git a/modules/angular2/src/core/compiler/view_ref.ts b/modules/angular2/src/core/compiler/view_ref.ts index 2706969258..6d73a2781f 100644 --- a/modules/angular2/src/core/compiler/view_ref.ts +++ b/modules/angular2/src/core/compiler/view_ref.ts @@ -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. * diff --git a/modules/angular2/test/public_api_spec.ts b/modules/angular2/test/public_api_spec.ts index 33d936b98f..23665ca3ac 100644 --- a/modules/angular2/test/public_api_spec.ts +++ b/modules/angular2/test/public_api_spec.ts @@ -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}', diff --git a/modules/upgrade/src/metadata.ts b/modules/upgrade/src/metadata.ts new file mode 100644 index 0000000000..9d6948d17e --- /dev/null +++ b/modules/upgrade/src/metadata.ts @@ -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 = (window).Reflect; +if (!(Reflect && (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()); +} diff --git a/modules/upgrade/src/upgrade_module.ts b/modules/upgrade/src/upgrade_module.ts new file mode 100644 index 0000000000..af3f5067d6 --- /dev/null +++ b/modules/upgrade/src/upgrade_module.ts @@ -0,0 +1,172 @@ +/// + +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 = []; + + 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, 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 { + var promises: Array> = []; + 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) => { + 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; } +} diff --git a/modules/upgrade/src/util.ts b/modules/upgrade/src/util.ts new file mode 100644 index 0000000000..c0f27eb037 --- /dev/null +++ b/modules/upgrade/src/util.ts @@ -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; +} diff --git a/modules/upgrade/test/integration_spec.ts b/modules/upgrade/test/integration_spec.ts index aa98548f88..131ceeec60 100644 --- a/modules/upgrade/test/integration_spec.ts +++ b/modules/upgrade/test/integration_spec.ts @@ -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("
{{ 'ng1-' }}~~{{ '-ng1' }}
"); + + 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("
{{'ng1('}}{{')'}}
"); + + 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(' }}{{ ')' }}`, + 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 body.firstChild; + return body; } diff --git a/modules/upgrade/test/metadata_spec.ts b/modules/upgrade/test/metadata_spec.ts new file mode 100644 index 0000000000..a7b235019d --- /dev/null +++ b/modules/upgrade/test/metadata_spec.ts @@ -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 {} diff --git a/modules/upgrade/upgrade.ts b/modules/upgrade/upgrade.ts new file mode 100644 index 0000000000..60af6cdc3f --- /dev/null +++ b/modules/upgrade/upgrade.ts @@ -0,0 +1 @@ +export {createUpgradeModule, UpgradeModule} from './src/upgrade_module';