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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a need for snake to camel case on parts[1] ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Parts[1] may not exist in case of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. my concern here is that |
||
var Attr = attr.charAt(0).toUpperCase() + attr.substr(1); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. renamed to |
||
attrProps.push({ | ||
prop: prop, | ||
attr: attr, | ||
bracketAttr: `[${attr}]`, | ||
parenAttr: `(${attr})`, | ||
bracketParanAttr: `[(${attr})]` | ||
onAttr: `on${Attr}`, | ||
bindAttr: `bind${Attr}`, | ||
bindonAttr: `bindon${Attr}` | ||
}); | ||
} | ||
} | ||
return attrProps; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
@@ -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; | ||
|
@@ -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; | ||
} | ||
|
||
|
@@ -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); | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add a comment on why "this" ? (initial unique ID) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bracketParenAttr (a -> e) ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed