fix(render): recurse into components/embedded templates not until all elements in a view have been visited

Fixes #4551
Closes #4601
This commit is contained in:
Tobias Bosch
2015-10-07 17:15:12 -07:00
parent ff77230edb
commit 6d4bd5d901
11 changed files with 294 additions and 133 deletions

View File

@ -74,7 +74,7 @@ export class CommandCompiler {
interface CommandFactory<R> {
createText(value: string, isBound: boolean, ngContentIndex: number): R;
createNgContent(ngContentIndex: number): R;
createNgContent(index: number, ngContentIndex: number): R;
createBeginElement(name: string, attrNameAndValues: string[], eventTargetAndNames: string[],
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
isBound: boolean, ngContentIndex: number): R;
@ -114,7 +114,9 @@ class RuntimeCommandFactory implements CommandFactory<TemplateCmd> {
createText(value: string, isBound: boolean, ngContentIndex: number): TemplateCmd {
return text(value, isBound, ngContentIndex);
}
createNgContent(ngContentIndex: number): TemplateCmd { return ngContent(ngContentIndex); }
createNgContent(index: number, ngContentIndex: number): TemplateCmd {
return ngContent(index, ngContentIndex);
}
createBeginElement(name: string, attrNameAndValues: string[], eventTargetAndNames: string[],
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
isBound: boolean, ngContentIndex: number): TemplateCmd {
@ -169,8 +171,8 @@ class CodegenCommandFactory implements CommandFactory<string> {
createText(value: string, isBound: boolean, ngContentIndex: number): string {
return `${TEMPLATE_COMMANDS_MODULE_REF}text(${escapeSingleQuoteString(value)}, ${isBound}, ${ngContentIndex})`;
}
createNgContent(ngContentIndex: number): string {
return `${TEMPLATE_COMMANDS_MODULE_REF}ngContent(${ngContentIndex})`;
createNgContent(index: number, ngContentIndex: number): string {
return `${TEMPLATE_COMMANDS_MODULE_REF}ngContent(${index}, ${ngContentIndex})`;
}
createBeginElement(name: string, attrNameAndValues: string[], eventTargetAndNames: string[],
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
@ -221,7 +223,7 @@ class CommandBuilderVisitor<R> implements TemplateAstVisitor {
visitNgContent(ast: NgContentAst, context: any): any {
this.transitiveNgContentCount++;
this.result.push(this.commandFactory.createNgContent(ast.ngContentIndex));
this.result.push(this.commandFactory.createNgContent(ast.index, ast.ngContentIndex));
return null;
}
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any {

View File

@ -104,7 +104,7 @@ export class DirectiveAst implements TemplateAst {
}
export class NgContentAst implements TemplateAst {
constructor(public ngContentIndex: number, public sourceInfo: string) {}
constructor(public index: number, public ngContentIndex: number, public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitNgContent(this, context);
}

View File

@ -96,6 +96,8 @@ class TemplateParseVisitor implements HtmlAstVisitor {
selectorMatcher: SelectorMatcher;
errors: string[] = [];
directivesIndex = new Map<CompileDirectiveMetadata, number>();
ngContentCount: number = 0;
constructor(directives: CompileDirectiveMetadata[], private _exprParser: Parser,
private _schemaRegistry: ElementSchemaRegistry) {
this.selectorMatcher = new SelectorMatcher();
@ -207,7 +209,8 @@ class TemplateParseVisitor implements HtmlAstVisitor {
hasInlineTemplates ? null : component.findNgContentIndex(elementCssSelector);
var parsedElement;
if (preparsedElement.type === PreparsedElementType.NG_CONTENT) {
parsedElement = new NgContentAst(elementNgContentIndex, element.sourceInfo);
parsedElement =
new NgContentAst(this.ngContentCount++, elementNgContentIndex, element.sourceInfo);
} else if (isTemplateElement) {
this._assertNoComponentsNorElementBindingsOnTemplate(directives, elementProps, events,
element.sourceInfo);

View File

@ -72,14 +72,14 @@ export function text(value: string, isBound: boolean, ngContentIndex: number): T
export class NgContentCmd implements TemplateCmd, RenderNgContentCmd {
isBound: boolean = false;
constructor(public ngContentIndex: number) {}
constructor(public index: number, public ngContentIndex: number) {}
visit(visitor: RenderCommandVisitor, context: any): any {
return visitor.visitNgContent(this, context);
}
}
export function ngContent(ngContentIndex: number): NgContentCmd {
return new NgContentCmd(ngContentIndex);
export function ngContent(index: number, ngContentIndex: number): NgContentCmd {
return new NgContentCmd(index, ngContentIndex);
}
export interface IBeginElementCmd extends TemplateCmd, RenderBeginElementCmd {

View File

@ -85,7 +85,13 @@ export interface RenderBeginCmd extends RenderTemplateCmd {
export interface RenderTextCmd extends RenderBeginCmd { value: string; }
export interface RenderNgContentCmd { ngContentIndex: number; }
export interface RenderNgContentCmd {
// The index of this NgContent element
index: number;
// The index of the NgContent element into which this
// NgContent element should be projected (if any)
ngContentIndex: number;
}
export interface RenderBeginElementCmd extends RenderBeginCmd {
name: string;

View File

@ -13,58 +13,20 @@ import {DefaultRenderView, DefaultRenderFragmentRef} from './view';
export function createRenderView(fragmentCmds: RenderTemplateCmd[], inplaceElement: any,
nodeFactory: NodeFactory<any>): DefaultRenderView<any> {
var builders: RenderViewBuilder<any>[] = [];
visitAll(new RenderViewBuilder<any>(null, null, inplaceElement, builders, nodeFactory),
fragmentCmds);
var boundElements: any[] = [];
var boundTextNodes: any[] = [];
var nativeShadowRoots: any[] = [];
var fragments: DefaultRenderFragmentRef<any>[] = [];
var viewElementOffset = 0;
var view: DefaultRenderView<any>;
var eventDispatcher = (boundElementIndex: number, eventName: string, event: any) =>
view.dispatchRenderEvent(boundElementIndex, eventName, event);
var globalEventAdders: Function[] = [];
for (var i = 0; i < builders.length; i++) {
var builder = builders[i];
addAll(builder.boundElements, boundElements);
addAll(builder.boundTextNodes, boundTextNodes);
addAll(builder.nativeShadowRoots, nativeShadowRoots);
if (isBlank(builder.rootNodesParent)) {
fragments.push(new DefaultRenderFragmentRef<any>(builder.fragmentRootNodes));
}
for (var j = 0; j < builder.eventData.length; j++) {
var eventData = builder.eventData[j];
var boundElementIndex = eventData[0] + viewElementOffset;
var target = eventData[1];
var eventName = eventData[2];
if (isPresent(target)) {
var handler =
createEventHandler(boundElementIndex, `${target}:${eventName}`, eventDispatcher);
globalEventAdders.push(createGlobalEventAdder(target, eventName, handler, nodeFactory));
} else {
var handler = createEventHandler(boundElementIndex, eventName, eventDispatcher);
nodeFactory.on(boundElements[boundElementIndex], eventName, handler);
}
}
viewElementOffset += builder.boundElements.length;
var context = new BuildContext(eventDispatcher, nodeFactory, inplaceElement);
context.build(fragmentCmds);
var fragments: DefaultRenderFragmentRef<any>[] = [];
for (var i = 0; i < context.fragments.length; i++) {
fragments.push(new DefaultRenderFragmentRef(context.fragments[i]));
}
view = new DefaultRenderView<any>(fragments, boundTextNodes, boundElements, nativeShadowRoots,
globalEventAdders);
view = new DefaultRenderView<any>(fragments, context.boundTextNodes, context.boundElements,
context.nativeShadowRoots, context.globalEventAdders);
return view;
}
function createEventHandler(boundElementIndex: number, eventName: string,
eventDispatcher: Function): Function {
return ($event) => eventDispatcher(boundElementIndex, eventName, $event);
}
function createGlobalEventAdder(target: string, eventName: string, eventHandler: Function,
nodeFactory: NodeFactory<any>): Function {
return () => nodeFactory.globalOn(target, eventName, eventHandler);
}
export interface NodeFactory<N> {
resolveComponentTemplate(templateId: number): RenderTemplateCmd[];
createTemplateAnchor(attrNameAndValues: string[]): N;
@ -77,96 +39,156 @@ export interface NodeFactory<N> {
globalOn(target: string, eventName: string, callback: Function): Function;
}
class BuildContext<N> {
constructor(private _eventDispatcher: Function, public factory: NodeFactory<N>,
private _inplaceElement: N) {}
private _builders: RenderViewBuilder<N>[] = [];
globalEventAdders: Function[] = [];
boundElements: N[] = [];
boundTextNodes: N[] = [];
nativeShadowRoots: N[] = [];
fragments: N[][] = [];
build(fragmentCmds: RenderTemplateCmd[]) {
this.enqueueFragmentBuilder(null, fragmentCmds);
this._build(this._builders[0]);
}
private _build(builder: RenderViewBuilder<N>) {
this._builders = [];
builder.build(this);
var enqueuedBuilders = this._builders;
for (var i = 0; i < enqueuedBuilders.length; i++) {
this._build(enqueuedBuilders[i]);
}
}
enqueueComponentBuilder(component: Component<N>) {
this._builders.push(new RenderViewBuilder<N>(
component, null, this.factory.resolveComponentTemplate(component.cmd.templateId)));
}
enqueueFragmentBuilder(parentComponent: Component<N>, commands: RenderTemplateCmd[]) {
var rootNodes = [];
this.fragments.push(rootNodes);
this._builders.push(new RenderViewBuilder<N>(parentComponent, rootNodes, commands));
}
consumeInplaceElement(): N {
var result = this._inplaceElement;
this._inplaceElement = null;
return result;
}
addEventListener(boundElementIndex: number, target: string, eventName: string) {
if (isPresent(target)) {
var handler =
createEventHandler(boundElementIndex, `${target}:${eventName}`, this._eventDispatcher);
this.globalEventAdders.push(createGlobalEventAdder(target, eventName, handler, this.factory));
} else {
var handler = createEventHandler(boundElementIndex, eventName, this._eventDispatcher);
this.factory.on(this.boundElements[boundElementIndex], eventName, handler);
}
}
}
function createEventHandler(boundElementIndex: number, eventName: string,
eventDispatcher: Function): Function {
return ($event) => eventDispatcher(boundElementIndex, eventName, $event);
}
function createGlobalEventAdder(target: string, eventName: string, eventHandler: Function,
nodeFactory: NodeFactory<any>): Function {
return () => nodeFactory.globalOn(target, eventName, eventHandler);
}
class RenderViewBuilder<N> implements RenderCommandVisitor {
parentStack: Array<N | Component<N>>;
boundTextNodes: N[] = [];
boundElements: N[] = [];
eventData: any[][] = [];
fragmentRootNodes: N[] = [];
nativeShadowRoots: N[] = [];
constructor(public parentComponent: Component<N>, public rootNodesParent: N,
public inplaceElement: N, public allBuilders: RenderViewBuilder<N>[],
public factory: NodeFactory<N>) {
constructor(public parentComponent: Component<N>, public fragmentRootNodes: N[],
public commands: RenderTemplateCmd[]) {
var rootNodesParent = isPresent(fragmentRootNodes) ? null : parentComponent.shadowRoot;
this.parentStack = [rootNodesParent];
allBuilders.push(this);
}
build(context: BuildContext<N>) {
for (var i = 0; i < this.commands.length; i++) {
this.commands[i].visit(this, context);
}
}
get parent(): N | Component<N> { return this.parentStack[this.parentStack.length - 1]; }
visitText(cmd: RenderTextCmd, context: any): any {
var text = this.factory.createText(cmd.value);
this._addChild(text, cmd.ngContentIndex);
visitText(cmd: RenderTextCmd, context: BuildContext<N>): any {
var text = context.factory.createText(cmd.value);
this._addChild(text, cmd.ngContentIndex, context);
if (cmd.isBound) {
this.boundTextNodes.push(text);
context.boundTextNodes.push(text);
}
return null;
}
visitNgContent(cmd: RenderNgContentCmd, context: any): any {
visitNgContent(cmd: RenderNgContentCmd, context: BuildContext<N>): any {
if (isPresent(this.parentComponent)) {
var projectedNodes = this.parentComponent.project();
var projectedNodes = this.parentComponent.project(cmd.index);
for (var i = 0; i < projectedNodes.length; i++) {
var node = projectedNodes[i];
this._addChild(node, cmd.ngContentIndex);
this._addChild(node, cmd.ngContentIndex, context);
}
}
return null;
}
visitBeginElement(cmd: RenderBeginElementCmd, context: any): any {
this.parentStack.push(this._beginElement(cmd));
visitBeginElement(cmd: RenderBeginElementCmd, context: BuildContext<N>): any {
this.parentStack.push(this._beginElement(cmd, context));
return null;
}
visitEndElement(context: any): any {
visitEndElement(context: BuildContext<N>): any {
this._endElement();
return null;
}
visitBeginComponent(cmd: RenderBeginComponentCmd, context: any): any {
var el = this._beginElement(cmd);
visitBeginComponent(cmd: RenderBeginComponentCmd, context: BuildContext<N>): any {
var el = this._beginElement(cmd, context);
var root = el;
if (cmd.nativeShadow) {
root = this.factory.createShadowRoot(el, cmd.templateId);
this.nativeShadowRoots.push(root);
root = context.factory.createShadowRoot(el, cmd.templateId);
context.nativeShadowRoots.push(root);
}
var component = new Component(el, root, cmd, this.factory, this.allBuilders);
var component = new Component(el, root, cmd);
context.enqueueComponentBuilder(component);
this.parentStack.push(component);
return null;
}
visitEndComponent(context: any): any {
var c = <Component<N>>this.parent;
c.build();
visitEndComponent(context: BuildContext<N>): any {
this._endElement();
return null;
}
visitEmbeddedTemplate(cmd: RenderEmbeddedTemplateCmd, context: any): any {
var el = this.factory.createTemplateAnchor(cmd.attrNameAndValues);
this._addChild(el, cmd.ngContentIndex);
this.boundElements.push(el);
visitEmbeddedTemplate(cmd: RenderEmbeddedTemplateCmd, context: BuildContext<N>): any {
var el = context.factory.createTemplateAnchor(cmd.attrNameAndValues);
this._addChild(el, cmd.ngContentIndex, context);
context.boundElements.push(el);
if (cmd.isMerged) {
visitAll(
new RenderViewBuilder(this.parentComponent, null, null, this.allBuilders, this.factory),
cmd.children);
context.enqueueFragmentBuilder(this.parentComponent, cmd.children);
}
return null;
}
private _beginElement(cmd: RenderBeginElementCmd): N {
var el: N;
if (isPresent(this.inplaceElement)) {
el = this.inplaceElement;
this.inplaceElement = null;
this.factory.mergeElement(el, cmd.attrNameAndValues);
private _beginElement(cmd: RenderBeginElementCmd, context: BuildContext<N>): N {
var el: N = context.consumeInplaceElement();
if (isPresent(el)) {
context.factory.mergeElement(el, cmd.attrNameAndValues);
this.fragmentRootNodes.push(el);
} else {
el = this.factory.createElement(cmd.name, cmd.attrNameAndValues);
this._addChild(el, cmd.ngContentIndex);
el = context.factory.createElement(cmd.name, cmd.attrNameAndValues);
this._addChild(el, cmd.ngContentIndex, context);
}
if (cmd.isBound) {
this.boundElements.push(el);
var boundElementIndex = context.boundElements.length;
context.boundElements.push(el);
for (var i = 0; i < cmd.eventTargetAndNames.length; i += 2) {
var target = cmd.eventTargetAndNames[i];
var eventName = cmd.eventTargetAndNames[i + 1];
this.eventData.push([this.boundElements.length - 1, target, eventName]);
context.addEventListener(boundElementIndex, target, eventName);
}
}
return el;
@ -174,13 +196,13 @@ class RenderViewBuilder<N> implements RenderCommandVisitor {
private _endElement() { this.parentStack.pop(); }
private _addChild(node: N, ngContentIndex: number) {
private _addChild(node: N, ngContentIndex: number, context: BuildContext<N>) {
var parent = this.parent;
if (isPresent(parent)) {
if (parent instanceof Component) {
parent.addContentNode(ngContentIndex, node);
parent.addContentNode(ngContentIndex, node, context);
} else {
this.factory.appendChild(<N>parent, node);
context.factory.appendChild(<N>parent, node);
}
} else {
this.fragmentRootNodes.push(node);
@ -190,17 +212,12 @@ class RenderViewBuilder<N> implements RenderCommandVisitor {
class Component<N> {
private contentNodesByNgContentIndex: N[][] = [];
private projectingNgContentIndex: number = 0;
private viewBuilder: RenderViewBuilder<N>;
constructor(public hostElement: N, shadowRoot: N, public cmd: RenderBeginComponentCmd,
public factory: NodeFactory<N>, allBuilders: RenderViewBuilder<N>[]) {
this.viewBuilder = new RenderViewBuilder(this, shadowRoot, null, allBuilders, factory);
}
addContentNode(ngContentIndex: number, node: N) {
constructor(public hostElement: N, public shadowRoot: N, public cmd: RenderBeginComponentCmd) {}
addContentNode(ngContentIndex: number, node: N, context: BuildContext<N>) {
if (isBlank(ngContentIndex)) {
if (this.cmd.nativeShadow) {
this.factory.appendChild(this.hostElement, node);
context.factory.appendChild(this.hostElement, node);
}
} else {
while (this.contentNodesByNgContentIndex.length <= ngContentIndex) {
@ -209,15 +226,11 @@ class Component<N> {
this.contentNodesByNgContentIndex[ngContentIndex].push(node);
}
}
project(): N[] {
var ngContentIndex = this.projectingNgContentIndex++;
project(ngContentIndex: number): N[] {
return ngContentIndex < this.contentNodesByNgContentIndex.length ?
this.contentNodesByNgContentIndex[ngContentIndex] :
[];
}
build() {
visitAll(this.viewBuilder, this.factory.resolveComponentTemplate(this.cmd.templateId));
}
}
function addAll(source: any[], target: any[]) {
@ -225,9 +238,3 @@ function addAll(source: any[], target: any[]) {
target.push(source[i]);
}
}
function visitAll(visitor: RenderCommandVisitor, fragmentCmds: RenderTemplateCmd[]) {
for (var i = 0; i < fragmentCmds.length; i++) {
fragmentCmds[i].visit(visitor, null);
}
}