Skip to content

Commit 8e1d2fb

Browse files
committed
feat(upgrade): support binding of Ng1 form Ng2
Closes #4542
1 parent bb4fd2d commit 8e1d2fb

6 files changed

Lines changed: 474 additions & 204 deletions

File tree

modules/upgrade/src/angular.d.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
declare namespace angular {
22
function module(prefix: string, dependencies?: string[]);
33
interface IModule {
4+
config(fn: any): IModule;
45
directive(selector: string, factory: any): IModule;
56
value(key: string, value: any): IModule;
67
run(a: any);
@@ -13,11 +14,19 @@ declare namespace angular {
1314
$watch(expr: any, fn?: (a1?: any, a2?: any) => void);
1415
$apply(): any;
1516
$apply(exp: string): any;
16-
$apply(exp: (scope: IScope) => any): any;
17+
$apply(exp: Function): any;
18+
$$childTail: IScope;
19+
$$childHead: IScope;
20+
$$nextSibling: IScope;
1721
}
1822
interface IScope extends IRootScopeService {}
1923
interface IAngularBootstrapConfig {}
20-
interface IDirective {}
24+
interface IDirective {
25+
require?: string;
26+
restrict?: string;
27+
scope?: {[key: string]: string};
28+
link?: Function;
29+
}
2130
interface IAttributes {
2231
$observe(attr: string, fn: (v: string) => void);
2332
}

modules/upgrade/src/constants.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export const NG2_APP_VIEW_MANAGER = 'ng2.AppViewManager';
2+
export const NG2_COMPILER = 'ng2.Compiler';
3+
export const NG2_INJECTOR = 'ng2.Injector';
4+
export const NG2_PROTO_VIEW_REF_MAP = 'ng2.ProtoViewRefMap';
5+
export const NG2_ZONE = 'ng2.NgZone';
6+
7+
export const NG1_REQUIRE_INJECTOR_REF = '$' + NG2_INJECTOR + 'Controller';
8+
export const NG1_SCOPE = '$scope';
9+
export const NG1_ROOT_SCOPE = '$rootScope';
10+
export const NG1_COMPILE = '$compile';
11+
export const NG1_INJECTOR = '$injector';
12+
export const NG1_PARSE = '$parse';
13+
export const REQUIRE_INJECTOR = '^' + NG2_INJECTOR;

modules/upgrade/src/ng1_facade.ts

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import {
2+
Directive,
3+
DoCheck,
4+
ElementRef,
5+
EventEmitter,
6+
Inject,
7+
OnChanges,
8+
SimpleChange,
9+
Type
10+
} from 'angular2/angular2';
11+
import {NG1_COMPILE, NG1_SCOPE} from './constants';
12+
13+
const CAMEL_CASE = /([A-Z])/g;
14+
const INITIAL_VALUE = {
15+
__UNINITIALIZED__: true
16+
};
17+
18+
19+
export class ExportedNg1Component {
20+
type: Type;
21+
inputs: string[] = [];
22+
inputsRename: string[] = [];
23+
outputs: string[] = [];
24+
outputsRename: string[] = [];
25+
propertyOutputs: string[] = [];
26+
checkProperties: string[] = [];
27+
propertyMap: {[name: string]: string} = {};
28+
29+
constructor(public name: string) {
30+
var selector = name.replace(CAMEL_CASE, (all, next: string) => '-' + next.toLowerCase());
31+
var self = this;
32+
this.type =
33+
Directive({selector: selector, inputs: this.inputsRename, outputs: this.outputsRename})
34+
.Class({
35+
constructor: [
36+
new Inject(NG1_COMPILE),
37+
new Inject(NG1_SCOPE),
38+
ElementRef,
39+
function(compile: angular.ICompileService, scope: angular.IScope,
40+
elementRef: ElementRef) {
41+
return new Ng1ComponentFacade(compile, scope, elementRef, self.inputs,
42+
self.outputs, self.propertyOutputs,
43+
self.checkProperties, self.propertyMap);
44+
}
45+
],
46+
onChanges: function() { /* needs to be here for ng2 to properly detect it */ },
47+
doCheck: function() { /* needs to be here for ng2 to properly detect it */ }
48+
});
49+
}
50+
51+
extractBindings(injector: angular.auto.IInjectorService) {
52+
var directives: angular.IDirective[] = injector.get(this.name + 'Directive');
53+
if (directives.length > 1) {
54+
throw new Error('Only support single directive definition for: ' + this.name);
55+
}
56+
var directive = directives[0];
57+
var scope = directive.scope;
58+
if (typeof scope == 'object') {
59+
for (var name in scope) {
60+
if (scope.hasOwnProperty(name)) {
61+
var localName = scope[name];
62+
var type = localName.charAt(0);
63+
localName = localName.substr(1) || name;
64+
var outputName = 'output_' + name;
65+
var outputNameRename = outputName + ': ' + name;
66+
var inputName = 'input_' + name;
67+
var inputNameRename = inputName + ': ' + name;
68+
switch (type) {
69+
case '=':
70+
this.propertyOutputs.push(outputName);
71+
this.checkProperties.push(localName);
72+
this.outputs.push(outputName);
73+
this.outputsRename.push(outputNameRename);
74+
this.propertyMap[outputName] = localName;
75+
// don't break; let it fall through to '@'
76+
case '@':
77+
this.inputs.push(inputName);
78+
this.inputsRename.push(inputNameRename);
79+
this.propertyMap[inputName] = localName;
80+
break;
81+
case '&':
82+
this.outputs.push(outputName);
83+
this.outputsRename.push(outputNameRename);
84+
this.propertyMap[outputName] = localName;
85+
break;
86+
default:
87+
var json = JSON.stringify(scope);
88+
throw new Error(
89+
`Unexpected mapping '${type}' in '${json}' in '${this.name}' directive.`);
90+
}
91+
}
92+
}
93+
}
94+
}
95+
96+
static resolve(exportedComponents: {[name: string]: ExportedNg1Component},
97+
injector: angular.auto.IInjectorService) {
98+
for (var name in exportedComponents) {
99+
if (exportedComponents.hasOwnProperty(name)) {
100+
var exportedComponent = exportedComponents[name];
101+
exportedComponent.extractBindings(injector);
102+
}
103+
}
104+
}
105+
}
106+
107+
class Ng1ComponentFacade implements OnChanges, DoCheck {
108+
componentScope: angular.IScope = null;
109+
checkLastValues: any[] = [];
110+
111+
constructor(compile: angular.ICompileService, scope: angular.IScope, elementRef: ElementRef,
112+
private inputs: string[], private outputs: string[], private propOuts: string[],
113+
private checkProperties: string[], private propertyMap: {[key: string]: string}) {
114+
var chailTail = scope.$$childTail; // remember where the next scope is inserted
115+
compile(elementRef.nativeElement)(scope);
116+
117+
// If we are first scope take it, otherwise take the next one in list.
118+
this.componentScope = chailTail ? chailTail.$$nextSibling : scope.$$childHead;
119+
120+
for (var i = 0; i < inputs.length; i++) {
121+
this[inputs[i]] = null;
122+
}
123+
for (var j = 0; j < outputs.length; j++) {
124+
var emitter = this[outputs[j]] = new EventEmitter();
125+
this.setComponentProperty(outputs[j], ((emitter) => (value) => emitter.next(value))(emitter));
126+
}
127+
for (var k = 0; k < propOuts.length; k++) {
128+
this[propOuts[k]] = new EventEmitter();
129+
this.checkLastValues.push(INITIAL_VALUE);
130+
}
131+
}
132+
133+
onChanges(changes) {
134+
for (var name in changes) {
135+
if (changes.hasOwnProperty(name)) {
136+
var change: SimpleChange = changes[name];
137+
this.setComponentProperty(name, change.currentValue);
138+
}
139+
}
140+
}
141+
142+
doCheck() {
143+
var count = 0;
144+
var scope = this.componentScope;
145+
var lastValues = this.checkLastValues;
146+
var checkProperties = this.checkProperties;
147+
for (var i = 0; i < checkProperties.length; i++) {
148+
var value = scope[checkProperties[i]];
149+
var last = lastValues[i];
150+
if (value !== last) {
151+
if (typeof value == 'number' && isNaN(value) && typeof last == 'number' && isNaN(last)) {
152+
// ignore because NaN != NaN
153+
} else {
154+
var eventEmitter: EventEmitter = this[this.propOuts[i]];
155+
eventEmitter.next(lastValues[i] = value);
156+
}
157+
}
158+
}
159+
return count;
160+
}
161+
162+
setComponentProperty(name: string, value: any) {
163+
this.componentScope[this.propertyMap[name]] = value;
164+
}
165+
}

modules/upgrade/src/ng2_facade.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import {
2+
bind,
3+
AppViewManager,
4+
ChangeDetectorRef,
5+
HostViewRef,
6+
Injector,
7+
ProtoViewRef,
8+
SimpleChange
9+
} from 'angular2/angular2';
10+
import {NG1_SCOPE} from './constants';
11+
import {ComponentInfo} from './metadata';
12+
13+
const INITIAL_VALUE = {
14+
__UNINITIALIZED__: true
15+
};
16+
17+
export class Ng2ComponentFacade {
18+
component: any = null;
19+
inputChangeCount: number = 0;
20+
inputChanges: {[key: string]: SimpleChange} = null;
21+
hostViewRef: HostViewRef = null;
22+
changeDetector: ChangeDetectorRef = null;
23+
componentScope: angular.IScope;
24+
25+
constructor(private id: string, private info: ComponentInfo,
26+
private element: angular.IAugmentedJQuery, private attrs: angular.IAttributes,
27+
private scope: angular.IScope, private parentInjector: Injector,
28+
private parse: angular.IParseService, private viewManager: AppViewManager,
29+
private protoView: ProtoViewRef) {
30+
this.componentScope = scope.$new();
31+
}
32+
33+
bootstrapNg2() {
34+
var childInjector =
35+
this.parentInjector.resolveAndCreateChild([bind(NG1_SCOPE).toValue(this.componentScope)]);
36+
this.hostViewRef =
37+
this.viewManager.createRootHostView(this.protoView, '#' + this.id, childInjector);
38+
var hostElement = this.viewManager.getHostElement(this.hostViewRef);
39+
this.changeDetector = this.hostViewRef.changeDetectorRef;
40+
this.component = this.viewManager.getComponent(hostElement);
41+
}
42+
43+
setupInputs() {
44+
var attrs = this.attrs;
45+
var inputs = this.info.inputs;
46+
for (var i = 0; i < inputs.length; i++) {
47+
var input = inputs[i];
48+
var expr = null;
49+
if (attrs.hasOwnProperty(input.attr)) {
50+
var observeFn = ((prop) => {
51+
var prevValue = INITIAL_VALUE;
52+
return (value) => {
53+
if (this.inputChanges !== null) {
54+
this.inputChangeCount++;
55+
this.inputChanges[prop] =
56+
new Ng1Change(value, prevValue === INITIAL_VALUE ? value : prevValue);
57+
prevValue = value;
58+
}
59+
this.component[prop] = value;
60+
}
61+
})(input.prop);
62+
attrs.$observe(input.attr, observeFn);
63+
} else if (attrs.hasOwnProperty(input.bindAttr)) {
64+
expr = attrs[input.bindAttr];
65+
} else if (attrs.hasOwnProperty(input.bracketAttr)) {
66+
expr = attrs[input.bracketAttr];
67+
} else if (attrs.hasOwnProperty(input.bindonAttr)) {
68+
expr = attrs[input.bindonAttr];
69+
} else if (attrs.hasOwnProperty(input.bracketParenAttr)) {
70+
expr = attrs[input.bracketParenAttr];
71+
}
72+
if (expr != null) {
73+
var watchFn = ((prop) => (value, prevValue) => {
74+
if (this.inputChanges != null) {
75+
this.inputChangeCount++;
76+
this.inputChanges[prop] = new Ng1Change(prevValue, value);
77+
}
78+
this.component[prop] = value;
79+
})(input.prop);
80+
this.componentScope.$watch(expr, watchFn);
81+
}
82+
}
83+
84+
var prototype = this.info.type.prototype;
85+
if (prototype && prototype.onChanges) {
86+
// Detect: OnChanges interface
87+
this.inputChanges = {};
88+
this.componentScope.$watch(() => this.inputChangeCount, () => {
89+
var inputChanges = this.inputChanges;
90+
this.inputChanges = {};
91+
this.component.onChanges(inputChanges);
92+
});
93+
}
94+
this.componentScope.$watch(() => this.changeDetector.detectChanges());
95+
}
96+
97+
setupOutputs() {
98+
var attrs = this.attrs;
99+
var outputs = this.info.outputs;
100+
for (var j = 0; j < outputs.length; j++) {
101+
var output = outputs[j];
102+
var expr = null;
103+
var assignExpr = false;
104+
if (attrs.hasOwnProperty(output.onAttr)) {
105+
expr = attrs[output.onAttr];
106+
} else if (attrs.hasOwnProperty(output.parenAttr)) {
107+
expr = attrs[output.parenAttr];
108+
} else if (attrs.hasOwnProperty(output.bindonAttr)) {
109+
expr = attrs[output.bindonAttr];
110+
assignExpr = true;
111+
} else if (attrs.hasOwnProperty(output.bracketParenAttr)) {
112+
expr = attrs[output.bracketParenAttr];
113+
assignExpr = true;
114+
}
115+
116+
if (expr != null && assignExpr != null) {
117+
var getter = this.parse(expr);
118+
var setter = getter.assign;
119+
if (assignExpr && !setter) {
120+
throw new Error(`Expression '${expr}' is not assignable!`);
121+
}
122+
var emitter = this.component[output.prop];
123+
if (emitter) {
124+
emitter.observer({
125+
next: assignExpr ? ((setter) => (value) => setter(this.scope, value))(setter) :
126+
((getter) => (value) => getter(this.scope, {$event: value}))(getter)
127+
});
128+
} else {
129+
throw new Error(`Missing emitter '${output.prop}' on component '${this.info.selector}'!`);
130+
}
131+
}
132+
}
133+
}
134+
135+
registerCleanup() {
136+
this.element.bind('$remove', () => this.viewManager.destroyRootHostView(this.hostViewRef));
137+
}
138+
}
139+
140+
class Ng1Change implements SimpleChange {
141+
constructor(public previousValue: any, public currentValue: any) {}
142+
143+
isFirstChange(): boolean { return this.previousValue === this.currentValue; }
144+
}

0 commit comments

Comments
 (0)