parent
0b3e4fa090
commit
09371a3f0b
|
@ -14,11 +14,57 @@ if (!(Reflect && (<any>Reflect)['getOwnMetadata'])) {
|
|||
throw 'reflect-metadata shim is required when using class decorators';
|
||||
}
|
||||
|
||||
export function getComponentSelector(type: Type): string {
|
||||
export interface AttrProp {
|
||||
prop: string;
|
||||
attr: string;
|
||||
bracketAttr: string;
|
||||
bracketParenAttr: string;
|
||||
parenAttr: string;
|
||||
onAttr: string;
|
||||
bindAttr: string;
|
||||
bindonAttr: string;
|
||||
}
|
||||
|
||||
export interface ComponentInfo {
|
||||
selector: string;
|
||||
inputs: AttrProp[];
|
||||
outputs: AttrProp[];
|
||||
}
|
||||
|
||||
export function getComponentInfo(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());
|
||||
var selector = selector.replace(SKEWER_CASE, (all, letter: string) => letter.toUpperCase());
|
||||
return {
|
||||
type: type,
|
||||
selector: selector,
|
||||
inputs: parseFields(resolvedMetadata.inputs),
|
||||
outputs: parseFields(resolvedMetadata.outputs)
|
||||
};
|
||||
}
|
||||
|
||||
export function parseFields(names: string[]): AttrProp[] {
|
||||
var attrProps: AttrProp[] = [];
|
||||
if (names) {
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
var parts = names[i].split(':');
|
||||
var prop = parts[0].trim();
|
||||
var attr = (parts[1] || parts[0]).trim();
|
||||
var capitalAttr = attr.charAt(0).toUpperCase() + attr.substr(1);
|
||||
attrProps.push(<AttrProp>{
|
||||
prop: prop,
|
||||
attr: attr,
|
||||
bracketAttr: `[${attr}]`,
|
||||
parenAttr: `(${attr})`,
|
||||
bracketParenAttr: `[(${attr})]`
|
||||
onAttr: `on${capitalAttr}`,
|
||||
bindAttr: `bind${capitalAttr}`,
|
||||
bindonAttr: `bindon${capitalAttr}`
|
||||
});
|
||||
}
|
||||
}
|
||||
return attrProps;
|
||||
}
|
||||
|
|
|
@ -21,13 +21,14 @@ import {
|
|||
ProtoViewRef,
|
||||
ElementRef,
|
||||
HostViewRef,
|
||||
ViewRef
|
||||
ViewRef,
|
||||
SimpleChange
|
||||
} from 'angular2/angular2';
|
||||
import {applicationDomBindings} from 'angular2/src/core/application_common';
|
||||
import {applicationCommonBindings} from '../../angular2/src/core/application_ref';
|
||||
import {compilerBindings} from 'angular2/src/core/compiler/compiler';
|
||||
|
||||
import {getComponentSelector} from './metadata';
|
||||
import {getComponentInfo, ComponentInfo} from './metadata';
|
||||
import {onError} from './util';
|
||||
export const INJECTOR = 'ng2.Injector';
|
||||
export const APP_VIEW_MANAGER = 'ng2.AppViewManager';
|
||||
|
@ -39,10 +40,12 @@ const NG1_REQUIRE_INJECTOR_REF = '$' + INJECTOR + 'Controller';
|
|||
const NG1_SCOPE = '$scope';
|
||||
const NG1_COMPILE = '$compile';
|
||||
const NG1_INJECTOR = '$injector';
|
||||
const NG1_PARSE = '$parse';
|
||||
const REQUIRE_INJECTOR = '^' + INJECTOR;
|
||||
|
||||
var moduleCount: number = 0;
|
||||
const CAMEL_CASE = /([A-Z])/g;
|
||||
var INITIAL_VALUE = {};
|
||||
|
||||
export function createUpgradeModule(): UpgradeModule {
|
||||
var prefix = `NG2_UPGRADE_m${moduleCount++}_`;
|
||||
|
@ -57,9 +60,9 @@ export class UpgradeModule {
|
|||
|
||||
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);
|
||||
var info: ComponentInfo = getComponentInfo(type);
|
||||
var factory: Function = ng1ComponentDirective(info, `${this.idPrefix}${info.selector}_c`);
|
||||
this.ng1Module.directive(info.selector, <any[]>factory);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -132,7 +135,7 @@ export class UpgradeModule {
|
|||
var protoViewRefMap: ProtoViewRefMap = {};
|
||||
var types = this.componentTypes;
|
||||
for (var i = 0; i < protoViews.length; i++) {
|
||||
protoViewRefMap[getComponentSelector(types[i])] = protoViews[i];
|
||||
protoViewRefMap[getComponentInfo(types[i]).selector] = protoViews[i];
|
||||
}
|
||||
return protoViewRefMap;
|
||||
}, onError);
|
||||
|
@ -143,32 +146,163 @@ 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);
|
||||
function ng1ComponentDirective(info: ComponentInfo, idPrefix: string): Function {
|
||||
directiveFactory.$inject = [PROTO_VIEW_REF_MAP, APP_VIEW_MANAGER, NG1_PARSE];
|
||||
function directiveFactory(protoViewRefMap: ProtoViewRefMap, viewManager: AppViewManager,
|
||||
parse: angular.IParseService): angular.IDirective {
|
||||
var protoView: ProtoViewRef = protoViewRefMap[info.selector];
|
||||
if (!protoView) throw new Error('Expecting ProtoViewRef for: ' + info.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 componentScope = scope.$new();
|
||||
componentScope.$watch(() => changeDetector.detectChanges());
|
||||
var childInjector =
|
||||
parentInjector.resolveAndCreateChild([bind(NG1_SCOPE).toValue(componentScope)]);
|
||||
var hostViewRef = viewManager.createRootHostView(protoView, '#' + id, childInjector);
|
||||
var changeDetector: ChangeDetectorRef = hostViewRef.changeDetectorRef;
|
||||
element.bind('$remove', () => viewManager.destroyRootHostView(hostViewRef));
|
||||
var facade =
|
||||
new Ng2ComponentFacade(element[0].id = idPrefix + (idCount++), info, element, attrs,
|
||||
scope, <Injector>parentInjector, parse, viewManager, protoView);
|
||||
|
||||
facade.setupInputs();
|
||||
facade.bootstrapNg2();
|
||||
facade.setupOutputs();
|
||||
facade.registerCleanup();
|
||||
}
|
||||
};
|
||||
}
|
||||
return directiveFactory;
|
||||
}
|
||||
|
||||
class Ng2ComponentFacade {
|
||||
component: any = null;
|
||||
inputChangeCount: number = 0;
|
||||
inputChanges: StringMap<string, SimpleChange> = null;
|
||||
hostViewRef: HostViewRef = null;
|
||||
changeDetector: ChangeDetectorRef = null;
|
||||
componentScope: angular.IScope;
|
||||
|
||||
constructor(private id: string, private info: ComponentInfo,
|
||||
private element: angular.IAugmentedJQuery, private attrs: angular.IAttributes,
|
||||
private scope: angular.IScope, private parentInjector: Injector,
|
||||
private parse: angular.IParseService, private viewManager: AppViewManager,
|
||||
private protoView: ProtoViewRef) {
|
||||
this.componentScope = scope.$new();
|
||||
}
|
||||
|
||||
bootstrapNg2() {
|
||||
var childInjector =
|
||||
this.parentInjector.resolveAndCreateChild([bind(NG1_SCOPE).toValue(this.componentScope)]);
|
||||
this.hostViewRef =
|
||||
this.viewManager.createRootHostView(this.protoView, '#' + this.id, childInjector);
|
||||
var hostElement = this.viewManager.getHostElement(this.hostViewRef);
|
||||
this.changeDetector = this.hostViewRef.changeDetectorRef;
|
||||
this.component = this.viewManager.getComponent(hostElement);
|
||||
}
|
||||
|
||||
setupInputs() {
|
||||
var attrs = this.attrs;
|
||||
var inputs = this.info.inputs;
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
var input = inputs[i];
|
||||
var expr = null;
|
||||
if (attrs.hasOwnProperty(input.attr)) {
|
||||
var observeFn = ((prop) => {
|
||||
var prevValue = INITIAL_VALUE;
|
||||
return (value) => {
|
||||
if (this.inputChanges !== null) {
|
||||
this.inputChangeCount++;
|
||||
this.inputChanges[prop] =
|
||||
new Ng1Change(value, prevValue === INITIAL_VALUE ? value : prevValue);
|
||||
prevValue = value;
|
||||
}
|
||||
this.component[prop] = value;
|
||||
}
|
||||
})(input.prop);
|
||||
attrs.$observe(input.attr, observeFn);
|
||||
} else if (attrs.hasOwnProperty(input.bindAttr)) {
|
||||
expr = attrs[input.bindAttr];
|
||||
} else if (attrs.hasOwnProperty(input.bracketAttr)) {
|
||||
expr = attrs[input.bracketAttr];
|
||||
} else if (attrs.hasOwnProperty(input.bindonAttr)) {
|
||||
expr = attrs[input.bindonAttr];
|
||||
} else if (attrs.hasOwnProperty(input.bracketParenAttr)) {
|
||||
expr = attrs[input.bracketParenAttr];
|
||||
}
|
||||
if (expr != null) {
|
||||
var watchFn = ((prop) => (value, prevValue) => {
|
||||
if (this.inputChanges != null) {
|
||||
this.inputChangeCount++;
|
||||
this.inputChanges[prop] = new Ng1Change(prevValue, value);
|
||||
}
|
||||
this.component[prop] = value;
|
||||
})(input.prop);
|
||||
this.componentScope.$watch(expr, watchFn);
|
||||
}
|
||||
}
|
||||
|
||||
var prototype = this.info.type.prototype;
|
||||
if (prototype && prototype.onChanges) {
|
||||
// Detect: OnChanges interface
|
||||
this.inputChanges = {};
|
||||
this.componentScope.$watch(() => this.inputChangeCount, () => {
|
||||
var inputChanges = this.inputChanges;
|
||||
this.inputChanges = {};
|
||||
this.component.onChanges(inputChanges);
|
||||
});
|
||||
}
|
||||
this.componentScope.$watch(() => this.changeDetector.detectChanges());
|
||||
}
|
||||
|
||||
setupOutputs() {
|
||||
var attrs = this.attrs;
|
||||
var outputs = this.info.outputs;
|
||||
for (var j = 0; j < outputs.length; j++) {
|
||||
var output = outputs[j];
|
||||
var expr = null;
|
||||
var assignExpr = false;
|
||||
if (attrs.hasOwnProperty(output.onAttr)) {
|
||||
expr = attrs[output.onAttr];
|
||||
} else if (attrs.hasOwnProperty(output.parenAttr)) {
|
||||
expr = attrs[output.parenAttr];
|
||||
} else if (attrs.hasOwnProperty(output.bindonAttr)) {
|
||||
expr = attrs[output.bindonAttr];
|
||||
assignExpr = true;
|
||||
} else if (attrs.hasOwnProperty(output.bracketParenAttr)) {
|
||||
expr = attrs[output.bracketParenAttr];
|
||||
assignExpr = true;
|
||||
}
|
||||
|
||||
if (expr != null && assignExpr != null) {
|
||||
var getter = this.parse(expr);
|
||||
var setter = getter.assign;
|
||||
if (assignExpr && !setter) {
|
||||
throw new Error(`Expression '${expr}' is not assignable!`);
|
||||
}
|
||||
var emitter = this.component[output.prop];
|
||||
if (emitter) {
|
||||
emitter.observer({
|
||||
next: assignExpr ? ((setter) => (value) => setter(this.scope, value))(setter) :
|
||||
((getter) => (value) => getter(this.scope, {$event: value}))(getter)
|
||||
});
|
||||
} else {
|
||||
throw new Error(
|
||||
`Missing emitter '${output.prop}' on component '${this.input.selector}'!`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerCleanup() {
|
||||
this.element.bind('$remove', () => this.viewManager.destroyRootHostView(this.hostViewRef));
|
||||
}
|
||||
}
|
||||
|
||||
export class Ng1Change implements SimpleChange {
|
||||
constructor(public previousValue: any, public currentValue: any) {}
|
||||
|
||||
isFirstChange(): boolean { return this.previousValue === this.currentValue; }
|
||||
}
|
||||
|
||||
|
||||
export class UpgradeRef {
|
||||
readyFn: Function;
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
xit,
|
||||
} from 'angular2/test_lib';
|
||||
|
||||
import {Component, View, Inject} from 'angular2/angular2';
|
||||
import {Component, View, Inject, EventEmitter} from 'angular2/angular2';
|
||||
import {createUpgradeModule, UpgradeModule, bootstrapHybrid} from 'upgrade/upgrade';
|
||||
|
||||
export function main() {
|
||||
|
@ -91,9 +91,127 @@ export function main() {
|
|||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('binding from ng1 to ng2', () => {
|
||||
it('should bind properties, events', inject([AsyncTestCompleter], (async) {
|
||||
var upgrMod: UpgradeModule = createUpgradeModule();
|
||||
upgrMod.ng1Module.run(($rootScope) => {
|
||||
$rootScope.dataA = 'A';
|
||||
$rootScope.dataB = 'B';
|
||||
$rootScope.modelA = 'initModelA';
|
||||
$rootScope.modelB = 'initModelB';
|
||||
$rootScope.eventA = '?';
|
||||
$rootScope.eventB = '?';
|
||||
});
|
||||
upgrMod.importNg2Component(
|
||||
Component({
|
||||
selector: 'ng2',
|
||||
inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'],
|
||||
outputs:
|
||||
['eventA', 'eventB', 'twoWayAEmitter: twoWayA', 'twoWayBEmitter: twoWayB']
|
||||
})
|
||||
.View({
|
||||
template:
|
||||
"ignore: {{ignore}}; " +
|
||||
"literal: {{literal}}; interpolate: {{interpolate}}; " +
|
||||
"oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; " +
|
||||
"twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{onChangesCount}})"
|
||||
})
|
||||
.Class({
|
||||
constructor: function() {
|
||||
this.onChangesCount = 0;
|
||||
this.ignore = '-';
|
||||
this.literal = '?';
|
||||
this.interpolate = '?';
|
||||
this.oneWayA = '?';
|
||||
this.oneWayB = '?';
|
||||
this.twoWayA = '?';
|
||||
this.twoWayB = '?';
|
||||
this.eventA = new EventEmitter();
|
||||
this.eventB = new EventEmitter();
|
||||
this.twoWayAEmitter = new EventEmitter();
|
||||
this.twoWayBEmitter = new EventEmitter();
|
||||
},
|
||||
onChanges: function(changes) {
|
||||
var assert =
|
||||
(prop, value) => {
|
||||
if (this[prop] != value) {
|
||||
throw new Error(
|
||||
`Expected: '${prop}' to be '${value}' but was '${this[prop]}'`);
|
||||
}
|
||||
}
|
||||
|
||||
var assertChange =
|
||||
(prop, value) => {
|
||||
assert(prop, value);
|
||||
if (!changes[prop]) {
|
||||
throw new Error(`Changes record for '${prop}' not found.`);
|
||||
}
|
||||
var actValue = changes[prop].currentValue;
|
||||
if (actValue != value) {
|
||||
throw new Error(
|
||||
`Expected changes record for'${prop}' to be '${value}' but was '${actValue}'`);
|
||||
}
|
||||
}
|
||||
|
||||
switch (this.onChangesCount++) {
|
||||
case 0:
|
||||
assert('ignore', '-');
|
||||
assertChange('literal', 'Text');
|
||||
assertChange('interpolate', 'Hello world');
|
||||
assertChange('oneWayA', 'A');
|
||||
assertChange('oneWayB', 'B');
|
||||
assertChange('twoWayA', 'initModelA');
|
||||
assertChange('twoWayB', 'initModelB');
|
||||
|
||||
this.twoWayAEmitter.next('newA');
|
||||
this.twoWayBEmitter.next('newB');
|
||||
this.eventA.next('aFired');
|
||||
this.eventB.next('bFired');
|
||||
break;
|
||||
case 1:
|
||||
assertChange('twoWayA', 'newA');
|
||||
break;
|
||||
case 2:
|
||||
assertChange('twoWayB', 'newB');
|
||||
break;
|
||||
default:
|
||||
throw new Error('Called too many times! ' + JSON.stringify(changes));
|
||||
}
|
||||
}
|
||||
}));
|
||||
var element = html(`<div>
|
||||
<ng2 literal="Text" interpolate="Hello {{'world'}}"
|
||||
bind-one-way-a="dataA" [one-way-b]="dataB"
|
||||
bindon-two-way-a="modelA" [(two-way-b)]="modelB"
|
||||
on-event-a='eventA=$event' (event-b)="eventB=$event"></ng2>
|
||||
| modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}};
|
||||
</div>`);
|
||||
upgrMod.bootstrap(element).ready(() => {
|
||||
expect(multiTrim(document.body.textContent))
|
||||
.toEqual(
|
||||
"ignore: -; " + "literal: Text; interpolate: Hello world; " +
|
||||
"oneWayA: A; oneWayB: B; twoWayA: initModelA; twoWayB: initModelB; (1) | " +
|
||||
"modelA: initModelA; modelB: initModelB; eventA: ?; eventB: ?;");
|
||||
setTimeout(() => {
|
||||
// we need to do setTimeout, because the EventEmitter uses setTimeout to schedule
|
||||
// events, and so without this we would not see the events processed.
|
||||
expect(multiTrim(document.body.textContent))
|
||||
.toEqual("ignore: -; " + "literal: Text; interpolate: Hello world; " +
|
||||
"oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | " +
|
||||
"modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;");
|
||||
async.done();
|
||||
});
|
||||
});
|
||||
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function multiTrim(text: string): string {
|
||||
return text.replace(/\n/g, '').replace(/\s\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function html(html: string): Element {
|
||||
var body = document.body;
|
||||
|
|
|
@ -12,27 +12,57 @@ import {
|
|||
} from 'angular2/test_lib';
|
||||
|
||||
import {Component, View} from 'angular2/angular2';
|
||||
import {getComponentSelector} from 'upgrade/src/metadata';
|
||||
import {getComponentInfo, parseFields} from 'upgrade/src/metadata';
|
||||
|
||||
export function main() {
|
||||
describe('upgrade metadata', () => {
|
||||
it('should extract component selector',
|
||||
() => { expect(getComponentSelector(ElementNameComponent)).toEqual('elementNameDashed'); });
|
||||
it('should extract component selector', () => {
|
||||
expect(getComponentInfo(ElementNameComponent).selector).toEqual('elementNameDashed');
|
||||
});
|
||||
|
||||
|
||||
describe('errors', () => {
|
||||
it('should throw on missing selector', () => {
|
||||
expect(() => getComponentSelector(AttributeNameComponent))
|
||||
expect(() => getComponentInfo(AttributeNameComponent))
|
||||
.toThrowErrorWith(
|
||||
"Only selectors matching element names are supported, got: [attr-name]");
|
||||
});
|
||||
|
||||
it('should throw on non element names', () => {
|
||||
expect(() => getComponentSelector(NoAnnotationComponent))
|
||||
expect(() => getComponentInfo(NoAnnotationComponent))
|
||||
.toThrowErrorWith("No Directive annotation found on NoAnnotationComponent");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('parseFields', () => {
|
||||
it('should process nulls', () => { expect(parseFields(null)).toEqual([]); });
|
||||
|
||||
it('should process values', () => {
|
||||
expect(parseFields([' name ', ' prop : attr ']))
|
||||
.toEqual([
|
||||
{
|
||||
prop: 'name',
|
||||
attr: 'name',
|
||||
bracketAttr: '[name]',
|
||||
parenAttr: '(name)',
|
||||
bracketParenAttr: '[(name)]',
|
||||
onAttr: 'onName',
|
||||
bindAttr: 'bindName',
|
||||
bindonAttr: 'bindonName'
|
||||
},
|
||||
{
|
||||
prop: 'prop',
|
||||
attr: 'attr',
|
||||
bracketAttr: '[attr]',
|
||||
parenAttr: '(attr)',
|
||||
bracketParenAttr: '[(attr)]',
|
||||
onAttr: 'onAttr',
|
||||
bindAttr: 'bindAttr',
|
||||
bindonAttr: 'bindonAttr'
|
||||
}
|
||||
]);
|
||||
});
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue