X Tutup
Skip to content

Commit 8427863

Browse files
committed
feat(upgrade): Allow including ng2/1 components in ng1/2
Closes #3539
1 parent db6d289 commit 8427863

File tree

8 files changed

+331
-3
lines changed

8 files changed

+331
-3
lines changed

modules/angular2/src/core/compiler/view_ref.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {isPresent} from 'angular2/src/core/facade/lang';
22
import * as viewModule from './view';
3+
import {ChangeDetectorRef} from '../change_detection/change_detector_ref';
34
import {RenderViewRef, RenderFragmentRef} from 'angular2/src/core/render/api';
45

56
// 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
1213
return isPresent(protoViewRef) ? protoViewRef._protoView : null;
1314
}
1415

15-
export interface HostViewRef {}
16+
export interface HostViewRef { changeDetectorRef: ChangeDetectorRef; }
1617

1718
/**
1819
* A reference to an Angular View.
@@ -66,6 +67,8 @@ export interface HostViewRef {}
6667
* ```
6768
*/
6869
export class ViewRef implements HostViewRef {
70+
private _changeDetectorRef: ChangeDetectorRef = null;
71+
6972
/**
7073
* @private
7174
*/
@@ -81,6 +84,19 @@ export class ViewRef implements HostViewRef {
8184
*/
8285
get renderFragment(): RenderFragmentRef { return this._view.renderFragment; }
8386

87+
/**
88+
* Return `ChangeDetectorRef`
89+
*/
90+
get changeDetectorRef(): ChangeDetectorRef {
91+
if (this._changeDetectorRef === null) {
92+
this._changeDetectorRef = this._view.changeDetector.ref;
93+
}
94+
return this._changeDetectorRef;
95+
}
96+
set changeDetectorRef(value: ChangeDetectorRef) {
97+
throw "readonly"; // TODO: https://github.com/Microsoft/TypeScript/issues/12
98+
}
99+
84100
/**
85101
* Set local variable in a view.
86102
*

modules/angular2/test/public_api_spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,6 +1089,8 @@ var NG_API = [
10891089
'ViewQueryMetadata.token',
10901090
'ViewQueryMetadata.varBindings',
10911091
'ViewRef',
1092+
'ViewRef.changeDetectorRef',
1093+
'ViewRef.changeDetectorRef=',
10921094
'ViewRef.render',
10931095
'ViewRef.renderFragment',
10941096
'ViewRef.setLocal()',
@@ -1137,6 +1139,8 @@ var NG_API = [
11371139
'{DoCheck}',
11381140
'{Form}',
11391141
'{HostViewRef}',
1142+
'{HostViewRef}.changeDetectorRef',
1143+
'{HostViewRef}.changeDetectorRef=',
11401144
'{IterableDifferFactory}',
11411145
'{IterableDiffer}',
11421146
'{KeyValueDifferFactory}',

modules/upgrade/src/metadata.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import {Type, ComponentMetadata, DirectiveResolver, DirectiveMetadata} from 'angular2/angular2';
2+
import {stringify} from 'upgrade/src/util';
3+
4+
var COMPONENT_SELECTOR = /^[\w|-]*$/;
5+
var SKEWER_CASE = /-(\w)/g;
6+
var directiveResolver = new DirectiveResolver();
7+
8+
interface Reflect {
9+
getOwnMetadata(name: string, type: Function): any;
10+
defineMetadata(name: string, value: any, cls: Type): void;
11+
}
12+
var Reflect: Reflect = <Reflect>(<any>window).Reflect;
13+
if (!(Reflect && (<any>Reflect)['getOwnMetadata'])) {
14+
throw 'reflect-metadata shim is required when using class decorators';
15+
}
16+
17+
export function getComponentSelector(type: Type): string {
18+
var resolvedMetadata: DirectiveMetadata = directiveResolver.resolve(type);
19+
var selector = resolvedMetadata.selector;
20+
if (!selector.match(COMPONENT_SELECTOR)) {
21+
throw new Error('Only selectors matching element names are supported, got: ' + selector);
22+
}
23+
return selector.replace(SKEWER_CASE, (all, letter: string) => letter.toUpperCase());
24+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
///<reference path="../typings/angularjs/angular.d.ts"/>
2+
3+
import {
4+
platform,
5+
PlatformRef,
6+
ApplicationRef,
7+
ComponentRef,
8+
bind,
9+
Directive,
10+
Component,
11+
Inject,
12+
View,
13+
Type,
14+
PlatformRef,
15+
ApplicationRef,
16+
ChangeDetectorRef,
17+
AppViewManager,
18+
NgZone,
19+
Injector,
20+
Compiler,
21+
ProtoViewRef,
22+
ElementRef,
23+
HostViewRef,
24+
ViewRef
25+
} from 'angular2/angular2';
26+
import {applicationDomBindings} from 'angular2/src/core/application_common';
27+
import {applicationCommonBindings} from "../../angular2/src/core/application_ref";
28+
29+
import {getComponentSelector} from './metadata';
30+
import {onError} from './util';
31+
export const INJECTOR = 'ng2.Injector';
32+
export const APP_VIEW_MANAGER = 'ng2.AppViewManager';
33+
export const NG2_COMPILER = 'ng2.Compiler';
34+
export const NG2_ZONE = 'ng2.NgZone';
35+
export const PROTO_VIEW_REF_MAP = 'ng2.ProtoViewRefMap';
36+
37+
const NG1_REQUIRE_INJECTOR_REF = '$' + INJECTOR + 'Controller';
38+
const NG1_SCOPE = '$scope';
39+
const NG1_COMPILE = '$compile';
40+
const NG1_INJECTOR = '$injector';
41+
const REQUIRE_INJECTOR = '^' + INJECTOR;
42+
43+
var moduleCount: number = 0;
44+
const CAMEL_CASE = /([A-Z])/g;
45+
46+
export function createUpgradeModule(): UpgradeModule {
47+
var prefix = `NG2_UPGRADE_m${moduleCount++}_`;
48+
return new UpgradeModule(prefix, angular.module(prefix, []));
49+
}
50+
51+
52+
export class UpgradeModule {
53+
componentTypes: Array<Type> = [];
54+
55+
constructor(public idPrefix: string, public ng1Module: angular.IModule) {}
56+
57+
importNg2Component(type: Type): UpgradeModule {
58+
this.componentTypes.push(type);
59+
var selector: string = getComponentSelector(type);
60+
var factory: Function = ng1ComponentDirective(selector, type, `${this.idPrefix}${selector}_c`);
61+
this.ng1Module.directive(selector, <any[]>factory);
62+
return this;
63+
}
64+
65+
exportAsNg2Component(name: string): Type {
66+
return Directive({
67+
selector: name.replace(CAMEL_CASE, (all, next: string) => '-' + next.toLowerCase())
68+
})
69+
.Class({
70+
constructor: [
71+
new Inject(NG1_COMPILE),
72+
new Inject(NG1_SCOPE),
73+
ElementRef,
74+
function(compile: angular.ICompileService, scope: angular.IScope,
75+
elementRef: ElementRef) { compile(elementRef.nativeElement)(scope); }
76+
]
77+
});
78+
}
79+
80+
bootstrap(element: Element, modules?: any[],
81+
config?: angular.IAngularBootstrapConfig): UpgradeRef {
82+
var upgrade = new UpgradeRef();
83+
var ng1Injector: angular.auto.IInjectorService = null;
84+
var bindings = [
85+
applicationCommonBindings(),
86+
applicationDomBindings(),
87+
bind(NG1_INJECTOR).toFactory(() => ng1Injector),
88+
bind(NG1_COMPILE).toFactory(() => ng1Injector.get(NG1_COMPILE))
89+
];
90+
91+
var platformRef: PlatformRef = platform();
92+
var applicationRef: ApplicationRef = platformRef.application(bindings);
93+
var injector: Injector = applicationRef.injector;
94+
var ngZone: NgZone = injector.get(NgZone);
95+
var compiler: Compiler = injector.get(Compiler);
96+
this.compileNg2Components(compiler).then((protoViewRefMap: ProtoViewRefMap) => {
97+
ngZone.run(() => {
98+
this.ng1Module.value(INJECTOR, injector)
99+
.value(NG2_ZONE, ngZone)
100+
.value(NG2_COMPILER, compiler)
101+
.value(PROTO_VIEW_REF_MAP, protoViewRefMap)
102+
.value(APP_VIEW_MANAGER, injector.get(AppViewManager))
103+
.run([
104+
'$injector',
105+
'$rootScope',
106+
(injector: angular.auto.IInjectorService, rootScope: angular.IRootScopeService) => {
107+
ng1Injector = injector;
108+
ngZone.overrideOnTurnDone(() => rootScope.$apply());
109+
}
110+
]);
111+
112+
modules = modules ? [].concat(modules) : [];
113+
modules.push(this.idPrefix);
114+
angular.element(element).data(NG1_REQUIRE_INJECTOR_REF, injector);
115+
angular.bootstrap(element, modules, config);
116+
117+
upgrade.readyFn && upgrade.readyFn();
118+
});
119+
});
120+
return upgrade;
121+
}
122+
123+
private compileNg2Components(compiler: Compiler): Promise<ProtoViewRefMap> {
124+
var promises: Array<Promise<ProtoViewRef>> = [];
125+
var types = this.componentTypes;
126+
for (var i = 0; i < types.length; i++) {
127+
promises.push(compiler.compileInHost(types[i]));
128+
}
129+
return Promise.all(promises).then((protoViews: Array<ProtoViewRef>) => {
130+
var protoViewRefMap: ProtoViewRefMap = {};
131+
var types = this.componentTypes;
132+
for (var i = 0; i < protoViews.length; i++) {
133+
protoViewRefMap[getComponentSelector(types[i])] = protoViews[i];
134+
}
135+
return protoViewRefMap;
136+
}, onError);
137+
}
138+
}
139+
140+
interface ProtoViewRefMap {
141+
[selector: string]: ProtoViewRef
142+
}
143+
144+
function ng1ComponentDirective(selector: string, type: Type, idPrefix: string): Function {
145+
directiveFactory.$inject = [PROTO_VIEW_REF_MAP, APP_VIEW_MANAGER];
146+
function directiveFactory(protoViewRefMap: ProtoViewRefMap, viewManager: AppViewManager):
147+
angular.IDirective {
148+
var protoView: ProtoViewRef = protoViewRefMap[selector];
149+
if (!protoView) throw new Error('Expecting ProtoViewRef for: ' + selector);
150+
var idCount = 0;
151+
return {
152+
restrict: 'E',
153+
require: REQUIRE_INJECTOR,
154+
link: (scope: angular.IScope, element: angular.IAugmentedJQuery, attrs: angular.IAttributes,
155+
parentInjector: any, transclude: angular.ITranscludeFunction): void => {
156+
var id = element[0].id = idPrefix + (idCount++);
157+
var childInjector = parentInjector.resolveAndCreateChild([bind(NG1_SCOPE).toValue(scope)]);
158+
var hostViewRef = viewManager.createRootHostView(protoView, '#' + id, childInjector);
159+
var changeDetector: ChangeDetectorRef = hostViewRef.changeDetectorRef;
160+
scope.$watch(() => changeDetector.detectChanges());
161+
element.bind('$remove', () => viewManager.destroyRootHostView(hostViewRef));
162+
}
163+
};
164+
}
165+
return directiveFactory;
166+
}
167+
168+
export class UpgradeRef {
169+
readyFn: Function;
170+
171+
ready(fn: Function) { this.readyFn = fn; }
172+
}

modules/upgrade/src/util.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
export function stringify(obj: any): string {
3+
if (typeof obj == 'function') return obj.name || obj.toString();
4+
return '' + obj;
5+
}
6+
7+
8+
export function onError(e: any) {
9+
// TODO: (misko): We seem to not have a stack trace here!
10+
console.log(e, e.stack);
11+
throw e;
12+
}

modules/upgrade/test/integration_spec.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,58 @@ import {
1111
xit,
1212
} from 'angular2/test_lib';
1313

14+
import {Component, View, Inject} from 'angular2/angular2';
15+
import {createUpgradeModule, UpgradeModule, bootstrapHybrid} from 'upgrade/upgrade';
1416

1517
export function main() {
16-
describe('upgrade integration',
17-
() => { it('should run', () => { expect(angular.version.major).toBe(1); }); });
18+
describe('upgrade: ng1 to ng2', () => {
19+
it('should have angular 1 loaded', () => expect(angular.version.major).toBe(1));
20+
21+
it('should instantiate ng2 in ng1 template', inject([AsyncTestCompleter], (async) => {
22+
var element = html("<div>{{ 'ng1-' }}<ng2>~~</ng2>{{ '-ng1' }}</div>");
23+
24+
var upgradeModule: UpgradeModule = createUpgradeModule();
25+
upgradeModule.importNg2Component(SimpleComponent);
26+
upgradeModule.bootstrap(element).ready(() => {
27+
expect(document.body.textContent).toEqual("ng1-NG2-ng1");
28+
async.done();
29+
});
30+
}));
31+
32+
it('should instantiate ng1 in ng2 template', inject([AsyncTestCompleter], (async) => {
33+
var element = html("<div>{{'ng1('}}<ng2-1></ng2-1>{{')'}}</div>");
34+
35+
ng1inNg2Module.bootstrap(element).ready(() => {
36+
expect(document.body.textContent).toEqual("ng1(ng2(ng1 WORKS!))");
37+
async.done();
38+
});
39+
}));
40+
});
41+
}
42+
43+
@Component({selector: 'ng2'})
44+
@View({template: `{{ 'NG2' }}`})
45+
class SimpleComponent {
46+
}
47+
48+
var ng1inNg2Module: UpgradeModule = createUpgradeModule();
49+
50+
@Component({selector: 'ng2-1'})
51+
@View({
52+
template: `{{ 'ng2(' }}<ng1></ng1>{{ ')' }}`,
53+
directives: [ng1inNg2Module.exportAsNg2Component('ng1')]
54+
})
55+
class Ng2ContainsNg1 {
56+
}
57+
58+
ng1inNg2Module.ng1Module.directive('ng1', () => { return {template: 'ng1 {{ "WORKS" }}!'}; });
59+
ng1inNg2Module.importNg2Component(Ng2ContainsNg1);
60+
61+
62+
function html(html: string): Element {
63+
var body = document.body;
64+
body.innerHTML = html;
65+
if (body.childNodes.length == 1 && body.firstChild instanceof HTMLElement)
66+
return <Element>body.firstChild;
67+
return body;
1868
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {
2+
AsyncTestCompleter,
3+
beforeEach,
4+
ddescribe,
5+
describe,
6+
expect,
7+
iit,
8+
inject,
9+
it,
10+
xdescribe,
11+
xit,
12+
} from 'angular2/test_lib';
13+
14+
import {Component, View} from 'angular2/angular2';
15+
import {getComponentSelector} from 'upgrade/src/metadata';
16+
17+
export function main() {
18+
describe('upgrade metadata', () => {
19+
it('should extract component selector',
20+
() => { expect(getComponentSelector(ElementNameComponent)).toEqual('elementNameDashed'); });
21+
22+
23+
describe('errors', () => {
24+
it('should throw on missing selector', () => {
25+
expect(() => getComponentSelector(AttributeNameComponent))
26+
.toThrowErrorWith(
27+
"Only selectors matching element names are supported, got: [attr-name]");
28+
});
29+
30+
it('should throw on non element names', () => {
31+
expect(() => getComponentSelector(NoAnnotationComponent))
32+
.toThrowErrorWith("No Directive annotation found on NoAnnotationComponent");
33+
});
34+
35+
});
36+
});
37+
}
38+
39+
@Component({selector: 'element-name-dashed'})
40+
@View({template: ``})
41+
class ElementNameComponent {
42+
}
43+
44+
@Component({selector: '[attr-name]'})
45+
@View({template: ``})
46+
class AttributeNameComponent {
47+
}
48+
49+
class NoAnnotationComponent {}

modules/upgrade/upgrade.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {createUpgradeModule, UpgradeModule} from './src/upgrade_module';

0 commit comments

Comments
 (0)
X Tutup