Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(upgrade): support binding of Ng2 form Ng1 #4458

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
50 changes: 48 additions & 2 deletions modules/upgrade/src/metadata.ts
Expand Up @@ -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;
bracketParanAttr: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bracketParenAttr (a -> e) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parts[1].trim() || prop but I don't think it make sense for part1 to be empty or ws only, right ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a need for snake to camel case on parts[1] ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parts[1] may not exist in case of foo only when we have foo:bar does it exist.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my concern here is that ' ' would be truthy, not sure if that's really an issue

var Attr = attr.charAt(0).toUpperCase() + attr.substr(1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Attr" vs "attr" is kind of nice but does not follow coding conventions and could not be easily differentiate

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed to capitalAttr

attrProps.push({
prop: prop,
attr: attr,
bracketAttr: `[${attr}]`,
parenAttr: `(${attr})`,
bracketParanAttr: `[(${attr})]`
onAttr: `on${Attr}`,
bindAttr: `bind${Attr}`,
bindonAttr: `bindon${Attr}`
});
}
}
return attrProps;
}
171 changes: 151 additions & 20 deletions modules/upgrade/src/upgrade_module.ts
Expand Up @@ -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/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';
Expand All @@ -39,6 +40,7 @@ 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;
Expand All @@ -57,9 +59,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;
}

Expand Down Expand Up @@ -132,7 +134,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);
Expand All @@ -143,32 +145,161 @@ 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)) {
attrs.$observe(input.attr, ((prop) => {
var prevValue = this;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a comment on why "this" ? (initial unique ID)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return (value) => {
if (this.inputChanges != null) {
this.inputChangeCount++;
this.inputChanges[prop] =
new Ng1Change(value, prevValue == this ? value : prevValue);
prevValue = value;
}
this.component[prop] = value;
}
})(input.prop));
} 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.bracketParanAttr)) {
expr = attrs[input.bracketParanAttr];
}
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add an explicit ref to the OnChanges interface as a comment ?
Could help to maintain code in sync in needed at some point

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, done.

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.bracketParanAttr)) {
expr = attrs[output.bracketParanAttr];
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;

Expand Down