refactor(compiler): speed up proto view merging

- Don't create intermediate merge results
- Only merge embedded ProtoViews that contain `<ng-content>` tags

Closes #3150
Closes #3177
This commit is contained in:
Tobias Bosch 2015-07-20 09:59:44 -07:00
parent de18da2a0d
commit 078475a082
20 changed files with 359 additions and 466 deletions

View File

@ -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<any> {
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<renderApi.RenderProtoViewRef | List<any>> {
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>| AppProtoView {
@ -207,7 +237,7 @@ export class Compiler {
appProtoView: AppProtoView,
componentType: Type): Promise<AppProtoView> {
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<renderApi.RenderProtoViewRef | List<any>>): Promise<AppProtoView> {
return this._render.mergeProtoViewsRecursively(mergeRenderProtoViews)
.then((mergeResults: List<renderApi.RenderProtoViewMergeMapping>) => {
// 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(
`<ng-content> 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<renderApi.RenderProtoViewRef | List<any>> {
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) {}
}

View File

@ -254,9 +254,12 @@ function _createAppProtoView(
renderProtoView: renderApi.ProtoViewDto, protoChangeDetector: ProtoChangeDetector,
variableBindings: Map<string, string>, allDirectives: List<DirectiveBinding>): AppProtoView {
var elementBinders = renderProtoView.elementBinders;
var protoView = new AppProtoView(renderProtoView.type, protoChangeDetector, variableBindings,
createVariableLocations(elementBinders),
renderProtoView.textBindings.length);
// Embedded ProtoViews that contain `<ng-content>` 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);

View File

@ -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<string, any>): 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<string, string>,
public variableLocations: Map<string, number>, public textBindingCount: number) {
this.ref = new ProtoViewRef(this);

View File

@ -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);
}
}
}
}

View File

@ -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)) {

View File

@ -114,19 +114,23 @@ export class ProtoViewDto {
variableBindings: Map<string, string>;
type: ViewType;
textBindings: List<ASTWithSource>;
transitiveNgContentCount: number;
constructor({render, elementBinders, variableBindings, type, textBindings}: {
constructor({render, elementBinders, variableBindings, type, textBindings,
transitiveNgContentCount}: {
render?: RenderProtoViewRef,
elementBinders?: List<ElementBinder>,
variableBindings?: Map<string, string>,
type?: ViewType,
textBindings?: List<ASTWithSource>
textBindings?: List<ASTWithSource>,
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<RenderProtoViewRef | List<any>>): Promise<RenderProtoViewMergeMapping[]> {
protoViewRefs: List<RenderProtoViewRef | List<any>>): Promise<RenderProtoViewMergeMapping> {
return null;
}
}

View File

@ -54,8 +54,7 @@ export class DomCompiler extends RenderCompiler {
}
mergeProtoViewsRecursively(
protoViewRefs:
List<RenderProtoViewRef | List<any>>): Promise<List<RenderProtoViewMergeMapping>> {
protoViewRefs: List<RenderProtoViewRef | List<any>>): Promise<RenderProtoViewMergeMapping> {
return PromiseWrapper.resolve(pvm.mergeProtoViewsRecursively(protoViewRefs));
}

View File

@ -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;

View File

@ -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<DomElementBinder>,
mappedElementIndices: number[], mappedTextIndices: number[],
hostElementIndicesByViewIndex: number[]): DomProtoView {
rootTextNodeIndices: number[],
elementBinders: List<DomElementBinder>): 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<DomElementBinder>, public rootTextNodeIndices: number[],
public boundTextNodeCount: number, public fragmentsRootNodeCount: number[],
public isSingleElementFragment: boolean, public mappedElementIndices: number[],
public mappedTextIndices: number[], public hostElementIndicesByViewIndex: number[]) {}
public isSingleElementFragment: boolean) {}
}

View File

@ -29,6 +29,7 @@ export class ProtoViewBuilder {
variableBindings: Map<string, string> = new Map();
elements: List<ElementBinderBuilder> = [];
rootTextBindings: Map<Node, ASTWithSource> = 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
});
}
}

View File

@ -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<RenderProtoViewRef | List<any>>):
RenderProtoViewMergeMapping[] {
var target = [];
_mergeProtoViewsRecursively(protoViewRefs, target);
return target;
}
function _mergeProtoViewsRecursively(protoViewRefs: List<RenderProtoViewRef | List<any>>,
target: RenderProtoViewMergeMapping[]): RenderProtoViewRef {
var targetIndex = target.length;
target.push(null);
var resolvedProtoViewRefs = protoViewRefs.map((entry) => {
if (isArray(entry)) {
return _mergeProtoViewsRecursively(<List<any>>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<RenderProtoViewRef | List<any>>,
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(<any[]>nestedEntry, targetClonedProtoViews,
targetHostViewAndBinderIndices);
} else {
targetClonedProtoViews.push(
cloneAndQueryProtoView(resolveInternalDomProtoView(nestedEntry), false));
}
}
}
}
}
function indexBoundTextNodes(mergableProtoViews: ClonedProtoView[]): Map<Node, any> {
@ -112,42 +96,56 @@ function indexBoundTextNodes(mergableProtoViews: ClonedProtoView[]): Map<Node, a
return boundTextNodeMap;
}
function mergeDom(clonedProtoViews: ClonedProtoView[], hostElementIndices: number[]) {
var nestedProtoViewByHostElement: Map<Element, ClonedProtoView> =
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<Element, ClonedProtoView> {
var hostProtoView = mergableProtoViews[0];
var mergableProtoViewIdx = 1;
var nestedProtoViewByHostElement: Map<Element, ClonedProtoView> = 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<Element, ClonedProtoView>) {
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 <ng-content>
// 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 <ng-content> 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<Element, number> = 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<Node, number>): 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<any, number> {

View File

@ -45,35 +45,6 @@ export function main() {
rootProtoView;
var renderCompileRequests: any[];
function mergeProtoViewsRecursively(
protoViewRefs: List<renderApi.RenderProtoViewRef | List<any>>,
target: renderApi.RenderProtoViewMergeMapping[]): renderApi.RenderProtoViewRef {
var targetIndex = target.length;
target.push(null);
var flattended = protoViewRefs.map(protoViewRefOrArray => {
var resolvedProtoViewRef;
if (isArray(protoViewRefOrArray)) {
resolvedProtoViewRef = mergeProtoViewsRecursively(
<List<renderApi.RenderProtoViewRef>>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<renderApi.ProtoViewDto | Promise<renderApi.ProtoViewDto>>,
protoViewFactoryResults: List<AppProtoView>) {
@ -102,10 +73,10 @@ export function main() {
});
renderCompiler.spy('mergeProtoViewsRecursively')
.andCallFake((protoViewRefs: List<renderApi.RenderProtoViewRef | List<any>>) => {
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: '<div></div>'}));
tplResolver.setView(NestedComponent, new viewAnn.View({template: '<div></div>'}));
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: '<div></div>'}));
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 <ng-content>',
inject([AsyncTestCompleter], (async) => {
tplResolver.setView(MainComponent, new viewAnn.View({template: '<div></div>'}));
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(
`<ng-content> 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: '<div></div>'}));
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'})

View File

@ -182,7 +182,8 @@ function createRenderProtoView(elementBinders = null, type: renderApi.ViewType =
elementBinders: elementBinders,
type: type,
variableBindings: variableBindings,
textBindings: []
textBindings: [],
transitiveNgContentCount: 0
});
}

View File

@ -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 = <any>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;
}

