From 078475a08223d99d5d206ab89987afe4b2e10b88 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Mon, 20 Jul 2015 09:59:44 -0700 Subject: [PATCH] refactor(compiler): speed up proto view merging - Don't create intermediate merge results - Only merge embedded ProtoViews that contain `` tags Closes #3150 Closes #3177 --- .../angular2/src/core/compiler/compiler.ts | 137 ++++---- .../src/core/compiler/proto_view_factory.ts | 9 +- modules/angular2/src/core/compiler/view.ts | 14 +- .../src/core/compiler/view_manager.ts | 19 +- .../src/core/compiler/view_manager_utils.ts | 16 +- modules/angular2/src/render/api.ts | 16 +- .../src/render/dom/compiler/compiler.ts | 3 +- .../dom/shadow_dom/shadow_dom_compile_step.ts | 8 +- .../src/render/dom/view/proto_view.ts | 27 +- .../src/render/dom/view/proto_view_builder.ts | 13 +- .../src/render/dom/view/proto_view_merger.ts | 318 ++++++++---------- .../test/core/compiler/compiler_spec.ts | 115 +++---- .../core/compiler/proto_view_factory_spec.ts | 3 +- .../core/compiler/view_manager_utils_spec.ts | 29 +- .../test/core/compiler/view_pool_spec.ts | 4 +- .../dom/dom_renderer_integration_spec.ts | 20 +- .../angular2/test/render/dom/dom_testbed.ts | 5 +- .../proto_view_merger_integration_spec.ts | 11 +- .../render/dom/view/proto_view_merger_spec.ts | 56 --- .../test/render/dom/view/view_spec.ts | 2 +- 20 files changed, 359 insertions(+), 466 deletions(-) delete mode 100644 modules/angular2/test/render/dom/view/proto_view_merger_spec.ts diff --git a/modules/angular2/src/core/compiler/compiler.ts b/modules/angular2/src/core/compiler/compiler.ts index c03e0a9c31..ba7e4ef83e 100644 --- a/modules/angular2/src/core/compiler/compiler.ts +++ b/modules/angular2/src/core/compiler/compiler.ts @@ -24,6 +24,7 @@ import {ComponentUrlMapper} from './component_url_mapper'; import {ProtoViewFactory} from './proto_view_factory'; import {UrlResolver} from 'angular2/src/services/url_resolver'; import {AppRootUrl} from 'angular2/src/services/app_root_url'; +import {ElementBinder} from './element_binder'; import * as renderApi from 'angular2/src/render/api'; @@ -90,7 +91,7 @@ export class Compiler { private _appUrl: string; private _render: renderApi.RenderCompiler; private _protoViewFactory: ProtoViewFactory; - private _unmergedCyclicEmbeddedProtoViews: RecursiveEmbeddedProtoView[] = []; + private _protoViewsToBeMerged: AppProtoView[] = []; /** * @private @@ -146,8 +147,37 @@ export class Compiler { return this._compileNestedProtoViews(hostRenderPv, protoView, componentType); }); } - return hostPvPromise.then( - hostAppProtoView => this._mergeCyclicEmbeddedProtoViews().then(_ => hostAppProtoView.ref)); + return hostPvPromise.then(hostAppProtoView => + this._mergeUnmergedProtoViews().then(_ => hostAppProtoView.ref)); + } + + private _mergeUnmergedProtoViews(): Promise { + var protoViewsToBeMerged = this._protoViewsToBeMerged; + this._protoViewsToBeMerged = []; + return PromiseWrapper.all(protoViewsToBeMerged.map((appProtoView) => { + return this._render.mergeProtoViewsRecursively( + this._collectMergeRenderProtoViews(appProtoView)) + .then((mergeResult: renderApi.RenderProtoViewMergeMapping) => { + appProtoView.mergeMapping = new AppProtoViewMergeMapping(mergeResult); + }); + })); + } + + private _collectMergeRenderProtoViews( + appProtoView: AppProtoView): List> { + var result = [appProtoView.render]; + for (var i = 0; i < appProtoView.elementBinders.length; i++) { + var binder = appProtoView.elementBinders[i]; + if (isPresent(binder.nestedProtoView)) { + if (binder.hasStaticComponent() || + (binder.hasEmbeddedProtoView() && binder.nestedProtoView.isEmbeddedFragment)) { + result.push(this._collectMergeRenderProtoViews(binder.nestedProtoView)); + } else { + result.push(null); + } + } + } + return result; } private _compile(componentBinding: DirectiveBinding): Promise| AppProtoView { @@ -207,7 +237,7 @@ export class Compiler { appProtoView: AppProtoView, componentType: Type): Promise { var nestedPVPromises = []; - this._loopComponentElementBinders(appProtoView, (parentPv, elementBinder) => { + this._loopComponentElementBinders(appProtoView, (parentPv, elementBinder: ElementBinder) => { var nestedComponent = elementBinder.componentDirective; var elementBinderDone = (nestedPv: AppProtoView) => { elementBinder.nestedProtoView = nestedPv; }; @@ -220,31 +250,40 @@ export class Compiler { }); return PromiseWrapper.all(nestedPVPromises) .then((_) => { - var appProtoViewsToMergeInto = []; - var mergeRenderProtoViews = this._collectMergeRenderProtoViewsRecurse( - renderProtoView, appProtoView, appProtoViewsToMergeInto); - if (isBlank(mergeRenderProtoViews)) { - throw new BaseException(`Unconditional component cycle in ${stringify(componentType)}`); - } - return this._mergeProtoViews(appProtoViewsToMergeInto, mergeRenderProtoViews); + this._collectMergableProtoViews(appProtoView, componentType); + return appProtoView; }); } - private _mergeProtoViews( - appProtoViewsToMergeInto: AppProtoView[], - mergeRenderProtoViews: - List>): Promise { - return this._render.mergeProtoViewsRecursively(mergeRenderProtoViews) - .then((mergeResults: List) => { - // Note: We don't need to check for nulls here as we filtered them out before! - // (in RenderCompiler.mergeProtoViewsRecursively and - // _collectMergeRenderProtoViewsRecurse). - for (var i = 0; i < mergeResults.length; i++) { - appProtoViewsToMergeInto[i].mergeMapping = - new AppProtoViewMergeMapping(mergeResults[i]); - } - return appProtoViewsToMergeInto[0]; - }); + private _collectMergableProtoViews(appProtoView: AppProtoView, componentType: Type) { + var isRecursive = false; + for (var i = 0; i < appProtoView.elementBinders.length; i++) { + var binder = appProtoView.elementBinders[i]; + if (binder.hasStaticComponent()) { + if (isBlank(binder.nestedProtoView.isRecursive)) { + // cycle via a component. We are in the tail recursion, + // so all components should have their isRecursive flag set already. + isRecursive = true; + break; + } + } else if (binder.hasEmbeddedProtoView()) { + this._collectMergableProtoViews(binder.nestedProtoView, componentType); + } + } + if (isRecursive) { + if (appProtoView.isEmbeddedFragment) { + throw new BaseException( + ` is used within the recursive path of ${stringify(componentType)}`); + } + if (appProtoView.type === renderApi.ViewType.COMPONENT) { + throw new BaseException(`Unconditional component cycle in ${stringify(componentType)}`); + } + } + if (appProtoView.type === renderApi.ViewType.EMBEDDED || + appProtoView.type === renderApi.ViewType.HOST) { + this._protoViewsToBeMerged.push(appProtoView); + } + appProtoView.isRecursive = isRecursive; } private _loopComponentElementBinders(appProtoView: AppProtoView, callback: Function) { @@ -257,48 +296,6 @@ export class Compiler { }); } - private _collectMergeRenderProtoViewsRecurse( - renderProtoView: renderApi.ProtoViewDto, appProtoView: AppProtoView, - targetAppProtoViews: AppProtoView[]): List> { - targetAppProtoViews.push(appProtoView); - var result = [renderProtoView.render]; - for (var i = 0; i < appProtoView.elementBinders.length; i++) { - var binder = appProtoView.elementBinders[i]; - if (binder.hasStaticComponent()) { - if (isBlank(binder.nestedProtoView.mergeMapping)) { - // cycle via an embedded ProtoView. store the AppProtoView and ProtoViewDto for later. - this._unmergedCyclicEmbeddedProtoViews.push( - new RecursiveEmbeddedProtoView(appProtoView, renderProtoView)); - return null; - } - result.push(binder.nestedProtoView.mergeMapping.renderProtoViewRef); - } else if (binder.hasEmbeddedProtoView()) { - result.push(this._collectMergeRenderProtoViewsRecurse( - renderProtoView.elementBinders[i].nestedProtoView, binder.nestedProtoView, - targetAppProtoViews)); - } - } - return result; - } - - private _mergeCyclicEmbeddedProtoViews() { - var pvs = this._unmergedCyclicEmbeddedProtoViews; - this._unmergedCyclicEmbeddedProtoViews = []; - var promises = pvs.map(entry => { - var appProtoView = entry.appProtoView; - var mergeRenderProtoViews = [entry.renderProtoView.render]; - appProtoView.elementBinders.forEach((binder) => { - if (binder.hasStaticComponent()) { - mergeRenderProtoViews.push(binder.nestedProtoView.mergeMapping.renderProtoViewRef); - } else if (binder.hasEmbeddedProtoView()) { - mergeRenderProtoViews.push(null); - } - }); - return this._mergeProtoViews([appProtoView], mergeRenderProtoViews); - }); - return PromiseWrapper.all(promises); - } - private _buildRenderTemplate(component, view, directives): renderApi.ViewDefinition { var componentUrl = this._urlResolver.resolve(this._appUrl, this._componentUrlMapper.getUrl(component)); @@ -356,7 +353,3 @@ export class Compiler { } } } - -class RecursiveEmbeddedProtoView { - constructor(public appProtoView: AppProtoView, public renderProtoView: renderApi.ProtoViewDto) {} -} diff --git a/modules/angular2/src/core/compiler/proto_view_factory.ts b/modules/angular2/src/core/compiler/proto_view_factory.ts index 974295a3dc..abb3d81edf 100644 --- a/modules/angular2/src/core/compiler/proto_view_factory.ts +++ b/modules/angular2/src/core/compiler/proto_view_factory.ts @@ -254,9 +254,12 @@ function _createAppProtoView( renderProtoView: renderApi.ProtoViewDto, protoChangeDetector: ProtoChangeDetector, variableBindings: Map, allDirectives: List): AppProtoView { var elementBinders = renderProtoView.elementBinders; - var protoView = new AppProtoView(renderProtoView.type, protoChangeDetector, variableBindings, - createVariableLocations(elementBinders), - renderProtoView.textBindings.length); + // Embedded ProtoViews that contain `` will be merged into their parents and use + // a RenderFragmentRef. I.e. renderProtoView.transitiveNgContentCount > 0. + var protoView = new AppProtoView( + renderProtoView.type, renderProtoView.transitiveNgContentCount > 0, renderProtoView.render, + protoChangeDetector, variableBindings, createVariableLocations(elementBinders), + renderProtoView.textBindings.length); _createElementBinders(protoView, elementBinders, allDirectives); _bindDirectiveEvents(protoView, elementBinders); diff --git a/modules/angular2/src/core/compiler/view.ts b/modules/angular2/src/core/compiler/view.ts index a07c606e93..6985cef2fe 100644 --- a/modules/angular2/src/core/compiler/view.ts +++ b/modules/angular2/src/core/compiler/view.ts @@ -32,6 +32,7 @@ export class AppProtoViewMergeMapping { renderTextIndices: number[]; nestedViewIndicesByElementIndex: number[]; hostElementIndicesByViewIndex: number[]; + nestedViewCountByViewIndex: number[]; constructor(renderProtoViewMergeMapping: renderApi.RenderProtoViewMergeMapping) { this.renderProtoViewRef = renderProtoViewMergeMapping.mergedProtoViewRef; this.renderFragmentCount = renderProtoViewMergeMapping.fragmentCount; @@ -42,11 +43,8 @@ export class AppProtoViewMergeMapping { this.hostElementIndicesByViewIndex = renderProtoViewMergeMapping.hostElementIndicesByViewIndex; this.nestedViewIndicesByElementIndex = inverseIndexMapping(this.hostElementIndicesByViewIndex, this.renderElementIndices.length); + this.nestedViewCountByViewIndex = renderProtoViewMergeMapping.nestedViewCountByViewIndex; } - - get viewCount() { return this.hostElementIndicesByViewIndex.length; } - - get elementCount() { return this.renderElementIndices.length; } } function inverseIndexMapping(input: number[], resultLength: number): number[] { @@ -215,7 +213,7 @@ export class AppView implements ChangeDispatcher, RenderEventDispatcher { dispatchRenderEvent(renderElementIndex: number, eventName: string, locals: Map): boolean { var elementRef = - this.elementRefs[this.proto.mergeMapping.renderInverseElementIndices[renderElementIndex]]; + this.elementRefs[this.mainMergeMapping.renderInverseElementIndices[renderElementIndex]]; var view = internalView(elementRef.parentView); return view.dispatchEvent(elementRef.boundElementIndex, eventName, locals); } @@ -258,7 +256,11 @@ export class AppProtoView { mergeMapping: AppProtoViewMergeMapping; ref: ProtoViewRef; - constructor(public type: renderApi.ViewType, public protoChangeDetector: ProtoChangeDetector, + isRecursive: boolean = null; + + constructor(public type: renderApi.ViewType, public isEmbeddedFragment: boolean, + public render: renderApi.RenderProtoViewRef, + public protoChangeDetector: ProtoChangeDetector, public variableBindings: Map, public variableLocations: Map, public textBindingCount: number) { this.ref = new ProtoViewRef(this); diff --git a/modules/angular2/src/core/compiler/view_manager.ts b/modules/angular2/src/core/compiler/view_manager.ts index 82d908f124..aeea9d356a 100644 --- a/modules/angular2/src/core/compiler/view_manager.ts +++ b/modules/angular2/src/core/compiler/view_manager.ts @@ -339,12 +339,19 @@ export class AppViewManager { this._utils.dehydrateView(view); } var viewContainers = view.viewContainers; - for (var i = view.elementOffset, ii = view.elementOffset + view.proto.mergeMapping.elementCount; - i < ii; i++) { - var vc = viewContainers[i]; - if (isPresent(vc)) { - for (var j = vc.views.length - 1; j >= 0; j--) { - this._destroyViewInContainer(view, i, j); + var startViewOffset = view.viewOffset; + var endViewOffset = + view.viewOffset + view.mainMergeMapping.nestedViewCountByViewIndex[view.viewOffset]; + var elementOffset = view.elementOffset; + for (var viewIdx = startViewOffset; viewIdx <= endViewOffset; viewIdx++) { + var currView = view.views[viewIdx]; + for (var binderIdx = 0; binderIdx < currView.proto.elementBinders.length; + binderIdx++, elementOffset++) { + var vc = viewContainers[elementOffset]; + if (isPresent(vc)) { + for (var j = vc.views.length - 1; j >= 0; j--) { + this._destroyViewInContainer(currView, elementOffset, j); + } } } } diff --git a/modules/angular2/src/core/compiler/view_manager_utils.ts b/modules/angular2/src/core/compiler/view_manager_utils.ts index 6dce0f3f6b..083b4e1baf 100644 --- a/modules/angular2/src/core/compiler/view_manager_utils.ts +++ b/modules/angular2/src/core/compiler/view_manager_utils.ts @@ -26,8 +26,8 @@ export class AppViewManagerUtils { var renderFragments = renderViewWithFragments.fragmentRefs; var renderView = renderViewWithFragments.viewRef; - var elementCount = mergedParentViewProto.mergeMapping.elementCount; - var viewCount = mergedParentViewProto.mergeMapping.viewCount; + var elementCount = mergedParentViewProto.mergeMapping.renderElementIndices.length; + var viewCount = mergedParentViewProto.mergeMapping.nestedViewCountByViewIndex[0] + 1; var elementRefs: ElementRef[] = ListWrapper.createFixedSize(elementCount); var viewContainers = ListWrapper.createFixedSize(elementCount); var preBuiltObjects: eli.PreBuiltObjects[] = ListWrapper.createFixedSize(elementCount); @@ -175,13 +175,13 @@ export class AppViewManagerUtils { _hydrateView(initView: viewModule.AppView, imperativelyCreatedInjector: Injector, hostElementInjector: eli.ElementInjector, context: Object, parentLocals: Locals) { var viewIdx = initView.viewOffset; - var endViewOffset = viewIdx + initView.proto.mergeMapping.viewCount; - while (viewIdx < endViewOffset) { + var endViewOffset = viewIdx + initView.mainMergeMapping.nestedViewCountByViewIndex[viewIdx]; + while (viewIdx <= endViewOffset) { var currView = initView.views[viewIdx]; var currProtoView = currView.proto; if (currView !== initView && currView.proto.type === ViewType.EMBEDDED) { // Don't hydrate components of embedded fragment views. - viewIdx += currProtoView.mergeMapping.viewCount; + viewIdx += initView.mainMergeMapping.nestedViewCountByViewIndex[viewIdx] + 1; } else { if (currView !== initView) { // hydrate a nested component view @@ -263,9 +263,9 @@ export class AppViewManagerUtils { } dehydrateView(initView: viewModule.AppView) { - for (var viewIdx = initView.viewOffset, - endViewOffset = viewIdx + initView.proto.mergeMapping.viewCount; - viewIdx < endViewOffset; viewIdx++) { + var endViewOffset = initView.viewOffset + + initView.mainMergeMapping.nestedViewCountByViewIndex[initView.viewOffset]; + for (var viewIdx = initView.viewOffset; viewIdx <= endViewOffset; viewIdx++) { var currView = initView.views[viewIdx]; if (currView.hydrated()) { if (isPresent(currView.locals)) { diff --git a/modules/angular2/src/render/api.ts b/modules/angular2/src/render/api.ts index 1ca61a26d7..fd12c6732a 100644 --- a/modules/angular2/src/render/api.ts +++ b/modules/angular2/src/render/api.ts @@ -114,19 +114,23 @@ export class ProtoViewDto { variableBindings: Map; type: ViewType; textBindings: List; + transitiveNgContentCount: number; - constructor({render, elementBinders, variableBindings, type, textBindings}: { + constructor({render, elementBinders, variableBindings, type, textBindings, + transitiveNgContentCount}: { render?: RenderProtoViewRef, elementBinders?: List, variableBindings?: Map, type?: ViewType, - textBindings?: List + textBindings?: List, + transitiveNgContentCount?: number }) { this.render = render; this.elementBinders = elementBinders; this.variableBindings = variableBindings; this.type = type; this.textBindings = textBindings; + this.transitiveNgContentCount = transitiveNgContentCount; } } @@ -308,7 +312,9 @@ export class RenderProtoViewMergeMapping { // indices for one ProtoView in a consecuitve block. public mappedTextIndices: number[], // Mapping from view index to app element index - public hostElementIndicesByViewIndex: number[]) {} + public hostElementIndicesByViewIndex: number[], + // Number of contained views by view index + public nestedViewCountByViewIndex: number[]) {} } export class RenderCompiler { @@ -331,10 +337,10 @@ export class RenderCompiler { * If the array contains other arrays, they will be merged before processing the parent array. * The array must contain an entry for every component and embedded ProtoView of the first entry. * @param protoViewRefs List of ProtoViewRefs or nested - * @return the merge result for every input array in depth first order. + * @return the merge result */ mergeProtoViewsRecursively( - protoViewRefs: List>): Promise { + protoViewRefs: List>): Promise { return null; } } diff --git a/modules/angular2/src/render/dom/compiler/compiler.ts b/modules/angular2/src/render/dom/compiler/compiler.ts index b2f04a3573..4927cb8955 100644 --- a/modules/angular2/src/render/dom/compiler/compiler.ts +++ b/modules/angular2/src/render/dom/compiler/compiler.ts @@ -54,8 +54,7 @@ export class DomCompiler extends RenderCompiler { } mergeProtoViewsRecursively( - protoViewRefs: - List>): Promise> { + protoViewRefs: List>): Promise { return PromiseWrapper.resolve(pvm.mergeProtoViewsRecursively(protoViewRefs)); } diff --git a/modules/angular2/src/render/dom/shadow_dom/shadow_dom_compile_step.ts b/modules/angular2/src/render/dom/shadow_dom/shadow_dom_compile_step.ts index 75df4795f8..8c393ec370 100644 --- a/modules/angular2/src/render/dom/shadow_dom/shadow_dom_compile_step.ts +++ b/modules/angular2/src/render/dom/shadow_dom/shadow_dom_compile_step.ts @@ -1,17 +1,17 @@ -import {DOM} from 'angular2/src/dom/dom_adapter'; - import {CompileStep} from '../compiler/compile_step'; import {CompileElement} from '../compiler/compile_element'; import {CompileControl} from '../compiler/compile_control'; import {ViewDefinition} from '../../api'; import {ShadowDomStrategy} from './shadow_dom_strategy'; +import {NG_CONTENT_ELEMENT_NAME, isElementWithTag} from '../util'; export class ShadowDomCompileStep implements CompileStep { constructor(public _shadowDomStrategy: ShadowDomStrategy, public _view: ViewDefinition) {} process(parent: CompileElement, current: CompileElement, control: CompileControl) { - var tagName = DOM.tagName(current.element).toUpperCase(); - if (tagName == 'STYLE') { + if (isElementWithTag(current.element, NG_CONTENT_ELEMENT_NAME)) { + current.inheritedProtoView.bindNgContent(); + } else if (isElementWithTag(current.element, 'style')) { this._processStyleElement(current, control); } else { var componentId = current.isBound() ? current.inheritedElementBinder.componentId : null; diff --git a/modules/angular2/src/render/dom/view/proto_view.ts b/modules/angular2/src/render/dom/view/proto_view.ts index ee78a5a5fe..81a4dd7865 100644 --- a/modules/angular2/src/render/dom/view/proto_view.ts +++ b/modules/angular2/src/render/dom/view/proto_view.ts @@ -1,4 +1,3 @@ -import {isBlank} from 'angular2/src/facade/lang'; import {List, ListWrapper} from 'angular2/src/facade/collection'; import {DomElementBinder} from './element_binder'; @@ -16,39 +15,21 @@ export class DomProtoViewRef extends RenderProtoViewRef { export class DomProtoView { static create(type: ViewType, rootElement: Element, fragmentsRootNodeCount: number[], - rootTextNodeIndices: number[], elementBinders: List, - mappedElementIndices: number[], mappedTextIndices: number[], - hostElementIndicesByViewIndex: number[]): DomProtoView { + rootTextNodeIndices: number[], + elementBinders: List): DomProtoView { var boundTextNodeCount = rootTextNodeIndices.length; for (var i = 0; i < elementBinders.length; i++) { boundTextNodeCount += elementBinders[i].textNodeIndices.length; } - if (isBlank(mappedElementIndices)) { - mappedElementIndices = ListWrapper.createFixedSize(elementBinders.length); - for (var i = 0; i < mappedElementIndices.length; i++) { - mappedElementIndices[i] = i; - } - } - if (isBlank(mappedTextIndices)) { - mappedTextIndices = ListWrapper.createFixedSize(boundTextNodeCount); - for (var i = 0; i < mappedTextIndices.length; i++) { - mappedTextIndices[i] = i; - } - } - if (isBlank(hostElementIndicesByViewIndex)) { - hostElementIndicesByViewIndex = [null]; - } var isSingleElementFragment = fragmentsRootNodeCount.length === 1 && fragmentsRootNodeCount[0] === 1 && DOM.isElementNode(DOM.firstChild(DOM.content(rootElement))); return new DomProtoView(type, rootElement, elementBinders, rootTextNodeIndices, - boundTextNodeCount, fragmentsRootNodeCount, isSingleElementFragment, - mappedElementIndices, mappedTextIndices, hostElementIndicesByViewIndex); + boundTextNodeCount, fragmentsRootNodeCount, isSingleElementFragment); } constructor(public type: ViewType, public rootElement: Element, public elementBinders: List, public rootTextNodeIndices: number[], public boundTextNodeCount: number, public fragmentsRootNodeCount: number[], - public isSingleElementFragment: boolean, public mappedElementIndices: number[], - public mappedTextIndices: number[], public hostElementIndicesByViewIndex: number[]) {} + public isSingleElementFragment: boolean) {} } diff --git a/modules/angular2/src/render/dom/view/proto_view_builder.ts b/modules/angular2/src/render/dom/view/proto_view_builder.ts index 0720f34f82..4208b8094c 100644 --- a/modules/angular2/src/render/dom/view/proto_view_builder.ts +++ b/modules/angular2/src/render/dom/view/proto_view_builder.ts @@ -29,6 +29,7 @@ export class ProtoViewBuilder { variableBindings: Map = new Map(); elements: List = []; rootTextBindings: Map = new Map(); + ngContentCount: number = 0; constructor(public rootElement, public type: api.ViewType, public useNativeShadowDom: boolean = false) {} @@ -57,12 +58,15 @@ export class ProtoViewBuilder { this.rootTextBindings.set(textNode, expression); } + bindNgContent() { this.ngContentCount++; } + build(): api.ProtoViewDto { var domElementBinders = []; var apiElementBinders = []; var textNodeExpressions = []; var rootTextNodeIndices = []; + var transitiveNgContentCount = this.ngContentCount; queryBoundTextNodeIndices(DOM.content(this.rootElement), this.rootTextBindings, (node, nodeIndex, expression) => { textNodeExpressions.push(expression); @@ -85,6 +89,9 @@ export class ProtoViewBuilder { }); }); var nestedProtoView = isPresent(ebb.nestedProtoView) ? ebb.nestedProtoView.build() : null; + if (isPresent(nestedProtoView)) { + transitiveNgContentCount += nestedProtoView.transitiveNgContentCount; + } var parentIndex = isPresent(ebb.parent) ? ebb.parent.index : -1; var textNodeIndices = []; queryBoundTextNodeIndices(ebb.element, ebb.textBindings, (node, nodeIndex, expression) => { @@ -116,12 +123,12 @@ export class ProtoViewBuilder { var rootNodeCount = DOM.childNodes(DOM.content(this.rootElement)).length; return new api.ProtoViewDto({ render: new DomProtoViewRef(DomProtoView.create(this.type, this.rootElement, [rootNodeCount], - rootTextNodeIndices, domElementBinders, null, - null, null)), + rootTextNodeIndices, domElementBinders)), type: this.type, elementBinders: apiElementBinders, variableBindings: this.variableBindings, - textBindings: textNodeExpressions + textBindings: textNodeExpressions, + transitiveNgContentCount: transitiveNgContentCount }); } } diff --git a/modules/angular2/src/render/dom/view/proto_view_merger.ts b/modules/angular2/src/render/dom/view/proto_view_merger.ts index 890d51b729..abe275a64d 100644 --- a/modules/angular2/src/render/dom/view/proto_view_merger.ts +++ b/modules/angular2/src/render/dom/view/proto_view_merger.ts @@ -1,5 +1,6 @@ import {DOM} from 'angular2/src/dom/dom_adapter'; import {isPresent, isBlank, BaseException, isArray} from 'angular2/src/facade/lang'; +import {ListWrapper} from 'angular2/src/facade/collection'; import {DomProtoView, DomProtoViewRef, resolveInternalDomProtoView} from './proto_view'; import {DomElementBinder} from './element_binder'; @@ -11,71 +12,25 @@ import { cloneAndQueryProtoView, queryBoundElements, queryBoundTextNodeIndices, - NG_SHADOW_ROOT_ELEMENT_NAME, - isElementWithTag + NG_SHADOW_ROOT_ELEMENT_NAME } from '../util'; -import {CssSelector} from '../compiler/selector'; - -const NOT_MATCHABLE_SELECTOR = '_not-matchable_'; export function mergeProtoViewsRecursively(protoViewRefs: List>): - RenderProtoViewMergeMapping[] { - var target = []; - _mergeProtoViewsRecursively(protoViewRefs, target); - return target; -} - -function _mergeProtoViewsRecursively(protoViewRefs: List>, - target: RenderProtoViewMergeMapping[]): RenderProtoViewRef { - var targetIndex = target.length; - target.push(null); - - var resolvedProtoViewRefs = protoViewRefs.map((entry) => { - if (isArray(entry)) { - return _mergeProtoViewsRecursively(>entry, target); - } else { - return entry; - } - }); - var mapping = mergeProtoViews(resolvedProtoViewRefs); - target[targetIndex] = mapping; - return mapping.mergedProtoViewRef; -} - -export function mergeProtoViews(protoViewRefs: RenderProtoViewRef[]): RenderProtoViewMergeMapping { - var hostProtoView = resolveInternalDomProtoView(protoViewRefs[0]); - - var mergeableProtoViews: DomProtoView[] = []; - var hostElementIndices: number[] = []; - - mergeableProtoViews.push(hostProtoView); - var protoViewIdx = 1; - for (var i = 0; i < hostProtoView.elementBinders.length; i++) { - var binder = hostProtoView.elementBinders[i]; - if (binder.hasNestedProtoView) { - var nestedProtoViewRef = protoViewRefs[protoViewIdx++]; - if (isPresent(nestedProtoViewRef)) { - mergeableProtoViews.push(resolveInternalDomProtoView(nestedProtoViewRef)); - hostElementIndices.push(i); - } - } - } - return _mergeProtoViews(mergeableProtoViews, hostElementIndices); -} - - -function _mergeProtoViews(mergeableProtoViews: DomProtoView[], hostElementIndices: number[]): RenderProtoViewMergeMapping { - var clonedProtoViews: ClonedProtoView[] = - mergeableProtoViews.map(domProtoView => cloneAndQueryProtoView(domProtoView, false)); - var hostProtoView: ClonedProtoView = clonedProtoViews[0]; + // clone + var clonedProtoViews = []; + var hostViewAndBinderIndices: number[][] = []; + cloneProtoViews(protoViewRefs, clonedProtoViews, hostViewAndBinderIndices); + var mainProtoView: ClonedProtoView = clonedProtoViews[0]; // modify the DOM - mergeDom(clonedProtoViews, hostElementIndices); + mergeEmbeddedPvsIntoComponentOrRootPv(clonedProtoViews, hostViewAndBinderIndices); + var fragments = []; + mergeComponents(clonedProtoViews, hostViewAndBinderIndices, fragments); // create a new root element with the changed fragments and elements - var rootElement = createRootElementFromFragments(hostProtoView.fragments); - var fragmentsRootNodeCount = hostProtoView.fragments.map(fragment => fragment.length); + var rootElement = createRootElementFromFragments(fragments); + var fragmentsRootNodeCount = fragments.map(fragment => fragment.length); var rootNode = DOM.content(rootElement); // read out the new element / text node / ElementBinder order @@ -90,16 +45,45 @@ function _mergeProtoViews(mergeableProtoViews: DomProtoView[], hostElementIndice // create element / text index mappings var mappedElementIndices = calcMappedElementIndices(clonedProtoViews, mergedBoundElements); var mappedTextIndices = calcMappedTextIndices(clonedProtoViews, mergedBoundTextIndices); - var hostElementIndicesByViewIndex = - calcHostElementIndicesByViewIndex(clonedProtoViews, hostElementIndices); // create result - var mergedProtoView = DomProtoView.create( - hostProtoView.original.type, rootElement, fragmentsRootNodeCount, rootTextNodeIndices, - mergedElementBinders, mappedElementIndices, mappedTextIndices, hostElementIndicesByViewIndex); - return new RenderProtoViewMergeMapping(new DomProtoViewRef(mergedProtoView), - fragmentsRootNodeCount.length, mappedElementIndices, - mappedTextIndices, hostElementIndicesByViewIndex); + var hostElementIndicesByViewIndex = + calcHostElementIndicesByViewIndex(clonedProtoViews, hostViewAndBinderIndices); + var nestedViewCounts = calcNestedViewCounts(hostViewAndBinderIndices); + var mergedProtoView = + DomProtoView.create(mainProtoView.original.type, rootElement, fragmentsRootNodeCount, + rootTextNodeIndices, mergedElementBinders); + return new RenderProtoViewMergeMapping( + new DomProtoViewRef(mergedProtoView), fragmentsRootNodeCount.length, mappedElementIndices, + mappedTextIndices, hostElementIndicesByViewIndex, nestedViewCounts); +} + +function cloneProtoViews(protoViewRefs: List>, + targetClonedProtoViews: ClonedProtoView[], + targetHostViewAndBinderIndices: number[][]) { + var hostProtoView = resolveInternalDomProtoView(protoViewRefs[0]); + var hostPvIdx = targetClonedProtoViews.length; + targetClonedProtoViews.push(cloneAndQueryProtoView(hostProtoView, false)); + if (targetHostViewAndBinderIndices.length === 0) { + targetHostViewAndBinderIndices.push([null, null]); + } + var protoViewIdx = 1; + for (var i = 0; i < hostProtoView.elementBinders.length; i++) { + var binder = hostProtoView.elementBinders[i]; + if (binder.hasNestedProtoView) { + var nestedEntry = protoViewRefs[protoViewIdx++]; + if (isPresent(nestedEntry)) { + targetHostViewAndBinderIndices.push([hostPvIdx, i]); + if (isArray(nestedEntry)) { + cloneProtoViews(nestedEntry, targetClonedProtoViews, + targetHostViewAndBinderIndices); + } else { + targetClonedProtoViews.push( + cloneAndQueryProtoView(resolveInternalDomProtoView(nestedEntry), false)); + } + } + } + } } function indexBoundTextNodes(mergableProtoViews: ClonedProtoView[]): Map { @@ -112,42 +96,56 @@ function indexBoundTextNodes(mergableProtoViews: ClonedProtoView[]): Map = - indexProtoViewsByHostElement(clonedProtoViews, hostElementIndices); - - var hostProtoView = clonedProtoViews[0]; - var mergableProtoViewIdx = 1; - hostElementIndices.forEach((boundElementIndex) => { - var binder = hostProtoView.original.elementBinders[boundElementIndex]; - if (binder.hasNestedProtoView) { - var mergableNestedProtoView: ClonedProtoView = clonedProtoViews[mergableProtoViewIdx++]; - if (mergableNestedProtoView.original.type === ViewType.COMPONENT) { - mergeComponentDom(hostProtoView, boundElementIndex, mergableNestedProtoView, - nestedProtoViewByHostElement); - } else { - mergeEmbeddedDom(hostProtoView, mergableNestedProtoView); - } +function mergeEmbeddedPvsIntoComponentOrRootPv(clonedProtoViews: ClonedProtoView[], + hostViewAndBinderIndices: number[][]) { + var nearestHostComponentOrRootPvIndices = + calcNearestHostComponentOrRootPvIndices(clonedProtoViews, hostViewAndBinderIndices); + for (var viewIdx = 1; viewIdx < clonedProtoViews.length; viewIdx++) { + var clonedProtoView = clonedProtoViews[viewIdx]; + if (clonedProtoView.original.type === ViewType.EMBEDDED) { + var hostComponentIdx = nearestHostComponentOrRootPvIndices[viewIdx]; + var hostPv = clonedProtoViews[hostComponentIdx]; + clonedProtoView.fragments.forEach((fragment) => hostPv.fragments.push(fragment)); } - }); + } } -function indexProtoViewsByHostElement(mergableProtoViews: ClonedProtoView[], - hostElementIndices: number[]): Map { - var hostProtoView = mergableProtoViews[0]; - var mergableProtoViewIdx = 1; - var nestedProtoViewByHostElement: Map = new Map(); - hostElementIndices.forEach((hostElementIndex) => { - nestedProtoViewByHostElement.set(hostProtoView.boundElements[hostElementIndex], - mergableProtoViews[mergableProtoViewIdx++]); - }); - return nestedProtoViewByHostElement; +function calcNearestHostComponentOrRootPvIndices(clonedProtoViews: ClonedProtoView[], + hostViewAndBinderIndices: number[][]): number[] { + var nearestHostComponentOrRootPvIndices = ListWrapper.createFixedSize(clonedProtoViews.length); + nearestHostComponentOrRootPvIndices[0] = null; + for (var viewIdx = 1; viewIdx < hostViewAndBinderIndices.length; viewIdx++) { + var hostViewIdx = hostViewAndBinderIndices[viewIdx][0]; + var hostProtoView = clonedProtoViews[hostViewIdx]; + if (hostViewIdx === 0 || hostProtoView.original.type === ViewType.COMPONENT) { + nearestHostComponentOrRootPvIndices[viewIdx] = hostViewIdx; + } else { + nearestHostComponentOrRootPvIndices[viewIdx] = + nearestHostComponentOrRootPvIndices[hostViewIdx]; + } + } + return nearestHostComponentOrRootPvIndices; } -function mergeComponentDom(hostProtoView: ClonedProtoView, boundElementIndex: number, - nestedProtoView: ClonedProtoView, - nestedProtoViewByHostElement: Map) { - var hostElement = hostProtoView.boundElements[boundElementIndex]; +function mergeComponents(clonedProtoViews: ClonedProtoView[], hostViewAndBinderIndices: number[][], + targetFragments: Node[][]) { + var hostProtoView = clonedProtoViews[0]; + hostProtoView.fragments.forEach((fragment) => targetFragments.push(fragment)); + + for (var viewIdx = 1; viewIdx < clonedProtoViews.length; viewIdx++) { + var hostViewIdx = hostViewAndBinderIndices[viewIdx][0]; + var hostBinderIdx = hostViewAndBinderIndices[viewIdx][1]; + var hostProtoView = clonedProtoViews[hostViewIdx]; + var clonedProtoView = clonedProtoViews[viewIdx]; + if (clonedProtoView.original.type === ViewType.COMPONENT) { + mergeComponent(hostProtoView, hostBinderIdx, clonedProtoView, targetFragments); + } + } +} + +function mergeComponent(hostProtoView: ClonedProtoView, binderIdx: number, + nestedProtoView: ClonedProtoView, targetFragments: Node[][]) { + var hostElement = hostProtoView.boundElements[binderIdx]; // We wrap the fragments into elements so that we can expand // even for root nodes in the fragment without special casing them. @@ -163,15 +161,15 @@ function mergeComponentDom(hostProtoView: ClonedProtoView, boundElementIndex: nu // unwrap the fragment elements into arrays of nodes after projecting var fragments = extractFragmentNodesFromElements(fragmentElements); - appendComponentNodesToHost(hostProtoView, boundElementIndex, fragments[0]); + appendComponentNodesToHost(hostProtoView, binderIdx, fragments[0]); for (var i = 1; i < fragments.length; i++) { - hostProtoView.fragments.push(fragments[i]); + targetFragments.push(fragments[i]); } } function mapFragmentsIntoElements(fragments: Node[][]): Element[] { - return fragments.map((fragment) => { + return fragments.map(fragment => { var fragmentElement = DOM.createTemplate(''); fragment.forEach(node => DOM.appendChild(DOM.content(fragmentElement), node)); return fragmentElement; @@ -195,10 +193,10 @@ function findContentElements(fragmentElements: Element[]): Element[] { return sortContentElements(contentElements); } -function appendComponentNodesToHost(hostProtoView: ClonedProtoView, boundElementIndex: number, +function appendComponentNodesToHost(hostProtoView: ClonedProtoView, binderIdx: number, componentRootNodes: Node[]) { - var hostElement = hostProtoView.boundElements[boundElementIndex]; - var binder = hostProtoView.original.elementBinders[boundElementIndex]; + var hostElement = hostProtoView.boundElements[binderIdx]; + var binder = hostProtoView.original.elementBinders[binderIdx]; if (binder.hasNativeShadowRoot) { var shadowRootWrapper = DOM.createElement(NG_SHADOW_ROOT_ELEMENT_NAME); for (var i = 0; i < componentRootNodes.length; i++) { @@ -218,40 +216,23 @@ function appendComponentNodesToHost(hostProtoView: ClonedProtoView, boundElement } } -function mergeEmbeddedDom(parentProtoView: ClonedProtoView, nestedProtoView: ClonedProtoView) { - nestedProtoView.fragments.forEach((fragment) => parentProtoView.fragments.push(fragment)); -} - function projectMatchingNodes(selector: string, contentElement: Element, nodes: Node[]): Node[] { var remaining = []; - var removeContentElement = true; for (var i = 0; i < nodes.length; i++) { var node = nodes[i]; + var matches = false; if (isWildcard(selector)) { + matches = true; + } else if (DOM.isElementNode(node) && DOM.elementMatches(node, selector)) { + matches = true; + } + if (matches) { DOM.insertBefore(contentElement, node); - } else if (DOM.isElementNode(node)) { - if (isElementWithTag(node, NG_CONTENT_ELEMENT_NAME)) { - // keep the projected content as other elements - // might want to use it as well. - remaining.push(node); - DOM.setAttribute(contentElement, 'select', - mergeSelectors(selector, DOM.getAttribute(node, 'select'))); - removeContentElement = false; - } else { - if (DOM.elementMatches(node, selector)) { - DOM.insertBefore(contentElement, node); - } else { - remaining.push(node); - } - } } else { - // non projected text nodes remaining.push(node); } } - if (removeContentElement) { - DOM.remove(contentElement); - } + DOM.remove(contentElement); return remaining; } @@ -259,36 +240,6 @@ function isWildcard(selector): boolean { return isBlank(selector) || selector.length === 0 || selector == '*'; } -export function mergeSelectors(selector1: string, selector2: string): string { - if (isWildcard(selector1)) { - return isBlank(selector2) ? '' : selector2; - } else if (isWildcard(selector2)) { - return isBlank(selector1) ? '' : selector1; - } else { - var sels1 = CssSelector.parse(selector1); - var sels2 = CssSelector.parse(selector2); - if (sels1.length > 1 || sels2.length > 1) { - throw new BaseException('multiple selectors are not supported in ng-content'); - } - var sel1 = sels1[0]; - var sel2 = sels2[0]; - if (sel1.notSelectors.length > 0 || sel2.notSelectors.length > 0) { - throw new BaseException(':not selector is not supported in ng-content'); - } - var merged = new CssSelector(); - if (isBlank(sel1.element)) { - merged.setElement(sel2.element); - } else if (isBlank(sel2.element)) { - merged.setElement(sel1.element); - } else { - return NOT_MATCHABLE_SELECTOR; - } - merged.attrs = sel1.attrs.concat(sel2.attrs); - merged.classNames = sel1.classNames.concat(sel2.classNames); - return merged.toString(); - } -} - // we need to sort content elements as they can originate from // different sub views function sortContentElements(contentElements: Element[]): Element[] { @@ -394,12 +345,8 @@ function calcMappedElementIndices(clonedProtoViews: ClonedProtoView[], var mergedBoundElementIndices: Map = indexArray(mergedBoundElements); var mappedElementIndices = []; clonedProtoViews.forEach((clonedProtoView) => { - clonedProtoView.original.mappedElementIndices.forEach((boundElementIndex) => { - var mappedElementIndex = null; - if (isPresent(boundElementIndex)) { - var boundElement = clonedProtoView.boundElements[boundElementIndex]; - mappedElementIndex = mergedBoundElementIndices.get(boundElement); - } + clonedProtoView.boundElements.forEach((boundElement) => { + var mappedElementIndex = mergedBoundElementIndices.get(boundElement); mappedElementIndices.push(mappedElementIndex); }); }); @@ -410,12 +357,8 @@ function calcMappedTextIndices(clonedProtoViews: ClonedProtoView[], mergedBoundTextIndices: Map): number[] { var mappedTextIndices = []; clonedProtoViews.forEach((clonedProtoView) => { - clonedProtoView.original.mappedTextIndices.forEach((textNodeIndex) => { - var mappedTextIndex = null; - if (isPresent(textNodeIndex)) { - var textNode = clonedProtoView.boundTextNodes[textNodeIndex]; - mappedTextIndex = mergedBoundTextIndices.get(textNode); - } + clonedProtoView.boundTextNodes.forEach((textNode) => { + var mappedTextIndex = mergedBoundTextIndices.get(textNode); mappedTextIndices.push(mappedTextIndex); }); }); @@ -423,23 +366,30 @@ function calcMappedTextIndices(clonedProtoViews: ClonedProtoView[], } function calcHostElementIndicesByViewIndex(clonedProtoViews: ClonedProtoView[], - hostElementIndices: number[]): number[] { - var mergedElementCount = 0; - var hostElementIndicesByViewIndex = []; - for (var i = 0; i < clonedProtoViews.length; i++) { - var clonedProtoView = clonedProtoViews[i]; - clonedProtoView.original.hostElementIndicesByViewIndex.forEach((hostElementIndex) => { - var mappedHostElementIndex; - if (isBlank(hostElementIndex)) { - mappedHostElementIndex = i > 0 ? hostElementIndices[i - 1] : null; - } else { - mappedHostElementIndex = hostElementIndex + mergedElementCount; - } - hostElementIndicesByViewIndex.push(mappedHostElementIndex); - }); - mergedElementCount += clonedProtoView.original.mappedElementIndices.length; + hostViewAndBinderIndices: number[][]): number[] { + var hostElementIndices = [null]; + var viewElementOffsets = [0]; + var elementIndex = clonedProtoViews[0].original.elementBinders.length; + for (var viewIdx = 1; viewIdx < hostViewAndBinderIndices.length; viewIdx++) { + viewElementOffsets.push(elementIndex); + elementIndex += clonedProtoViews[viewIdx].original.elementBinders.length; + var hostViewIdx = hostViewAndBinderIndices[viewIdx][0]; + var hostBinderIdx = hostViewAndBinderIndices[viewIdx][1]; + hostElementIndices.push(viewElementOffsets[hostViewIdx] + hostBinderIdx); } - return hostElementIndicesByViewIndex; + return hostElementIndices; +} + +function calcNestedViewCounts(hostViewAndBinderIndices: number[][]): number[] { + var nestedViewCounts = ListWrapper.createFixedSize(hostViewAndBinderIndices.length); + ListWrapper.fill(nestedViewCounts, 0); + for (var viewIdx = hostViewAndBinderIndices.length - 1; viewIdx >= 1; viewIdx--) { + var hostViewAndElementIdx = hostViewAndBinderIndices[viewIdx]; + if (isPresent(hostViewAndElementIdx)) { + nestedViewCounts[hostViewAndElementIdx[0]] += nestedViewCounts[viewIdx] + 1; + } + } + return nestedViewCounts; } function indexArray(arr: any[]): Map { diff --git a/modules/angular2/test/core/compiler/compiler_spec.ts b/modules/angular2/test/core/compiler/compiler_spec.ts index ad4af2ef81..44560d668e 100644 --- a/modules/angular2/test/core/compiler/compiler_spec.ts +++ b/modules/angular2/test/core/compiler/compiler_spec.ts @@ -45,35 +45,6 @@ export function main() { rootProtoView; var renderCompileRequests: any[]; - function mergeProtoViewsRecursively( - protoViewRefs: List>, - target: renderApi.RenderProtoViewMergeMapping[]): renderApi.RenderProtoViewRef { - var targetIndex = target.length; - target.push(null); - - var flattended = protoViewRefs.map(protoViewRefOrArray => { - var resolvedProtoViewRef; - if (isArray(protoViewRefOrArray)) { - resolvedProtoViewRef = mergeProtoViewsRecursively( - >protoViewRefOrArray, target); - } else { - resolvedProtoViewRef = protoViewRefOrArray; - } - return resolvedProtoViewRef; - }); - var merged = []; - flattended.forEach((entry) => { - if (entry instanceof MergedRenderProtoViewRef) { - entry.originals.forEach(ref => merged.push(ref)); - } else { - merged.push(entry); - } - }); - var result = new MergedRenderProtoViewRef(merged); - target[targetIndex] = new renderApi.RenderProtoViewMergeMapping(result, 1, [], [], []); - return result; - } - function createCompiler(renderCompileResults: List>, protoViewFactoryResults: List) { @@ -102,10 +73,10 @@ export function main() { }); renderCompiler.spy('mergeProtoViewsRecursively') .andCallFake((protoViewRefs: List>) => { - var result: renderApi.RenderProtoViewMergeMapping[] = []; - mergeProtoViewsRecursively(protoViewRefs, result); - return PromiseWrapper.resolve(result); + return PromiseWrapper.resolve(new renderApi.RenderProtoViewMergeMapping( + new MergedRenderProtoViewRef(protoViewRefs), 1, [], [], [], [null])); }); + // TODO spy on .compile and return RenderProtoViewRef, same for compileHost rootProtoView = createRootProtoView(directiveResolver, MainComponent); }); @@ -360,13 +331,12 @@ export function main() { createCompiler(renderPvDtos, [rootProtoView, mainProtoView, nestedProtoView]); compiler.compileInHost(MainComponent) .then((protoViewRef) => { + expect(originalRenderProtoViewRefs(internalProtoView(protoViewRef))) + .toEqual( + [rootProtoView.render, [mainProtoView.render, [nestedProtoView.render]]]); expect(internalProtoView(protoViewRef).elementBinders[0].nestedProtoView) .toBe(mainProtoView); - expect(originalRenderProtoViewRefs(mainProtoView)) - .toEqual([renderPvDtos[0].render, renderPvDtos[1].render]); expect(mainProtoView.elementBinders[0].nestedProtoView).toBe(nestedProtoView); - expect(originalRenderProtoViewRefs(nestedProtoView)) - .toEqual([renderPvDtos[1].render]); async.done(); }); })); @@ -375,7 +345,8 @@ export function main() { tplResolver.setView(MainComponent, new viewAnn.View({template: '
'})); tplResolver.setView(NestedComponent, new viewAnn.View({template: '
'})); var viewportProtoView = - createProtoView([createComponentElementBinder(directiveResolver, NestedComponent)]); + createProtoView([createComponentElementBinder(directiveResolver, NestedComponent)], + renderApi.ViewType.EMBEDDED); var mainProtoView = createProtoView([createViewportElementBinder(viewportProtoView)]); var nestedProtoView = createProtoView(); var renderPvDtos = [ @@ -391,23 +362,11 @@ export function main() { .then((protoViewRef) => { expect(internalProtoView(protoViewRef).elementBinders[0].nestedProtoView) .toBe(mainProtoView); - expect(originalRenderProtoViewRefs(mainProtoView)) - .toEqual([ - renderPvDtos[0] - .render, - renderPvDtos[0].elementBinders[0].nestedProtoView.render, - renderPvDtos[1].render - ]); + expect(originalRenderProtoViewRefs(internalProtoView(protoViewRef))) + .toEqual([rootProtoView.render, [mainProtoView.render, null]]); expect(viewportProtoView.elementBinders[0].nestedProtoView).toBe(nestedProtoView); expect(originalRenderProtoViewRefs(viewportProtoView)) - .toEqual([ - renderPvDtos[0] - .elementBinders[0] - .nestedProtoView.render, - renderPvDtos[1].render - ]); - expect(originalRenderProtoViewRefs(nestedProtoView)) - .toEqual([renderPvDtos[1].render]); + .toEqual([viewportProtoView.render, [nestedProtoView.render]]); async.done(); }); })); @@ -520,7 +479,8 @@ export function main() { inject([AsyncTestCompleter], (async) => { tplResolver.setView(MainComponent, new viewAnn.View({template: '
'})); var viewportProtoView = - createProtoView([createComponentElementBinder(directiveResolver, MainComponent)]); + createProtoView([createComponentElementBinder(directiveResolver, MainComponent)], + renderApi.ViewType.EMBEDDED); var mainProtoView = createProtoView([createViewportElementBinder(viewportProtoView)]); var renderPvDtos = [ createRenderProtoView([ @@ -539,20 +499,39 @@ export function main() { .nestedProtoView) .toBe(mainProtoView); // In case of a cycle, don't merge the embedded proto views into the component! - expect(originalRenderProtoViewRefs(mainProtoView)) - .toEqual([renderPvDtos[0].render, null]); + expect(originalRenderProtoViewRefs(internalProtoView(protoViewRef))) + .toEqual([rootProtoView.render, [mainProtoView.render, null]]); expect(originalRenderProtoViewRefs(viewportProtoView)) - .toEqual([ - renderPvDtos[0] - .elementBinders[0] - .nestedProtoView.render, - renderPvDtos[1].render, - null - ]); + .toEqual([viewportProtoView.render, [mainProtoView.render, null]]); async.done(); }); })); + it('should throw on recursive components that are connected via an embedded ProtoView with ', + inject([AsyncTestCompleter], (async) => { + tplResolver.setView(MainComponent, new viewAnn.View({template: '
'})); + var viewportProtoView = + createProtoView([createComponentElementBinder(directiveResolver, MainComponent)], + renderApi.ViewType.EMBEDDED, true); + var mainProtoView = createProtoView([createViewportElementBinder(viewportProtoView)]); + var renderPvDtos = [ + createRenderProtoView([ + createRenderViewportElementBinder(createRenderProtoView( + [createRenderComponentElementBinder(0)], renderApi.ViewType.EMBEDDED)) + ]), + createRenderProtoView() + ]; + var compiler = createCompiler(renderPvDtos, [rootProtoView, mainProtoView]); + PromiseWrapper.catchError(compiler.compileInHost(MainComponent), (e) => { + expect(() => { throw e; }) + .toThrowError( + ` is used within the recursive path of ${stringify(MainComponent)}`); + async.done(); + return null; + }); + })); + + it('should create host proto views', inject([AsyncTestCompleter], (async) => { tplResolver.setView(MainComponent, new viewAnn.View({template: '
'})); var rootProtoView = @@ -581,8 +560,13 @@ function createDirectiveBinding(directiveResolver, type): DirectiveBinding { return DirectiveBinding.createFromType(type, annotation); } -function createProtoView(elementBinders = null): AppProtoView { - var pv = new AppProtoView(null, null, null, new Map(), null); +function createProtoView(elementBinders = null, type: renderApi.ViewType = null, + isEmbeddedFragment: boolean = false): AppProtoView { + if (isBlank(type)) { + type = renderApi.ViewType.COMPONENT; + } + var pv = new AppProtoView(type, isEmbeddedFragment, new renderApi.RenderProtoViewRef(), null, + null, new Map(), null); if (isBlank(elementBinders)) { elementBinders = []; } @@ -623,7 +607,8 @@ function createRenderViewportElementBinder(nestedProtoView): renderApi.ElementBi } function createRootProtoView(directiveResolver, type): AppProtoView { - return createProtoView([createComponentElementBinder(directiveResolver, type)]); + return createProtoView([createComponentElementBinder(directiveResolver, type)], + renderApi.ViewType.HOST); } @Component({selector: 'main-comp'}) diff --git a/modules/angular2/test/core/compiler/proto_view_factory_spec.ts b/modules/angular2/test/core/compiler/proto_view_factory_spec.ts index 9b5719b372..d056fd320f 100644 --- a/modules/angular2/test/core/compiler/proto_view_factory_spec.ts +++ b/modules/angular2/test/core/compiler/proto_view_factory_spec.ts @@ -182,7 +182,8 @@ function createRenderProtoView(elementBinders = null, type: renderApi.ViewType = elementBinders: elementBinders, type: type, variableBindings: variableBindings, - textBindings: [] + textBindings: [], + transitiveNgContentCount: 0 }); } diff --git a/modules/angular2/test/core/compiler/view_manager_utils_spec.ts b/modules/angular2/test/core/compiler/view_manager_utils_spec.ts index 800d16173a..b29606c46f 100644 --- a/modules/angular2/test/core/compiler/view_manager_utils_spec.ts +++ b/modules/angular2/test/core/compiler/view_manager_utils_spec.ts @@ -289,13 +289,32 @@ function calcHostElementIndicesByViewIndex(pv: AppProtoView, elementOffset = 0, return target; } +function countNestedProtoViews(pv: AppProtoView, target: number[] = null): number[] { + if (isBlank(target)) { + target = []; + } + target.push(null); + var resultIndex = target.length - 1; + var count = 0; + for (var binderIdx = 0; binderIdx < pv.elementBinders.length; binderIdx++) { + var binder = pv.elementBinders[binderIdx]; + if (isPresent(binder.nestedProtoView)) { + var nextResultIndex = target.length; + countNestedProtoViews(binder.nestedProtoView, target); + count += target[nextResultIndex] + 1; + } + } + target[resultIndex] = count; + return target; +} + function _createProtoView(type: ViewType, binders: ElementBinder[] = null) { if (isBlank(binders)) { binders = []; } var protoChangeDetector = new SpyProtoChangeDetector(); protoChangeDetector.spy('instantiate').andReturn(new SpyChangeDetector()); - var res = new AppProtoView(type, protoChangeDetector, null, null, 0); + var res = new AppProtoView(type, null, null, protoChangeDetector, null, null, 0); res.elementBinders = binders; var mappedElementIndices = ListWrapper.createFixedSize(countNestedElementBinders(res)); for (var i = 0; i < binders.length; i++) { @@ -304,9 +323,11 @@ function _createProtoView(type: ViewType, binders: ElementBinder[] = null) { binder.protoElementInjector.index = i; } var hostElementIndicesByViewIndex = calcHostElementIndicesByViewIndex(res); - res.mergeMapping = new AppProtoViewMergeMapping( - new RenderProtoViewMergeMapping(null, hostElementIndicesByViewIndex.length, - mappedElementIndices, [], hostElementIndicesByViewIndex)); + if (type === ViewType.EMBEDDED || type === ViewType.HOST) { + res.mergeMapping = new AppProtoViewMergeMapping(new RenderProtoViewMergeMapping( + null, hostElementIndicesByViewIndex.length, mappedElementIndices, [], + hostElementIndicesByViewIndex, countNestedProtoViews(res))); + } return res; } diff --git a/modules/angular2/test/core/compiler/view_pool_spec.ts b/modules/angular2/test/core/compiler/view_pool_spec.ts index 0245b9c4fe..0a933ba5be 100644 --- a/modules/angular2/test/core/compiler/view_pool_spec.ts +++ b/modules/angular2/test/core/compiler/view_pool_spec.ts @@ -24,7 +24,9 @@ export function main() { function createViewPool({capacity}): AppViewPool { return new AppViewPool(capacity); } - function createProtoView() { return new AppProtoView(null, null, null, null, null); } + function createProtoView() { + return new AppProtoView(null, null, null, null, null, null, null); + } function createView(pv) { return new AppView(null, pv, null, null, null, null, new Map(), null, null); diff --git a/modules/angular2/test/render/dom/dom_renderer_integration_spec.ts b/modules/angular2/test/render/dom/dom_renderer_integration_spec.ts index 5d52c21109..e31b28b9b4 100644 --- a/modules/angular2/test/render/dom/dom_renderer_integration_spec.ts +++ b/modules/angular2/test/render/dom/dom_renderer_integration_spec.ts @@ -53,7 +53,7 @@ export function main() { {componentId: 'someComponent', template: '{{a}}', directives: []}) ]) .then((protoViewMergeMappings) => { - var rootView = tb.createView(protoViewMergeMappings[0]); + var rootView = tb.createView(protoViewMergeMappings); tb.renderer.setText(rootView.viewRef, 0, 'hello'); expect(rootView.hostElement).toHaveText('hello'); @@ -92,7 +92,7 @@ export function main() { expect(DOM.getAttribute(el, 'some-attr')).toEqual('someValue'); }; - var rootView = tb.createView(protoViewMergeMappings[0]); + var rootView = tb.createView(protoViewMergeMappings); // root element checkSetters(elRef(rootView.viewRef, 0), rootView.hostElement); // nested elements @@ -114,7 +114,7 @@ export function main() { }) ]) .then((protoViewMergeMappings) => { - var rootView = tb.createView(protoViewMergeMappings[0]); + var rootView = tb.createView(protoViewMergeMappings); var el = DOM.childNodes(rootView.hostElement)[0]; tb.renderer.setElementProperty(elRef(rootView.viewRef, 1), 'maxLength', '20'); expect(DOM.getAttribute(el, 'ng-reflect-max-length')) @@ -138,7 +138,7 @@ export function main() { }) ]) .then((protoViewMergeMappings) => { - var rootView = tb.createView(protoViewMergeMappings[0]); + var rootView = tb.createView(protoViewMergeMappings); var el = DOM.childNodes(rootView.hostElement)[0]; tb.renderer.setElementProperty(elRef(rootView.viewRef, 1), 'maxLength', '20'); expect(DOM.getAttribute(el, 'ng-reflect-max-length')) @@ -160,7 +160,7 @@ export function main() { }) ]) .then((protoViewMergeMappings) => { - var rootView = tb.createView(protoViewMergeMappings[0]); + var rootView = tb.createView(protoViewMergeMappings); tb.renderer.invokeElementMethod(elRef(rootView.viewRef, 1), 'setAttribute', ['a', 'b']); @@ -183,7 +183,7 @@ export function main() { }) ]) .then((protoViewMergeMappings) => { - var rootView = tb.createView(protoViewMergeMappings[0]); + var rootView = tb.createView(protoViewMergeMappings); var elr = elRef(rootView.viewRef, 1); expect(rootView.hostElement).toHaveText(''); @@ -208,7 +208,7 @@ export function main() { }) ]) .then((protoViewMergeMappings) => { - var rootView = tb.createView(protoViewMergeMappings[0]); + var rootView = tb.createView(protoViewMergeMappings); var elr = elRef(rootView.viewRef, 1); expect(rootView.hostElement).toHaveText(''); @@ -233,8 +233,8 @@ export function main() { directives: [] }) ]) - .then((protoViewDtos) => { - var rootView = tb.createView(protoViewDtos[0]); + .then((protoViewMergeMappings) => { + var rootView = tb.createView(protoViewMergeMappings); tb.triggerEvent(elRef(rootView.viewRef, 1), 'change'); var eventEntry = rootView.events[0]; @@ -263,7 +263,7 @@ export function main() { {componentId: 'someComponent', template: 'hello', directives: []}) ]) .then((protoViewMergeMappings) => { - var rootView = tb.createView(protoViewMergeMappings[0]); + var rootView = tb.createView(protoViewMergeMappings); expect(DOM.getShadowRoot(rootView.hostElement)).toHaveText('hello'); async.done(); }); diff --git a/modules/angular2/test/render/dom/dom_testbed.ts b/modules/angular2/test/render/dom/dom_testbed.ts index b02d454cfc..25d3aede26 100644 --- a/modules/angular2/test/render/dom/dom_testbed.ts +++ b/modules/angular2/test/render/dom/dom_testbed.ts @@ -82,14 +82,13 @@ export class DomTestbed { return PromiseWrapper.all(promises); } - merge(protoViews: - List): Promise { + merge(protoViews: List): Promise { return this.compiler.mergeProtoViewsRecursively(collectMergeRenderProtoViewsRecurse( protoViews[0], ListWrapper.slice(protoViews, 1))); } compileAndMerge(host: DirectiveMetadata, - componentViews: ViewDefinition[]): Promise { + componentViews: ViewDefinition[]): Promise { return this.compile(host, componentViews).then(protoViewDtos => this.merge(protoViewDtos)); } diff --git a/modules/angular2/test/render/dom/view/proto_view_merger_integration_spec.ts b/modules/angular2/test/render/dom/view/proto_view_merger_integration_spec.ts index 98756f04ed..0bc52e73aa 100644 --- a/modules/angular2/test/render/dom/view/proto_view_merger_integration_spec.ts +++ b/modules/angular2/test/render/dom/view/proto_view_merger_integration_spec.ts @@ -133,7 +133,7 @@ export function main() { 'A(B(
)
)
' ])); - it('should keep non projected embedded views (so that they can be moved manually)', + it('should keep non projected embedded views as fragments (so that they can be moved manually)', runAndAssert( 'root', ['', ''], ['', 'b'])); @@ -145,13 +145,6 @@ export function main() { 'b' ])); - it('should project embedded views and match the single root element', - runAndAssert( - 'root', ['
', 'A()'], [ - 'A()', - '
' - ])); - it('should project nodes using the ng-content in embedded views', runAndAssert('root', ['b', 'A()'], [ 'A()', @@ -258,7 +251,7 @@ function runAndAssert(hostElementName: string, componentTemplates: string[], directives: [aComp, bComp, cComp] }))) .then((mergeMappings) => { - expect(stringify(mergeMappings[0])).toEqual(expectedFragments); + expect(stringify(mergeMappings)).toEqual(expectedFragments); async.done(); }); }); diff --git a/modules/angular2/test/render/dom/view/proto_view_merger_spec.ts b/modules/angular2/test/render/dom/view/proto_view_merger_spec.ts deleted file mode 100644 index adc0a39496..0000000000 --- a/modules/angular2/test/render/dom/view/proto_view_merger_spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { - AsyncTestCompleter, - beforeEach, - ddescribe, - describe, - el, - expect, - iit, - inject, - it, - xit, - beforeEachBindings, - SpyObject, - stringifyElement -} from 'angular2/test_lib'; - -import {mergeSelectors} from 'angular2/src/render/dom/view/proto_view_merger'; - -export function main() { - describe('ProtoViewMerger test', () => { - - describe('mergeSelectors', () => { - it('should merge empty selectors', () => { - expect(mergeSelectors('', 'a')).toEqual('a'); - expect(mergeSelectors('a', '')).toEqual('a'); - expect(mergeSelectors('', '')).toEqual(''); - }); - - it('should merge wildcard selectors', () => { - expect(mergeSelectors('*', 'a')).toEqual('a'); - expect(mergeSelectors('a', '*')).toEqual('a'); - expect(mergeSelectors('*', '*')).toEqual('*'); - }); - - it('should merge 2 element selectors', - () => { expect(mergeSelectors('a', 'b')).toEqual('_not-matchable_'); }); - - it('should merge elements and non element selector', () => { - expect(mergeSelectors('a', '.b')).toEqual('a.b'); - expect(mergeSelectors('.b', 'a')).toEqual('a.b'); - }); - - it('should merge attributes', () => { - expect(mergeSelectors('[a]', '[b]')).toEqual('[a][b]'); - expect(mergeSelectors('[a][b]', '[c][d]')).toEqual('[a][b][c][d]'); - expect(mergeSelectors('[a=1]', '[b=2]')).toEqual('[a=1][b=2]'); - }); - - it('should merge classes', () => { - expect(mergeSelectors('.a', '.b')).toEqual('.a.b'); - expect(mergeSelectors('.a.b', '.c.d')).toEqual('.a.b.c.d'); - }); - }); - - }); -} diff --git a/modules/angular2/test/render/dom/view/view_spec.ts b/modules/angular2/test/render/dom/view/view_spec.ts index 6e6f8faa21..05ae70fc70 100644 --- a/modules/angular2/test/render/dom/view/view_spec.ts +++ b/modules/angular2/test/render/dom/view/view_spec.ts @@ -30,7 +30,7 @@ export function main() { binders = []; } var rootEl = DOM.createTemplate('
'); - return DomProtoView.create(null, rootEl, [1], [], binders, null, null, null); + return DomProtoView.create(null, rootEl, [1], [], binders); } function createElementBinder() { return new DomElementBinder({textNodeIndices: []}); }