feat(core): view engine - support content projection (#14209)

Part of #14013

PR Close #14209
This commit is contained in:
Tobias Bosch
2017-01-31 08:51:42 -08:00
committed by Miško Hevery
parent 86b2b2504f
commit 0a29574d98
24 changed files with 545 additions and 285 deletions

View File

@ -13,8 +13,8 @@ import {BindingDef, BindingType, DebugContext, DisposableFn, ElementData, Elemen
import {checkAndUpdateBinding, entryAction, setBindingDebugInfo, setCurrentNode, sliceErrorStack} from './util';
export function anchorDef(
flags: NodeFlags, matchedQueries: [string, QueryValueType][], childCount: number,
template?: ViewDefinition): NodeDef {
flags: NodeFlags, matchedQueries: [string, QueryValueType][], ngContentIndex: number,
childCount: number, template?: ViewDefinition): NodeDef {
const matchedQueryDefs: {[queryId: string]: QueryValueType} = {};
if (matchedQueries) {
matchedQueries.forEach(([queryId, valueType]) => { matchedQueryDefs[queryId] = valueType; });
@ -33,7 +33,7 @@ export function anchorDef(
disposableIndex: undefined,
// regular values
flags,
matchedQueries: matchedQueryDefs, childCount,
matchedQueries: matchedQueryDefs, ngContentIndex, childCount,
bindings: [],
disposableCount: 0,
element: {
@ -41,18 +41,19 @@ export function anchorDef(
attrs: undefined,
outputs: [], template,
// will bet set by the view definition
providerIndices: undefined, source
providerIndices: undefined, source,
},
provider: undefined,
text: undefined,
pureExpression: undefined,
query: undefined,
ngContent: undefined
};
}
export function elementDef(
flags: NodeFlags, matchedQueries: [string, QueryValueType][], childCount: number, name: string,
fixedAttrs: {[name: string]: string} = {},
flags: NodeFlags, matchedQueries: [string, QueryValueType][], ngContentIndex: number,
childCount: number, name: string, fixedAttrs: {[name: string]: string} = {},
bindings?:
([BindingType.ElementClass, string] | [BindingType.ElementStyle, string, string] |
[BindingType.ElementAttribute | BindingType.ElementProperty, string, SecurityContext])[],
@ -108,7 +109,7 @@ export function elementDef(
disposableIndex: undefined,
// regular values
flags,
matchedQueries: matchedQueryDefs, childCount,
matchedQueries: matchedQueryDefs, ngContentIndex, childCount,
bindings: bindingDefs,
disposableCount: outputDefs.length,
element: {
@ -117,12 +118,13 @@ export function elementDef(
outputs: outputDefs,
template: undefined,
// will bet set by the view definition
providerIndices: undefined, source
providerIndices: undefined, source,
},
provider: undefined,
text: undefined,
pureExpression: undefined,
query: undefined,
ngContent: undefined
};
}

View File

@ -7,13 +7,14 @@
*/
export {anchorDef, elementDef} from './element';
export {ngContentDef} from './ng_content';
export {providerDef} from './provider';
export {pureArrayDef, pureObjectDef, purePipeDef} from './pure_expression';
export {queryDef} from './query';
export {textDef} from './text';
export {setCurrentNode} from './util';
export {rootRenderNodes, setCurrentNode} from './util';
export {checkAndUpdateView, checkNoChangesView, checkNodeDynamic, checkNodeInline, createEmbeddedView, createRootView, destroyView, viewDef} from './view';
export {attachEmbeddedView, detachEmbeddedView, rootRenderNodes} from './view_attach';
export {attachEmbeddedView, detachEmbeddedView} from './view_attach';
export * from './types';
export {DefaultServices} from './services';

View File

@ -0,0 +1,58 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {NodeDef, NodeType, ViewData, asElementData} from './types';
import {RenderNodeAction, visitProjectedRenderNodes} from './util';
export function ngContentDef(ngContentIndex: number, index: number): NodeDef {
return {
type: NodeType.NgContent,
// will bet set by the view definition
index: undefined,
reverseChildIndex: undefined,
parent: undefined,
childFlags: undefined,
childMatchedQueries: undefined,
bindingIndex: undefined,
disposableIndex: undefined,
// regular values
flags: 0,
matchedQueries: {}, ngContentIndex,
childCount: 0,
bindings: [],
disposableCount: 0,
element: undefined,
provider: undefined,
text: undefined,
pureExpression: undefined,
query: undefined,
ngContent: {index}
};
}
export function appendNgContent(view: ViewData, renderHost: any, def: NodeDef) {
if (def.ngContentIndex != null) {
// Do nothing if we are reprojected!
return;
}
const parentEl = def.parent != null ? asElementData(view, def.parent).renderElement : renderHost;
if (!parentEl) {
// Nothing to do if there is no parent element.
return;
}
const ngContentIndex = def.ngContent.index;
if (view.renderer) {
const projectedNodes: any[] = [];
visitProjectedRenderNodes(
view, ngContentIndex, RenderNodeAction.Collect, undefined, undefined, projectedNodes);
view.renderer.projectNodes(parentEl, projectedNodes);
} else {
visitProjectedRenderNodes(
view, ngContentIndex, RenderNodeAction.AppendChild, parentEl, undefined, undefined);
}
}

View File

@ -80,7 +80,8 @@ export function providerDef(
disposableIndex: undefined,
// regular values
flags,
matchedQueries: matchedQueryDefs, childCount, bindings,
matchedQueries: matchedQueryDefs,
ngContentIndex: undefined, childCount, bindings,
disposableCount: outputDefs.length,
element: undefined,
provider: {
@ -91,7 +92,8 @@ export function providerDef(
},
text: undefined,
pureExpression: undefined,
query: undefined
query: undefined,
ngContent: undefined
};
}

View File

@ -50,6 +50,7 @@ function _pureExpressionDef(
// regular values
flags: 0,
matchedQueries: {},
ngContentIndex: undefined,
childCount: 0, bindings,
disposableCount: 0,
element: undefined,
@ -57,6 +58,7 @@ function _pureExpressionDef(
text: undefined,
pureExpression: {type, pipeDep},
query: undefined,
ngContent: undefined
};
}

View File

@ -34,6 +34,7 @@ export function queryDef(
disposableIndex: undefined,
// regular values
flags,
ngContentIndex: undefined,
matchedQueries: {},
childCount: 0,
bindings: [],
@ -42,7 +43,8 @@ export function queryDef(
provider: undefined,
text: undefined,
pureExpression: undefined,
query: {id, bindings: bindingDefs}
query: {id, bindings: bindingDefs},
ngContent: undefined
};
}

View File

@ -19,9 +19,9 @@ import {Sanitizer, SecurityContext} from '../security';
import {createInjector} from './provider';
import {getQueryValue} from './query';
import {DebugContext, ElementData, NodeData, NodeDef, NodeType, Services, ViewData, ViewDefinition, asElementData} from './types';
import {isComponentView, renderNode} from './util';
import {isComponentView, renderNode, rootRenderNodes} from './util';
import {checkAndUpdateView, checkNoChangesView, createEmbeddedView, destroyView} from './view';
import {attachEmbeddedView, detachEmbeddedView, rootRenderNodes} from './view_attach';
import {attachEmbeddedView, detachEmbeddedView} from './view_attach';
@Injectable()
export class DefaultServices implements Services {
@ -140,6 +140,10 @@ class DebugContext_ implements DebugContext {
private nodeDef: NodeDef;
private elDef: NodeDef;
constructor(public view: ViewData, public nodeIndex: number) {
if (nodeIndex == null) {
this.nodeIndex = nodeIndex = view.parentIndex;
this.view = view = view.parent;
}
this.nodeDef = view.def.nodes[nodeIndex];
this.elDef = findElementDef(view, nodeIndex);
}

View File

@ -12,7 +12,7 @@ import {looseIdentical} from '../facade/lang';
import {BindingDef, BindingType, DebugContext, NodeData, NodeDef, NodeFlags, NodeType, Services, TextData, ViewData, ViewFlags, asElementData, asTextData} from './types';
import {checkAndUpdateBinding, sliceErrorStack} from './util';
export function textDef(constants: string[]): NodeDef {
export function textDef(ngContentIndex: number, constants: string[]): NodeDef {
// skip the call to sliceErrorStack itself + the call to this function.
const source = isDevMode() ? sliceErrorStack(2, 3) : '';
const bindings: BindingDef[] = new Array(constants.length - 1);
@ -37,7 +37,7 @@ export function textDef(constants: string[]): NodeDef {
disposableIndex: undefined,
// regular values
flags: 0,
matchedQueries: {},
matchedQueries: {}, ngContentIndex,
childCount: 0, bindings,
disposableCount: 0,
element: undefined,
@ -45,6 +45,7 @@ export function textDef(constants: string[]): NodeDef {
text: {prefix: constants[0], source},
pureExpression: undefined,
query: undefined,
ngContent: undefined
};
}

View File

@ -24,14 +24,14 @@ export interface ViewDefinition {
handleEvent: ViewHandleEventFn;
/**
* Order: Depth first.
* Especially providers are before elements / anchros.
* Especially providers are before elements / anchors.
*/
nodes: NodeDef[];
/** aggregated NodeFlags for all nodes **/
nodeFlags: NodeFlags;
/**
* Order: parents before children, but children in reverse order.
* Especially providers are after elements / anchros.
* Especially providers are after elements / anchors.
*/
reverseChildNodes: NodeDef[];
lastRootNode: NodeDef;
@ -71,6 +71,8 @@ export interface NodeDef {
reverseChildIndex: number;
flags: NodeFlags;
parent: number;
/** this is checked against NgContentDef.index to find matched nodes */
ngContentIndex: number;
/** number of transitive children */
childCount: number;
/** aggregated NodeFlags for all children **/
@ -94,6 +96,7 @@ export interface NodeDef {
text: TextDef;
pureExpression: PureExpressionDef;
query: QueryDef;
ngContent: NgContentDef;
}
export enum NodeType {
@ -102,6 +105,7 @@ export enum NodeType {
Provider,
PureExpression,
Query,
NgContent
}
/**
@ -229,6 +233,16 @@ export enum QueryBindingType {
All
}
export interface NgContentDef {
/**
* this index is checked against NodeDef.ngContentIndex to find the nodes
* that are matched by this ng-content.
* Note that a NodeDef with an ng-content can be reprojected, i.e.
* have a ngContentIndex on its own.
*/
index: number;
}
// -------------------------------------
// Data
// -------------------------------------

View File

@ -170,3 +170,82 @@ function callWithTryCatch(fn: (a: any) => any, arg: any): any {
throw viewWrappedError(e, debugContext);
}
}
export function rootRenderNodes(view: ViewData): any[] {
const renderNodes: any[] = [];
visitRootRenderNodes(view, RenderNodeAction.Collect, undefined, undefined, renderNodes);
return renderNodes;
}
export enum RenderNodeAction {
Collect,
AppendChild,
InsertBefore,
RemoveChild
}
export function visitRootRenderNodes(
view: ViewData, action: RenderNodeAction, parentNode: any, nextSibling: any, target: any[]) {
const len = view.def.nodes.length;
for (let i = 0; i < len; i++) {
const nodeDef = view.def.nodes[i];
visitRenderNode(view, nodeDef, action, parentNode, nextSibling, target);
// jump to next sibling
i += nodeDef.childCount;
}
}
export function visitProjectedRenderNodes(
view: ViewData, ngContentIndex: number, action: RenderNodeAction, parentNode: any,
nextSibling: any, target: any[]) {
let compView = view;
while (!isComponentView(compView)) {
compView = compView.parent;
}
const hostView = compView.parent;
const hostElDef = hostView.def.nodes[compView.parentIndex];
const startIndex = hostElDef.index + 1;
const endIndex = hostElDef.index + hostElDef.childCount;
for (let i = startIndex; i <= endIndex; i++) {
const nodeDef = hostView.def.nodes[i];
if (nodeDef.ngContentIndex === ngContentIndex) {
visitRenderNode(hostView, nodeDef, action, parentNode, nextSibling, target);
}
// jump to next sibling
i += nodeDef.childCount;
}
}
function visitRenderNode(
view: ViewData, nodeDef: NodeDef, action: RenderNodeAction, parentNode: any, nextSibling: any,
target: any[]) {
if (nodeDef.type === NodeType.NgContent) {
visitProjectedRenderNodes(
view, nodeDef.ngContent.index, action, parentNode, nextSibling, target);
} else {
const rn = renderNode(view, nodeDef);
switch (action) {
case RenderNodeAction.AppendChild:
parentNode.appendChild(rn);
break;
case RenderNodeAction.InsertBefore:
parentNode.insertBefore(rn, nextSibling);
break;
case RenderNodeAction.RemoveChild:
parentNode.removeChild(rn);
break;
case RenderNodeAction.Collect:
target.push(rn);
break;
}
if (nodeDef.flags & NodeFlags.HasEmbeddedViews) {
const embeddedViews = asElementData(view, nodeDef.index).embeddedViews;
if (embeddedViews) {
for (let k = 0; k < embeddedViews.length; k++) {
visitRootRenderNodes(embeddedViews[k], action, parentNode, nextSibling, target);
}
}
}
}
}

View File

@ -11,6 +11,7 @@ import {RenderComponentType, Renderer} from '../render/api';
import {checkAndUpdateElementDynamic, checkAndUpdateElementInline, createElement} from './element';
import {expressionChangedAfterItHasBeenCheckedError} from './errors';
import {appendNgContent} from './ng_content';
import {callLifecycleHooksChildrenFirst, checkAndUpdateProviderDynamic, checkAndUpdateProviderInline, createProvider} from './provider';
import {checkAndUpdatePureExpressionDynamic, checkAndUpdatePureExpressionInline, createPureExpression} from './pure_expression';
import {checkAndUpdateQuery, createQuery, queryDef} from './query';
@ -316,6 +317,11 @@ function _createViewNodes(view: ViewData) {
case NodeType.Query:
nodeData = createQuery();
break;
case NodeType.NgContent:
appendNgContent(view, renderHost, nodeDef);
// no runtime data needed for NgContent...
nodeData = undefined;
break;
}
nodes[i] = nodeData;
}

View File

@ -7,8 +7,8 @@
*/
import {dirtyParentQuery} from './query';
import {ElementData, NodeData, NodeFlags, NodeType, ViewData, asElementData, asProviderData, asTextData} from './types';
import {declaredViewContainer, renderNode} from './util';
import {ElementData, NodeData, NodeDef, NodeFlags, NodeType, ViewData, asElementData, asProviderData, asTextData} from './types';
import {RenderNodeAction, declaredViewContainer, isComponentView, renderNode, rootRenderNodes, visitProjectedRenderNodes, visitRootRenderNodes} from './util';
export function attachEmbeddedView(elementData: ElementData, viewIndex: number, view: ViewData) {
let embeddedViews = elementData.embeddedViews;
@ -39,8 +39,8 @@ export function attachEmbeddedView(elementData: ElementData, viewIndex: number,
const parentNode = prevRenderNode.parentNode;
const nextSibling = prevRenderNode.nextSibling;
if (parentNode) {
const action = nextSibling ? DirectDomAction.InsertBefore : DirectDomAction.AppendChild;
directDomAttachDetachSiblingRenderNodes(view, 0, action, parentNode, nextSibling);
const action = nextSibling ? RenderNodeAction.InsertBefore : RenderNodeAction.AppendChild;
visitRootRenderNodes(view, action, parentNode, nextSibling, undefined);
}
}
}
@ -69,8 +69,7 @@ export function detachEmbeddedView(elementData: ElementData, viewIndex: number):
} else {
const parentNode = elementData.renderElement.parentNode;
if (parentNode) {
directDomAttachDetachSiblingRenderNodes(
view, 0, DirectDomAction.RemoveChild, parentNode, null);
visitRootRenderNodes(view, RenderNodeAction.RemoveChild, parentNode, null, undefined);
}
}
return view;
@ -93,65 +92,3 @@ function removeFromArray(arr: any[], index: number) {
arr.splice(index, 1);
}
}
export function rootRenderNodes(view: ViewData): any[] {
const renderNodes: any[] = [];
collectSiblingRenderNodes(view, 0, renderNodes);
return renderNodes;
}
function collectSiblingRenderNodes(view: ViewData, startIndex: number, target: any[]) {
const nodeCount = view.def.nodes.length;
for (let i = startIndex; i < nodeCount; i++) {
const nodeDef = view.def.nodes[i];
target.push(renderNode(view, nodeDef));
if (nodeDef.flags & NodeFlags.HasEmbeddedViews) {
const embeddedViews = asElementData(view, i).embeddedViews;
if (embeddedViews) {
for (let k = 0; k < embeddedViews.length; k++) {
collectSiblingRenderNodes(embeddedViews[k], 0, target);
}
}
}
// jump to next sibling
i += nodeDef.childCount;
}
}
enum DirectDomAction {
AppendChild,
InsertBefore,
RemoveChild
}
function directDomAttachDetachSiblingRenderNodes(
view: ViewData, startIndex: number, action: DirectDomAction, parentNode: any,
nextSibling: any) {
const nodeCount = view.def.nodes.length;
for (let i = startIndex; i < nodeCount; i++) {
const nodeDef = view.def.nodes[i];
const rn = renderNode(view, nodeDef);
switch (action) {
case DirectDomAction.AppendChild:
parentNode.appendChild(rn);
break;
case DirectDomAction.InsertBefore:
parentNode.insertBefore(rn, nextSibling);
break;
case DirectDomAction.RemoveChild:
parentNode.removeChild(rn);
break;
}
if (nodeDef.flags & NodeFlags.HasEmbeddedViews) {
const embeddedViews = asElementData(view, i).embeddedViews;
if (embeddedViews) {
for (let k = 0; k < embeddedViews.length; k++) {
directDomAttachDetachSiblingRenderNodes(
embeddedViews[k], 0, action, parentNode, nextSibling);
}
}
}
// jump to next sibling
i += nodeDef.childCount;
}
}