View File

@ -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);

View File

@ -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(<HTMLInputElement>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(<HTMLInputElement>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();
});

View File

@ -82,14 +82,13 @@ export class DomTestbed {
return PromiseWrapper.all(promises);
}
merge(protoViews:
List<ProtoViewDto | RenderProtoViewRef>): Promise<RenderProtoViewMergeMapping[]> {
merge(protoViews: List<ProtoViewDto | RenderProtoViewRef>): Promise<RenderProtoViewMergeMapping> {
return this.compiler.mergeProtoViewsRecursively(collectMergeRenderProtoViewsRecurse(
<ProtoViewDto>protoViews[0], ListWrapper.slice(protoViews, 1)));
}
compileAndMerge(host: DirectiveMetadata,
componentViews: ViewDefinition[]): Promise<RenderProtoViewMergeMapping[]> {
componentViews: ViewDefinition[]): Promise<RenderProtoViewMergeMapping> {
return this.compile(host, componentViews).then(protoViewDtos => this.merge(protoViewDtos));
}

View File

@ -133,7 +133,7 @@ export function main() {
'<root class="ng-binding" idx="0"><a class="ng-binding" idx="1">A(<b class="ng-binding" idx="2">B(<div class="x y"></div>)</b>)</a></root>'
]));
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', ['<a><template class="x">b</template></a>', ''],
['<root class="ng-binding" idx="0"><a class="ng-binding" idx="1"></a></root>', 'b']));
@ -145,13 +145,6 @@ export function main() {
'b'
]));
it('should project embedded views and match the single root element',
runAndAssert(
'root', ['<a><div class="x" *ng-if></div></a>', 'A(<ng-content></ng-content>)'], [
'<root class="ng-binding" idx="0"><a class="ng-binding" idx="1">A(<template class="ng-binding" idx="2" ng-if=""></template>)</a></root>',
'<div *ng-if="" class="x"></div>'
]));
it('should project nodes using the ng-content in embedded views',
runAndAssert('root', ['<a>b</a>', 'A(<ng-content *ng-if></ng-content>)'], [
'<root class="ng-binding" idx="0"><a class="ng-binding" idx="1">A(<template class="ng-binding" idx="2" ng-if=""></template>)</a></root>',
@ -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();
});
});

View File

@ -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');
});
});
});
}

View File

@ -30,7 +30,7 @@ export function main() {
binders = [];
}
var rootEl = DOM.createTemplate('<div></div>');
return DomProtoView.create(null, <Element>rootEl, [1], [], binders, null, null, null);
return DomProtoView.create(null, <Element>rootEl, [1], [], binders);
}
function createElementBinder() { return new DomElementBinder({textNodeIndices: []}); }