From cd90e6ed8f1118ce141b1dd41539ec2785482f20 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Fri, 9 Oct 2015 14:53:04 -0700 Subject: [PATCH] feat(ngUpgrade): support for content project from ng1->ng2 --- .../src/core/render/dom/dom_renderer.ts | 5 +++ modules/angular2/src/core/render/view.ts | 2 +- .../angular2/src/core/render/view_factory.ts | 36 ++++++++++++++----- .../test/core/render/view_factory_spec.ts | 13 +++++++ .../angular2/test/core/render/view_spec.ts | 4 +-- modules/upgrade/src/angular.d.ts | 8 +++-- modules/upgrade/src/ng2_facade.ts | 21 ++++++++++- modules/upgrade/src/upgrade_module.ts | 23 ++++++------ modules/upgrade/test/integration_spec.ts | 9 ++--- 9 files changed, 93 insertions(+), 28 deletions(-) diff --git a/modules/angular2/src/core/render/dom/dom_renderer.ts b/modules/angular2/src/core/render/dom/dom_renderer.ts index 6891107c4d..e97fed9074 100644 --- a/modules/angular2/src/core/render/dom/dom_renderer.ts +++ b/modules/angular2/src/core/render/dom/dom_renderer.ts @@ -50,6 +50,8 @@ export abstract class DomRenderer extends Renderer implements NodeFactory abstract destroyView(viewRef: RenderViewRef); + abstract createRootContentInsertionPoint(); + getNativeElementSync(location: RenderElementRef): any { return resolveInternalDomView(location.renderView).boundElements[location.boundElementIndex]; } @@ -282,6 +284,9 @@ export class DomRenderer_ extends DomRenderer { DOM.setAttribute(node, attrNameAndValues[attrIdx], attrNameAndValues[attrIdx + 1]); } } + createRootContentInsertionPoint(): Node { + return DOM.createComment('root-content-insertion-point'); + } createShadowRoot(host: Node, templateId: number): Node { var sr = DOM.createShadowRoot(host); var styles = this._nativeShadowStyles.get(templateId); diff --git a/modules/angular2/src/core/render/view.ts b/modules/angular2/src/core/render/view.ts index 74a3c76bc2..3cd64f7c1c 100644 --- a/modules/angular2/src/core/render/view.ts +++ b/modules/angular2/src/core/render/view.ts @@ -25,7 +25,7 @@ export class DefaultRenderView extends RenderViewRef { constructor(public fragments: DefaultRenderFragmentRef[], public boundTextNodes: N[], public boundElements: N[], public nativeShadowRoots: N[], - public globalEventAdders: Function[]) { + public globalEventAdders: Function[], public rootContentInsertionPoints: N[]) { super(); } diff --git a/modules/angular2/src/core/render/view_factory.ts b/modules/angular2/src/core/render/view_factory.ts index b66a06487e..ae3fec85fd 100644 --- a/modules/angular2/src/core/render/view_factory.ts +++ b/modules/angular2/src/core/render/view_factory.ts @@ -23,7 +23,8 @@ export function createRenderView(fragmentCmds: RenderTemplateCmd[], inplaceEleme fragments.push(new DefaultRenderFragmentRef(context.fragments[i])); } view = new DefaultRenderView(fragments, context.boundTextNodes, context.boundElements, - context.nativeShadowRoots, context.globalEventAdders); + context.nativeShadowRoots, context.globalEventAdders, + context.rootContentInsertionPoints); return view; } @@ -31,6 +32,7 @@ export interface NodeFactory { resolveComponentTemplate(templateId: number): RenderTemplateCmd[]; createTemplateAnchor(attrNameAndValues: string[]): N; createElement(name: string, attrNameAndValues: string[]): N; + createRootContentInsertionPoint(): N; mergeElement(existing: N, attrNameAndValues: string[]); createShadowRoot(host: N, templateId: number): N; createText(value: string): N; @@ -41,7 +43,9 @@ export interface NodeFactory { class BuildContext { constructor(private _eventDispatcher: Function, public factory: NodeFactory, - private _inplaceElement: N) {} + private _inplaceElement: N) { + this.isHost = isPresent((_inplaceElement)); + } private _builders: RenderViewBuilder[] = []; globalEventAdders: Function[] = []; @@ -49,6 +53,9 @@ class BuildContext { boundTextNodes: N[] = []; nativeShadowRoots: N[] = []; fragments: N[][] = []; + rootContentInsertionPoints: N[] = []; + componentCount: number = 0; + isHost: boolean; build(fragmentCmds: RenderTemplateCmd[]) { this.enqueueFragmentBuilder(null, fragmentCmds); @@ -65,6 +72,7 @@ class BuildContext { } enqueueComponentBuilder(component: Component) { + this.componentCount++; this._builders.push(new RenderViewBuilder( component, null, this.factory.resolveComponentTemplate(component.cmd.templateId))); } @@ -131,10 +139,20 @@ class RenderViewBuilder implements RenderCommandVisitor { } visitNgContent(cmd: RenderNgContentCmd, context: BuildContext): any { if (isPresent(this.parentComponent)) { - var projectedNodes = this.parentComponent.project(cmd.index); - for (var i = 0; i < projectedNodes.length; i++) { - var node = projectedNodes[i]; - this._addChild(node, cmd.ngContentIndex, context); + if (this.parentComponent.isRoot) { + var insertionPoint = context.factory.createRootContentInsertionPoint(); + if (this.parent instanceof Component) { + context.factory.appendChild((>this.parent).shadowRoot, insertionPoint); + } else { + context.factory.appendChild(this.parent, insertionPoint); + } + context.rootContentInsertionPoints.push(insertionPoint); + } else { + var projectedNodes = this.parentComponent.project(cmd.index); + for (var i = 0; i < projectedNodes.length; i++) { + var node = projectedNodes[i]; + this._addChild(node, cmd.ngContentIndex, context); + } } } return null; @@ -154,7 +172,8 @@ class RenderViewBuilder implements RenderCommandVisitor { root = context.factory.createShadowRoot(el, cmd.templateId); context.nativeShadowRoots.push(root); } - var component = new Component(el, root, cmd); + var isRoot = context.componentCount === 0 && context.isHost; + var component = new Component(el, root, cmd, isRoot); context.enqueueComponentBuilder(component); this.parentStack.push(component); return null; @@ -213,7 +232,8 @@ class RenderViewBuilder implements RenderCommandVisitor { class Component { private contentNodesByNgContentIndex: N[][] = []; - constructor(public hostElement: N, public shadowRoot: N, public cmd: RenderBeginComponentCmd) {} + constructor(public hostElement: N, public shadowRoot: N, public cmd: RenderBeginComponentCmd, + public isRoot: boolean) {} addContentNode(ngContentIndex: number, node: N, context: BuildContext) { if (isBlank(ngContentIndex)) { if (this.cmd.nativeShadow) { diff --git a/modules/angular2/test/core/render/view_factory_spec.ts b/modules/angular2/test/core/render/view_factory_spec.ts index 3a7ac77cda..ab55a1b09f 100644 --- a/modules/angular2/test/core/render/view_factory_spec.ts +++ b/modules/angular2/test/core/render/view_factory_spec.ts @@ -517,6 +517,16 @@ export function main() { expect(stringifyFragment(view.fragments[0].nodes)) .toEqual('(hello)'); }); + + + it('should store content injection points for root component in a view', () => { + componentTemplates.set(0, [ngContent(0, null)]); + var view = + createRenderView([beginComponent('a-comp', [], [], false, null, 0), endComponent()], + DOM.createElement('root'), nodeFactory); + expect(stringifyFragment(view.rootContentInsertionPoints)) + .toEqual(''); + }); }); }); } @@ -571,6 +581,9 @@ class DomNodeFactory implements NodeFactory { return root; } createText(value: string): Node { return DOM.createTextNode(isPresent(value) ? value : ''); } + createRootContentInsertionPoint(): Node { + return DOM.createElement('root-content-insertion-point'); + } appendChild(parent: Node, child: Node) { DOM.appendChild(parent, child); } on(element: Node, eventName: string, callback: Function) { this._localEventListeners.push(new LocalEventListener(element, eventName, callback)); diff --git a/modules/angular2/test/core/render/view_spec.ts b/modules/angular2/test/core/render/view_spec.ts index ac8909a33b..d31b01f8bb 100644 --- a/modules/angular2/test/core/render/view_spec.ts +++ b/modules/angular2/test/core/render/view_spec.ts @@ -18,7 +18,7 @@ export function main() { it('should register global event listeners', () => { var addCount = 0; var adder = () => { addCount++ }; - var view = new DefaultRenderView([], [], [], [], [adder]); + var view = new DefaultRenderView([], [], [], [], [adder], []); view.hydrate(); expect(addCount).toBe(1); }); @@ -28,7 +28,7 @@ export function main() { it('should deregister global event listeners', () => { var removeCount = 0; var adder = () => () => { removeCount++ }; - var view = new DefaultRenderView([], [], [], [], [adder]); + var view = new DefaultRenderView([], [], [], [], [adder], []); view.hydrate(); view.dehydrate(); expect(removeCount).toBe(1); diff --git a/modules/upgrade/src/angular.d.ts b/modules/upgrade/src/angular.d.ts index 3aac7529e2..03b7921843 100644 --- a/modules/upgrade/src/angular.d.ts +++ b/modules/upgrade/src/angular.d.ts @@ -25,7 +25,7 @@ declare namespace angular { require?: string; restrict?: string; scope?: {[key: string]: string}; - link?: Function; + link?: {pre?: Function, post?: Function}; } interface IAttributes { $observe(attr: string, fn: (v: string) => void); @@ -33,6 +33,10 @@ declare namespace angular { interface ITranscludeFunction {} interface IAugmentedJQuery { bind(name: string, fn: () => void); + data(name: string, value?: any); + contents(): IAugmentedJQuery; + length: number; + [index: number]: Node; } interface IParseService { (expression: string): ICompiledExpression; @@ -40,7 +44,7 @@ declare namespace angular { interface ICompiledExpression { assign(context: any, value: any): any; } - function element(e: Element); + function element(e: Element): IAugmentedJQuery; function bootstrap(e: Element, modules: IModule[], config: IAngularBootstrapConfig); namespace auto { diff --git a/modules/upgrade/src/ng2_facade.ts b/modules/upgrade/src/ng2_facade.ts index 63baa0dcdd..c3cd6f3e09 100644 --- a/modules/upgrade/src/ng2_facade.ts +++ b/modules/upgrade/src/ng2_facade.ts @@ -5,10 +5,13 @@ import { HostViewRef, Injector, ProtoViewRef, - SimpleChange + SimpleChange, + ViewRef } from 'angular2/angular2'; import {NG1_SCOPE} from './constants'; import {ComponentInfo} from './metadata'; +import {ViewRef_} from "../../angular2/src/core/linker/view_ref"; +import Element = protractor.Element; const INITIAL_VALUE = { __UNINITIALIZED__: true @@ -21,13 +24,17 @@ export class Ng2ComponentFacade { hostViewRef: HostViewRef = null; changeDetector: ChangeDetectorRef = null; componentScope: angular.IScope; + childNodes: Node[]; + contentInserctionPoint: Node = null; 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.element[0]).id = id; this.componentScope = scope.$new(); + this.childNodes = element.contents(); } bootstrapNg2() { @@ -35,9 +42,11 @@ export class Ng2ComponentFacade { this.parentInjector.resolveAndCreateChild([bind(NG1_SCOPE).toValue(this.componentScope)]); this.hostViewRef = this.viewManager.createRootHostView(this.protoView, '#' + this.id, childInjector); + var renderer: any = (this.hostViewRef).render; var hostElement = this.viewManager.getHostElement(this.hostViewRef); this.changeDetector = this.hostViewRef.changeDetectorRef; this.component = this.viewManager.getComponent(hostElement); + this.contentInserctionPoint = renderer.rootContentInsertionPoints[0]; } setupInputs() { @@ -94,6 +103,16 @@ export class Ng2ComponentFacade { this.componentScope.$watch(() => this.changeDetector.detectChanges()); } + projectContent() { + var childNodes = this.childNodes; + if (this.contentInserctionPoint) { + var parent = this.contentInserctionPoint.parentNode; + for (var i = 0, ii = childNodes.length; i < ii; i++) { + parent.insertBefore(childNodes[i], this.contentInserctionPoint); + } + } + } + setupOutputs() { var attrs = this.attrs; var outputs = this.info.outputs; diff --git a/modules/upgrade/src/upgrade_module.ts b/modules/upgrade/src/upgrade_module.ts index 3f9abb433f..24423e370b 100644 --- a/modules/upgrade/src/upgrade_module.ts +++ b/modules/upgrade/src/upgrade_module.ts @@ -169,16 +169,19 @@ function ng1ComponentDirective(info: ComponentInfo, idPrefix: string): Function return { restrict: 'E', require: REQUIRE_INJECTOR, - link: (scope: angular.IScope, element: angular.IAugmentedJQuery, attrs: angular.IAttributes, - parentInjector: any, transclude: angular.ITranscludeFunction): void => { - var facade = - new Ng2ComponentFacade(element[0].id = idPrefix + (idCount++), info, element, attrs, - scope, parentInjector, parse, viewManager, protoView); - - facade.setupInputs(); - facade.bootstrapNg2(); - facade.setupOutputs(); - facade.registerCleanup(); + link: { + post: (scope: angular.IScope, element: angular.IAugmentedJQuery, attrs: angular.IAttributes, + parentInjector: any, transclude: angular.ITranscludeFunction): void => { + var domElement = element[0]; + var facade = + new Ng2ComponentFacade(idPrefix + (idCount++), info, element, attrs, scope, + parentInjector, parse, viewManager, protoView); + facade.setupInputs(); + facade.bootstrapNg2(); + facade.projectContent(); + facade.setupOutputs(); + facade.registerCleanup(); + } } }; } diff --git a/modules/upgrade/test/integration_spec.ts b/modules/upgrade/test/integration_spec.ts index d6c4d04d4c..2d204359d1 100644 --- a/modules/upgrade/test/integration_spec.ts +++ b/modules/upgrade/test/integration_spec.ts @@ -18,17 +18,18 @@ export function main() { describe('upgrade: ng1 to ng2', () => { it('should have angular 1 loaded', () => expect(angular.version.major).toBe(1)); - it('should instantiate ng2 in ng1 template', inject([AsyncTestCompleter], (async) => { + it('should instantiate ng2 in ng1 template and project content', + inject([AsyncTestCompleter], (async) => { var Ng2 = Component({selector: 'ng2'}) - .View({template: `{{ 'NG2' }}`}) + .View({template: `{{ 'NG2' }}()`}) .Class({constructor: function() {}}); - var element = html("
{{ 'ng1-' }}~~{{ '-ng1' }}
"); + var element = html("
{{ 'ng1[' }}~{{ 'ng-content' }}~{{ ']' }}
"); var upgradeModule: UpgradeModule = createUpgradeModule(); upgradeModule.importNg2Component(Ng2); upgradeModule.bootstrap(element).ready(() => { - expect(document.body.textContent).toEqual("ng1-NG2-ng1"); + expect(document.body.textContent).toEqual("ng1[NG2(~ng-content~)]"); async.done(); }); }));