-
Notifications
You must be signed in to change notification settings - Fork 27.1k
feat(upgrade): support binding of Ng2 form Ng1 #4458
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
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(); | ||
|
Contributor
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.
Contributor
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] ?
Contributor
Author
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
Contributor
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); | ||
|
Contributor
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
Contributor
Author
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; | ||
|
Contributor
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)
Contributor
Author
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) { | ||
|
Contributor
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 ?
Contributor
Author
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