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:
parent
ff77230edb
commit
6d4bd5d901
|
@ -74,7 +74,7 @@ export class CommandCompiler {
|
||||||
|
|
||||||
interface CommandFactory<R> {
|
interface CommandFactory<R> {
|
||||||
createText(value: string, isBound: boolean, ngContentIndex: number): 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[],
|
createBeginElement(name: string, attrNameAndValues: string[], eventTargetAndNames: string[],
|
||||||
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
|
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
|
||||||
isBound: boolean, ngContentIndex: number): R;
|
isBound: boolean, ngContentIndex: number): R;
|
||||||
|
@ -114,7 +114,9 @@ class RuntimeCommandFactory implements CommandFactory<TemplateCmd> {
|
||||||
createText(value: string, isBound: boolean, ngContentIndex: number): TemplateCmd {
|
createText(value: string, isBound: boolean, ngContentIndex: number): TemplateCmd {
|
||||||
return text(value, isBound, ngContentIndex);
|
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[],
|
createBeginElement(name: string, attrNameAndValues: string[], eventTargetAndNames: string[],
|
||||||
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
|
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
|
||||||
isBound: boolean, ngContentIndex: number): TemplateCmd {
|
isBound: boolean, ngContentIndex: number): TemplateCmd {
|
||||||
|
@ -169,8 +171,8 @@ class CodegenCommandFactory implements CommandFactory<string> {
|
||||||
createText(value: string, isBound: boolean, ngContentIndex: number): string {
|
createText(value: string, isBound: boolean, ngContentIndex: number): string {
|
||||||
return `${TEMPLATE_COMMANDS_MODULE_REF}text(${escapeSingleQuoteString(value)}, ${isBound}, ${ngContentIndex})`;
|
return `${TEMPLATE_COMMANDS_MODULE_REF}text(${escapeSingleQuoteString(value)}, ${isBound}, ${ngContentIndex})`;
|
||||||
}
|
}
|
||||||
createNgContent(ngContentIndex: number): string {
|
createNgContent(index: number, ngContentIndex: number): string {
|
||||||
return `${TEMPLATE_COMMANDS_MODULE_REF}ngContent(${ngContentIndex})`;
|
return `${TEMPLATE_COMMANDS_MODULE_REF}ngContent(${index}, ${ngContentIndex})`;
|
||||||
}
|
}
|
||||||
createBeginElement(name: string, attrNameAndValues: string[], eventTargetAndNames: string[],
|
createBeginElement(name: string, attrNameAndValues: string[], eventTargetAndNames: string[],
|
||||||
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
|
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
|
||||||
|
@ -221,7 +223,7 @@ class CommandBuilderVisitor<R> implements TemplateAstVisitor {
|
||||||
|
|
||||||
visitNgContent(ast: NgContentAst, context: any): any {
|
visitNgContent(ast: NgContentAst, context: any): any {
|
||||||
this.transitiveNgContentCount++;
|
this.transitiveNgContentCount++;
|
||||||
this.result.push(this.commandFactory.createNgContent(ast.ngContentIndex));
|
this.result.push(this.commandFactory.createNgContent(ast.index, ast.ngContentIndex));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any {
|
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any {
|
||||||
|
|
|
@ -104,7 +104,7 @@ export class DirectiveAst implements TemplateAst {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NgContentAst 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 {
|
visit(visitor: TemplateAstVisitor, context: any): any {
|
||||||
return visitor.visitNgContent(this, context);
|
return visitor.visitNgContent(this, context);
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,6 +96,8 @@ class TemplateParseVisitor implements HtmlAstVisitor {
|
||||||
selectorMatcher: SelectorMatcher;
|
selectorMatcher: SelectorMatcher;
|
||||||
errors: string[] = [];
|
errors: string[] = [];
|
||||||
directivesIndex = new Map<CompileDirectiveMetadata, number>();
|
directivesIndex = new Map<CompileDirectiveMetadata, number>();
|
||||||
|
ngContentCount: number = 0;
|
||||||
|
|
||||||
constructor(directives: CompileDirectiveMetadata[], private _exprParser: Parser,
|
constructor(directives: CompileDirectiveMetadata[], private _exprParser: Parser,
|
||||||
private _schemaRegistry: ElementSchemaRegistry) {
|
private _schemaRegistry: ElementSchemaRegistry) {
|
||||||
this.selectorMatcher = new SelectorMatcher();
|
this.selectorMatcher = new SelectorMatcher();
|
||||||
|
@ -207,7 +209,8 @@ class TemplateParseVisitor implements HtmlAstVisitor {
|
||||||
hasInlineTemplates ? null : component.findNgContentIndex(elementCssSelector);
|
hasInlineTemplates ? null : component.findNgContentIndex(elementCssSelector);
|
||||||
var parsedElement;
|
var parsedElement;
|
||||||
if (preparsedElement.type === PreparsedElementType.NG_CONTENT) {
|
if (preparsedElement.type === PreparsedElementType.NG_CONTENT) {
|
||||||
parsedElement = new NgContentAst(elementNgContentIndex, element.sourceInfo);
|
parsedElement =
|
||||||
|
new NgContentAst(this.ngContentCount++, elementNgContentIndex, element.sourceInfo);
|
||||||
} else if (isTemplateElement) {
|
} else if (isTemplateElement) {
|
||||||
this._assertNoComponentsNorElementBindingsOnTemplate(directives, elementProps, events,
|
this._assertNoComponentsNorElementBindingsOnTemplate(directives, elementProps, events,
|
||||||
element.sourceInfo);
|
element.sourceInfo);
|
||||||
|
|
|
@ -72,14 +72,14 @@ export function text(value: string, isBound: boolean, ngContentIndex: number): T
|
||||||
|
|
||||||
export class NgContentCmd implements TemplateCmd, RenderNgContentCmd {
|
export class NgContentCmd implements TemplateCmd, RenderNgContentCmd {
|
||||||
isBound: boolean = false;
|
isBound: boolean = false;
|
||||||
constructor(public ngContentIndex: number) {}
|
constructor(public index: number, public ngContentIndex: number) {}
|
||||||
visit(visitor: RenderCommandVisitor, context: any): any {
|
visit(visitor: RenderCommandVisitor, context: any): any {
|
||||||
return visitor.visitNgContent(this, context);
|
return visitor.visitNgContent(this, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ngContent(ngContentIndex: number): NgContentCmd {
|
export function ngContent(index: number, ngContentIndex: number): NgContentCmd {
|
||||||
return new NgContentCmd(ngContentIndex);
|
return new NgContentCmd(index, ngContentIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBeginElementCmd extends TemplateCmd, RenderBeginElementCmd {
|
export interface IBeginElementCmd extends TemplateCmd, RenderBeginElementCmd {
|
||||||
|
|
|
@ -85,7 +85,13 @@ export interface RenderBeginCmd extends RenderTemplateCmd {
|
||||||
|
|
||||||
export interface RenderTextCmd extends RenderBeginCmd { value: string; }
|
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 {
|
export interface RenderBeginElementCmd extends RenderBeginCmd {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
@ -13,58 +13,20 @@ import {DefaultRenderView, DefaultRenderFragmentRef} from './view';
|
||||||
|
|
||||||
export function createRenderView(fragmentCmds: RenderTemplateCmd[], inplaceElement: any,
|
export function createRenderView(fragmentCmds: RenderTemplateCmd[], inplaceElement: any,
|
||||||
nodeFactory: NodeFactory<any>): DefaultRenderView<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 view: DefaultRenderView<any>;
|
||||||
var eventDispatcher = (boundElementIndex: number, eventName: string, event: any) =>
|
var eventDispatcher = (boundElementIndex: number, eventName: string, event: any) =>
|
||||||
view.dispatchRenderEvent(boundElementIndex, eventName, event);
|
view.dispatchRenderEvent(boundElementIndex, eventName, event);
|
||||||
var globalEventAdders: Function[] = [];
|
var context = new BuildContext(eventDispatcher, nodeFactory, inplaceElement);
|
||||||
|
context.build(fragmentCmds);
|
||||||
for (var i = 0; i < builders.length; i++) {
|
var fragments: DefaultRenderFragmentRef<any>[] = [];
|
||||||
var builder = builders[i];
|
for (var i = 0; i < context.fragments.length; i++) {
|
||||||
addAll(builder.boundElements, boundElements);
|
fragments.push(new DefaultRenderFragmentRef(context.fragments[i]));
|
||||||
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++) {
|
view = new DefaultRenderView<any>(fragments, context.boundTextNodes, context.boundElements,
|
||||||
var eventData = builder.eventData[j];
|
context.nativeShadowRoots, context.globalEventAdders);
|
||||||
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;
|
|
||||||
}
|
|
||||||
view = new DefaultRenderView<any>(fragments, boundTextNodes, boundElements, nativeShadowRoots,
|
|
||||||
globalEventAdders);
|
|
||||||
return view;
|
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> {
|
export interface NodeFactory<N> {
|
||||||
resolveComponentTemplate(templateId: number): RenderTemplateCmd[];
|
resolveComponentTemplate(templateId: number): RenderTemplateCmd[];
|
||||||
createTemplateAnchor(attrNameAndValues: string[]): N;
|
createTemplateAnchor(attrNameAndValues: string[]): N;
|
||||||
|
@ -77,96 +39,156 @@ export interface NodeFactory<N> {
|
||||||
globalOn(target: string, eventName: string, callback: Function): Function;
|
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 {
|
class RenderViewBuilder<N> implements RenderCommandVisitor {
|
||||||
parentStack: Array<N | Component<N>>;
|
parentStack: Array<N | Component<N>>;
|
||||||
boundTextNodes: N[] = [];
|
|
||||||
boundElements: N[] = [];
|
|
||||||
eventData: any[][] = [];
|
|
||||||
|
|
||||||
fragmentRootNodes: N[] = [];
|
constructor(public parentComponent: Component<N>, public fragmentRootNodes: N[],
|
||||||
nativeShadowRoots: N[] = [];
|
public commands: RenderTemplateCmd[]) {
|
||||||
|
var rootNodesParent = isPresent(fragmentRootNodes) ? null : parentComponent.shadowRoot;
|
||||||
constructor(public parentComponent: Component<N>, public rootNodesParent: N,
|
|
||||||
public inplaceElement: N, public allBuilders: RenderViewBuilder<N>[],
|
|
||||||
public factory: NodeFactory<N>) {
|
|
||||||
this.parentStack = [rootNodesParent];
|
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]; }
|
get parent(): N | Component<N> { return this.parentStack[this.parentStack.length - 1]; }
|
||||||
|
|
||||||
visitText(cmd: RenderTextCmd, context: any): any {
|
visitText(cmd: RenderTextCmd, context: BuildContext<N>): any {
|
||||||
var text = this.factory.createText(cmd.value);
|
var text = context.factory.createText(cmd.value);
|
||||||
this._addChild(text, cmd.ngContentIndex);
|
this._addChild(text, cmd.ngContentIndex, context);
|
||||||
if (cmd.isBound) {
|
if (cmd.isBound) {
|
||||||
this.boundTextNodes.push(text);
|
context.boundTextNodes.push(text);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
visitNgContent(cmd: RenderNgContentCmd, context: any): any {
|
visitNgContent(cmd: RenderNgContentCmd, context: BuildContext<N>): any {
|
||||||
if (isPresent(this.parentComponent)) {
|
if (isPresent(this.parentComponent)) {
|
||||||
var projectedNodes = this.parentComponent.project();
|
var projectedNodes = this.parentComponent.project(cmd.index);
|
||||||
for (var i = 0; i < projectedNodes.length; i++) {
|
for (var i = 0; i < projectedNodes.length; i++) {
|
||||||
var node = projectedNodes[i];
|
var node = projectedNodes[i];
|
||||||
this._addChild(node, cmd.ngContentIndex);
|
this._addChild(node, cmd.ngContentIndex, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
visitBeginElement(cmd: RenderBeginElementCmd, context: any): any {
|
visitBeginElement(cmd: RenderBeginElementCmd, context: BuildContext<N>): any {
|
||||||
this.parentStack.push(this._beginElement(cmd));
|
this.parentStack.push(this._beginElement(cmd, context));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
visitEndElement(context: any): any {
|
visitEndElement(context: BuildContext<N>): any {
|
||||||
this._endElement();
|
this._endElement();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
visitBeginComponent(cmd: RenderBeginComponentCmd, context: any): any {
|
visitBeginComponent(cmd: RenderBeginComponentCmd, context: BuildContext<N>): any {
|
||||||
var el = this._beginElement(cmd);
|
var el = this._beginElement(cmd, context);
|
||||||
var root = el;
|
var root = el;
|
||||||
if (cmd.nativeShadow) {
|
if (cmd.nativeShadow) {
|
||||||
root = this.factory.createShadowRoot(el, cmd.templateId);
|
root = context.factory.createShadowRoot(el, cmd.templateId);
|
||||||
this.nativeShadowRoots.push(root);
|
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);
|
this.parentStack.push(component);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
visitEndComponent(context: any): any {
|
visitEndComponent(context: BuildContext<N>): any {
|
||||||
var c = <Component<N>>this.parent;
|
|
||||||
c.build();
|
|
||||||
this._endElement();
|
this._endElement();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
visitEmbeddedTemplate(cmd: RenderEmbeddedTemplateCmd, context: any): any {
|
visitEmbeddedTemplate(cmd: RenderEmbeddedTemplateCmd, context: BuildContext<N>): any {
|
||||||
var el = this.factory.createTemplateAnchor(cmd.attrNameAndValues);
|
var el = context.factory.createTemplateAnchor(cmd.attrNameAndValues);
|
||||||
this._addChild(el, cmd.ngContentIndex);
|
this._addChild(el, cmd.ngContentIndex, context);
|
||||||
this.boundElements.push(el);
|
context.boundElements.push(el);
|
||||||
if (cmd.isMerged) {
|
if (cmd.isMerged) {
|
||||||
visitAll(
|
context.enqueueFragmentBuilder(this.parentComponent, cmd.children);
|
||||||
new RenderViewBuilder(this.parentComponent, null, null, this.allBuilders, this.factory),
|
|
||||||
cmd.children);
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _beginElement(cmd: RenderBeginElementCmd): N {
|
private _beginElement(cmd: RenderBeginElementCmd, context: BuildContext<N>): N {
|
||||||
var el: N;
|
var el: N = context.consumeInplaceElement();
|
||||||
if (isPresent(this.inplaceElement)) {
|
if (isPresent(el)) {
|
||||||
el = this.inplaceElement;
|
context.factory.mergeElement(el, cmd.attrNameAndValues);
|
||||||
this.inplaceElement = null;
|
|
||||||
this.factory.mergeElement(el, cmd.attrNameAndValues);
|
|
||||||
this.fragmentRootNodes.push(el);
|
this.fragmentRootNodes.push(el);
|
||||||
} else {
|
} else {
|
||||||
el = this.factory.createElement(cmd.name, cmd.attrNameAndValues);
|
el = context.factory.createElement(cmd.name, cmd.attrNameAndValues);
|
||||||
this._addChild(el, cmd.ngContentIndex);
|
this._addChild(el, cmd.ngContentIndex, context);
|
||||||
}
|
}
|
||||||
if (cmd.isBound) {
|
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) {
|
for (var i = 0; i < cmd.eventTargetAndNames.length; i += 2) {
|
||||||
var target = cmd.eventTargetAndNames[i];
|
var target = cmd.eventTargetAndNames[i];
|
||||||
var eventName = cmd.eventTargetAndNames[i + 1];
|
var eventName = cmd.eventTargetAndNames[i + 1];
|
||||||
this.eventData.push([this.boundElements.length - 1, target, eventName]);
|
context.addEventListener(boundElementIndex, target, eventName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return el;
|
return el;
|
||||||
|
@ -174,13 +196,13 @@ class RenderViewBuilder<N> implements RenderCommandVisitor {
|
||||||
|
|
||||||
private _endElement() { this.parentStack.pop(); }
|
private _endElement() { this.parentStack.pop(); }
|
||||||
|
|
||||||
private _addChild(node: N, ngContentIndex: number) {
|
private _addChild(node: N, ngContentIndex: number, context: BuildContext<N>) {
|
||||||
var parent = this.parent;
|
var parent = this.parent;
|
||||||
if (isPresent(parent)) {
|
if (isPresent(parent)) {
|
||||||
if (parent instanceof Component) {
|
if (parent instanceof Component) {
|
||||||
parent.addContentNode(ngContentIndex, node);
|
parent.addContentNode(ngContentIndex, node, context);
|
||||||
} else {
|
} else {
|
||||||
this.factory.appendChild(<N>parent, node);
|
context.factory.appendChild(<N>parent, node);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.fragmentRootNodes.push(node);
|
this.fragmentRootNodes.push(node);
|
||||||
|
@ -190,17 +212,12 @@ class RenderViewBuilder<N> implements RenderCommandVisitor {
|
||||||
|
|
||||||
class Component<N> {
|
class Component<N> {
|
||||||
private contentNodesByNgContentIndex: N[][] = [];
|
private contentNodesByNgContentIndex: N[][] = [];
|
||||||
private projectingNgContentIndex: number = 0;
|
|
||||||
private viewBuilder: RenderViewBuilder<N>;
|
|
||||||
|
|
||||||
constructor(public hostElement: N, shadowRoot: N, public cmd: RenderBeginComponentCmd,
|
constructor(public hostElement: N, public shadowRoot: N, public cmd: RenderBeginComponentCmd) {}
|
||||||
public factory: NodeFactory<N>, allBuilders: RenderViewBuilder<N>[]) {
|
addContentNode(ngContentIndex: number, node: N, context: BuildContext<N>) {
|
||||||
this.viewBuilder = new RenderViewBuilder(this, shadowRoot, null, allBuilders, factory);
|
|
||||||
}
|
|
||||||
addContentNode(ngContentIndex: number, node: N) {
|
|
||||||
if (isBlank(ngContentIndex)) {
|
if (isBlank(ngContentIndex)) {
|
||||||
if (this.cmd.nativeShadow) {
|
if (this.cmd.nativeShadow) {
|
||||||
this.factory.appendChild(this.hostElement, node);
|
context.factory.appendChild(this.hostElement, node);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
while (this.contentNodesByNgContentIndex.length <= ngContentIndex) {
|
while (this.contentNodesByNgContentIndex.length <= ngContentIndex) {
|
||||||
|
@ -209,15 +226,11 @@ class Component<N> {
|
||||||
this.contentNodesByNgContentIndex[ngContentIndex].push(node);
|
this.contentNodesByNgContentIndex[ngContentIndex].push(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
project(): N[] {
|
project(ngContentIndex: number): N[] {
|
||||||
var ngContentIndex = this.projectingNgContentIndex++;
|
|
||||||
return ngContentIndex < this.contentNodesByNgContentIndex.length ?
|
return ngContentIndex < this.contentNodesByNgContentIndex.length ?
|
||||||
this.contentNodesByNgContentIndex[ngContentIndex] :
|
this.contentNodesByNgContentIndex[ngContentIndex] :
|
||||||
[];
|
[];
|
||||||
}
|
}
|
||||||
build() {
|
|
||||||
visitAll(this.viewBuilder, this.factory.resolveComponentTemplate(this.cmd.templateId));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addAll(source: any[], target: any[]) {
|
function addAll(source: any[], target: any[]) {
|
||||||
|
@ -225,9 +238,3 @@ function addAll(source: any[], target: any[]) {
|
||||||
target.push(source[i]);
|
target.push(source[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function visitAll(visitor: RenderCommandVisitor, fragmentCmds: RenderTemplateCmd[]) {
|
|
||||||
for (var i = 0; i < fragmentCmds.length; i++) {
|
|
||||||
fragmentCmds[i].visit(visitor, null);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -30,7 +30,7 @@ export class WebWorkerTextCmd implements RenderTextCmd {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WebWorkerNgContentCmd implements RenderNgContentCmd {
|
export class WebWorkerNgContentCmd implements RenderNgContentCmd {
|
||||||
constructor(public ngContentIndex: number) {}
|
constructor(public index: number, public ngContentIndex: number) {}
|
||||||
visit(visitor: RenderCommandVisitor, context: any): any {
|
visit(visitor: RenderCommandVisitor, context: any): any {
|
||||||
return visitor.visitNgContent(this, context);
|
return visitor.visitNgContent(this, context);
|
||||||
}
|
}
|
||||||
|
|
|
@ -163,7 +163,7 @@ class RenderTemplateCmdSerializer implements RenderCommandVisitor {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
visitNgContent(cmd: RenderNgContentCmd, context: any): any {
|
visitNgContent(cmd: RenderNgContentCmd, context: any): any {
|
||||||
return {'deserializerIndex': 1, 'ngContentIndex': cmd.ngContentIndex};
|
return {'deserializerIndex': 1, 'index': cmd.index, 'ngContentIndex': cmd.ngContentIndex};
|
||||||
}
|
}
|
||||||
visitBeginElement(cmd: RenderBeginElementCmd, context: any): any {
|
visitBeginElement(cmd: RenderBeginElementCmd, context: any): any {
|
||||||
return {
|
return {
|
||||||
|
@ -209,7 +209,7 @@ var RENDER_TEMPLATE_CMD_SERIALIZER = new RenderTemplateCmdSerializer();
|
||||||
var RENDER_TEMPLATE_CMD_DESERIALIZERS = [
|
var RENDER_TEMPLATE_CMD_DESERIALIZERS = [
|
||||||
(data: {[key: string]: any}) =>
|
(data: {[key: string]: any}) =>
|
||||||
new WebWorkerTextCmd(data['isBound'], data['ngContentIndex'], data['value']),
|
new WebWorkerTextCmd(data['isBound'], data['ngContentIndex'], data['value']),
|
||||||
(data: {[key: string]: any}) => new WebWorkerNgContentCmd(data['ngContentIndex']),
|
(data: {[key: string]: any}) => new WebWorkerNgContentCmd(data['index'], data['ngContentIndex']),
|
||||||
(data: {[key: string]: any}) =>
|
(data: {[key: string]: any}) =>
|
||||||
new WebWorkerBeginElementCmd(data['isBound'], data['ngContentIndex'], data['name'],
|
new WebWorkerBeginElementCmd(data['isBound'], data['ngContentIndex'], data['name'],
|
||||||
data['attrNameAndValues'], data['eventTargetAndNames']),
|
data['attrNameAndValues'], data['eventTargetAndNames']),
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {
|
||||||
} from 'angular2/test_lib';
|
} from 'angular2/test_lib';
|
||||||
|
|
||||||
import {DOM} from 'angular2/src/core/dom/dom_adapter';
|
import {DOM} from 'angular2/src/core/dom/dom_adapter';
|
||||||
|
import {AppViewListener} from 'angular2/src/core/linker/view_listener';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
bind,
|
bind,
|
||||||
|
@ -38,6 +39,8 @@ import {By} from 'angular2/src/core/debug';
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
describe('projection', () => {
|
describe('projection', () => {
|
||||||
|
beforeEachBindings(() => [bind(AppViewListener).toClass(AppViewListener)]);
|
||||||
|
|
||||||
it('should support simple components',
|
it('should support simple components',
|
||||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||||
tcb.overrideView(MainComp, new ViewMetadata({
|
tcb.overrideView(MainComp, new ViewMetadata({
|
||||||
|
@ -464,6 +467,38 @@ export function main() {
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should allow to switch the order of nested components via ng-content',
|
||||||
|
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||||
|
tcb.overrideView(
|
||||||
|
MainComp,
|
||||||
|
new ViewMetadata(
|
||||||
|
{template: `<cmp-a><cmp-b></cmp-b></cmp-a>`, directives: [CmpA, CmpB]}))
|
||||||
|
.createAsync(MainComp)
|
||||||
|
.then((main) => {
|
||||||
|
main.detectChanges();
|
||||||
|
expect(DOM.getInnerHTML(main.debugElement.nativeElement))
|
||||||
|
.toEqual(
|
||||||
|
'<cmp-a><cmp-b><cmp-d><d>cmp-d</d></cmp-d></cmp-b><cmp-c><c>cmp-c</c></cmp-c></cmp-a>');
|
||||||
|
async.done();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should create nested components in the right order',
|
||||||
|
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||||
|
tcb.overrideView(
|
||||||
|
MainComp,
|
||||||
|
new ViewMetadata(
|
||||||
|
{template: `<cmp-a1></cmp-a1><cmp-a2></cmp-a2>`, directives: [CmpA1, CmpA2]}))
|
||||||
|
.createAsync(MainComp)
|
||||||
|
.then((main) => {
|
||||||
|
main.detectChanges();
|
||||||
|
expect(DOM.getInnerHTML(main.debugElement.nativeElement))
|
||||||
|
.toEqual(
|
||||||
|
'<cmp-a1>a1<cmp-b11>b11</cmp-b11><cmp-b12>b12</cmp-b12></cmp-a1><cmp-a2>a2<cmp-b21>b21</cmp-b21><cmp-b22>b22</cmp-b22></cmp-a2>');
|
||||||
|
async.done();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -600,3 +635,65 @@ class Tab {
|
||||||
class Tree {
|
class Tree {
|
||||||
depth = 0;
|
depth = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Component({selector: 'cmp-d'})
|
||||||
|
@View({template: `<d>{{tagName}}</d>`})
|
||||||
|
class CmpD {
|
||||||
|
tagName: string;
|
||||||
|
constructor(elementRef: ElementRef) {
|
||||||
|
this.tagName = DOM.tagName(elementRef.nativeElement).toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Component({selector: 'cmp-c'})
|
||||||
|
@View({template: `<c>{{tagName}}</c>`})
|
||||||
|
class CmpC {
|
||||||
|
tagName: string;
|
||||||
|
constructor(elementRef: ElementRef) {
|
||||||
|
this.tagName = DOM.tagName(elementRef.nativeElement).toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Component({selector: 'cmp-b'})
|
||||||
|
@View({template: `<ng-content></ng-content><cmp-d></cmp-d>`, directives: [CmpD]})
|
||||||
|
class CmpB {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Component({selector: 'cmp-a'})
|
||||||
|
@View({template: `<ng-content></ng-content><cmp-c></cmp-c>`, directives: [CmpC]})
|
||||||
|
class CmpA {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({selector: 'cmp-b11'})
|
||||||
|
@View({template: `{{'b11'}}`, directives: []})
|
||||||
|
class CmpB11 {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({selector: 'cmp-b12'})
|
||||||
|
@View({template: `{{'b12'}}`, directives: []})
|
||||||
|
class CmpB12 {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({selector: 'cmp-b21'})
|
||||||
|
@View({template: `{{'b21'}}`, directives: []})
|
||||||
|
class CmpB21 {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({selector: 'cmp-b22'})
|
||||||
|
@View({template: `{{'b22'}}`, directives: []})
|
||||||
|
class CmpB22 {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({selector: 'cmp-a1'})
|
||||||
|
@View({template: `{{'a1'}}<cmp-b11></cmp-b11><cmp-b12></cmp-b12>`, directives: [CmpB11, CmpB12]})
|
||||||
|
class CmpA1 {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({selector: 'cmp-a2'})
|
||||||
|
@View({template: `{{'a2'}}<cmp-b21></cmp-b21><cmp-b22></cmp-b22>`, directives: [CmpB21, CmpB22]})
|
||||||
|
class CmpA2 {
|
||||||
|
}
|
||||||
|
|
|
@ -49,8 +49,8 @@ function endComponent() {
|
||||||
return appCmds.endComponent();
|
return appCmds.endComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
function ngContent(ngContentIndex: number) {
|
function ngContent(index: number, ngContentIndex: number) {
|
||||||
return appCmds.ngContent(ngContentIndex);
|
return appCmds.ngContent(index, ngContentIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
|
@ -369,6 +369,50 @@ export function main() {
|
||||||
expect(mapAttrs(view.boundElements, 'id')).toEqual(['1.1', '1.2', '2.1', '3.1']);
|
expect(mapAttrs(view.boundElements, 'id')).toEqual(['1.1', '1.2', '2.1', '3.1']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should process nested components in depth first order', () => {
|
||||||
|
componentTemplates.set(0, [
|
||||||
|
beginComponent('b11-comp', ['id', '2.1'], [], false, null, 2),
|
||||||
|
endComponent(),
|
||||||
|
beginComponent('b12-comp', ['id', '2.2'], [], false, null, 3),
|
||||||
|
endComponent(),
|
||||||
|
]);
|
||||||
|
componentTemplates.set(1, [
|
||||||
|
beginComponent('b21-comp', ['id', '3.1'], [], false, null, 4),
|
||||||
|
endComponent(),
|
||||||
|
beginComponent('b22-comp', ['id', '3.2'], [], false, null, 5),
|
||||||
|
endComponent(),
|
||||||
|
]);
|
||||||
|
componentTemplates.set(2, [
|
||||||
|
beginElement('b11', ['id', '4.11'], [], true, null),
|
||||||
|
endElement(),
|
||||||
|
]);
|
||||||
|
componentTemplates.set(3, [
|
||||||
|
beginElement('b12', ['id', '4.12'], [], true, null),
|
||||||
|
endElement(),
|
||||||
|
]);
|
||||||
|
componentTemplates.set(4, [
|
||||||
|
beginElement('b21', ['id', '4.21'], [], true, null),
|
||||||
|
endElement(),
|
||||||
|
]);
|
||||||
|
componentTemplates.set(5, [
|
||||||
|
beginElement('b22', ['id', '4.22'], [], true, null),
|
||||||
|
endElement(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
var view = createRenderView(
|
||||||
|
[
|
||||||
|
beginComponent('a1-comp', ['id', '1.1'], [], false, null, 0),
|
||||||
|
endComponent(),
|
||||||
|
beginComponent('a2-comp', ['id', '1.2'], [], false, null, 1),
|
||||||
|
endComponent(),
|
||||||
|
],
|
||||||
|
null, nodeFactory);
|
||||||
|
|
||||||
|
expect(mapAttrs(view.boundElements, 'id'))
|
||||||
|
.toEqual(['1.1', '1.2', '2.1', '2.2', '4.11', '4.12', '3.1', '3.2', '4.21', '4.22']);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should store bound text nodes after the bound text nodes of the main template', () => {
|
it('should store bound text nodes after the bound text nodes of the main template', () => {
|
||||||
componentTemplates.set(0, [
|
componentTemplates.set(0, [
|
||||||
text('2.1', true, null),
|
text('2.1', true, null),
|
||||||
|
@ -442,9 +486,9 @@ export function main() {
|
||||||
it('should project commands based on their ngContentIndex', () => {
|
it('should project commands based on their ngContentIndex', () => {
|
||||||
componentTemplates.set(0, [
|
componentTemplates.set(0, [
|
||||||
text('(', false, null),
|
text('(', false, null),
|
||||||
ngContent(null),
|
ngContent(0, null),
|
||||||
text(',', false, null),
|
text(',', false, null),
|
||||||
ngContent(null),
|
ngContent(1, null),
|
||||||
text(')', false, null)
|
text(')', false, null)
|
||||||
]);
|
]);
|
||||||
var view = createRenderView(
|
var view = createRenderView(
|
||||||
|
@ -460,9 +504,9 @@ export function main() {
|
||||||
|
|
||||||
it('should reproject nodes over multiple ng-content commands', () => {
|
it('should reproject nodes over multiple ng-content commands', () => {
|
||||||
componentTemplates.set(
|
componentTemplates.set(
|
||||||
0, [beginComponent('b-comp', [], [], false, null, 1), ngContent(0), endComponent()]);
|
0, [beginComponent('b-comp', [], [], false, null, 1), ngContent(0, 0), endComponent()]);
|
||||||
componentTemplates.set(1,
|
componentTemplates.set(
|
||||||
[text('(', false, null), ngContent(null), text(')', false, null)]);
|
1, [text('(', false, null), ngContent(0, null), text(')', false, null)]);
|
||||||
var view = createRenderView(
|
var view = createRenderView(
|
||||||
[
|
[
|
||||||
beginComponent('a-comp', [], [], false, null, 0),
|
beginComponent('a-comp', [], [], false, null, 0),
|
||||||
|
|
|
@ -1106,6 +1106,8 @@ var NG_API = [
|
||||||
'{RenderEmbeddedTemplateCmd}.isMerged',
|
'{RenderEmbeddedTemplateCmd}.isMerged',
|
||||||
'{RenderEmbeddedTemplateCmd}.isMerged=',
|
'{RenderEmbeddedTemplateCmd}.isMerged=',
|
||||||
'{RenderNgContentCmd}',
|
'{RenderNgContentCmd}',
|
||||||
|
'{RenderNgContentCmd}.index',
|
||||||
|
'{RenderNgContentCmd}.index=',
|
||||||
'{RenderNgContentCmd}.ngContentIndex',
|
'{RenderNgContentCmd}.ngContentIndex',
|
||||||
'{RenderNgContentCmd}.ngContentIndex=',
|
'{RenderNgContentCmd}.ngContentIndex=',
|
||||||
'{RenderTemplateCmd}',
|
'{RenderTemplateCmd}',
|
||||||
|
|
Loading…
Reference in New Issue