feat(ivy): ViewContainerRef basic scenarios support (#23021)
PR Close #23021
This commit is contained in:
parent
a4bf5621ed
commit
fa2c9a81dd
@ -19,7 +19,7 @@ import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, ViewRef as viewEngine_Vie
|
|||||||
import {Type} from '../type';
|
import {Type} from '../type';
|
||||||
|
|
||||||
import {assertLessThan, assertNotNull} from './assert';
|
import {assertLessThan, assertNotNull} from './assert';
|
||||||
import {assertPreviousIsParent, enterView, getDirectiveInstance, getPreviousOrParentNode, getRenderer, isComponent, renderEmbeddedTemplate} from './instructions';
|
import {addToViewTree, assertPreviousIsParent, createLContainer, createLNodeObject, getDirectiveInstance, getPreviousOrParentNode, getRenderer, isComponent, renderEmbeddedTemplate} from './instructions';
|
||||||
import {ComponentTemplate, DirectiveDef} from './interfaces/definition';
|
import {ComponentTemplate, DirectiveDef} from './interfaces/definition';
|
||||||
import {LInjector} from './interfaces/injector';
|
import {LInjector} from './interfaces/injector';
|
||||||
import {LContainerNode, LElementNode, LNode, LNodeType, LViewNode, TNodeFlags} from './interfaces/node';
|
import {LContainerNode, LElementNode, LNode, LNodeType, LViewNode, TNodeFlags} from './interfaces/node';
|
||||||
@ -27,7 +27,7 @@ import {QueryReadType} from './interfaces/query';
|
|||||||
import {Renderer3} from './interfaces/renderer';
|
import {Renderer3} from './interfaces/renderer';
|
||||||
import {LView} from './interfaces/view';
|
import {LView} from './interfaces/view';
|
||||||
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
|
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
|
||||||
import {insertView} from './node_manipulation';
|
import {insertView, removeView} from './node_manipulation';
|
||||||
import {notImplemented, stringify} from './util';
|
import {notImplemented, stringify} from './util';
|
||||||
import {EmbeddedViewRef, ViewRef, addDestroyable, createViewRef} from './view_ref';
|
import {EmbeddedViewRef, ViewRef, addDestroyable, createViewRef} from './view_ref';
|
||||||
|
|
||||||
@ -551,8 +551,21 @@ class ElementRef implements viewEngine_ElementRef {
|
|||||||
* @returns The ViewContainerRef instance to use
|
* @returns The ViewContainerRef instance to use
|
||||||
*/
|
*/
|
||||||
export function getOrCreateContainerRef(di: LInjector): viewEngine_ViewContainerRef {
|
export function getOrCreateContainerRef(di: LInjector): viewEngine_ViewContainerRef {
|
||||||
return di.viewContainerRef ||
|
if (!di.viewContainerRef) {
|
||||||
(di.viewContainerRef = new ViewContainerRef(di.node as LContainerNode));
|
const vcRefHost = di.node;
|
||||||
|
|
||||||
|
ngDevMode && assertNodeOfPossibleTypes(vcRefHost, LNodeType.Container, LNodeType.Element);
|
||||||
|
|
||||||
|
const lContainer = createLContainer(vcRefHost.parent !, vcRefHost.view, undefined, vcRefHost);
|
||||||
|
const lContainerNode: LContainerNode = createLNodeObject(
|
||||||
|
LNodeType.Container, vcRefHost.view, vcRefHost.parent !, undefined, lContainer, null);
|
||||||
|
|
||||||
|
addToViewTree(vcRefHost.view, lContainer);
|
||||||
|
|
||||||
|
di.viewContainerRef = new ViewContainerRef(lContainerNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return di.viewContainerRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -560,19 +573,27 @@ export function getOrCreateContainerRef(di: LInjector): viewEngine_ViewContainer
|
|||||||
* imperatively.
|
* imperatively.
|
||||||
*/
|
*/
|
||||||
class ViewContainerRef implements viewEngine_ViewContainerRef {
|
class ViewContainerRef implements viewEngine_ViewContainerRef {
|
||||||
|
private _viewRefs: viewEngine_ViewRef[] = [];
|
||||||
element: viewEngine_ElementRef;
|
element: viewEngine_ElementRef;
|
||||||
injector: Injector;
|
injector: Injector;
|
||||||
parentInjector: Injector;
|
parentInjector: Injector;
|
||||||
|
|
||||||
constructor(private _node: LContainerNode) {}
|
constructor(private _lContainerNode: LContainerNode) {}
|
||||||
|
|
||||||
clear(): void { throw notImplemented(); }
|
clear(): void {
|
||||||
get(index: number): viewEngine_ViewRef|null { throw notImplemented(); }
|
const lContainer = this._lContainerNode.data;
|
||||||
length: number;
|
while (lContainer.views.length) {
|
||||||
createEmbeddedView<C>(
|
this.remove(0);
|
||||||
templateRef: viewEngine_TemplateRef<C>, context?: C|undefined,
|
}
|
||||||
index?: number|undefined): viewEngine_EmbeddedViewRef<C> {
|
}
|
||||||
const viewRef = templateRef.createEmbeddedView(context !);
|
get(index: number): viewEngine_ViewRef|null { return this._viewRefs[index] || null; }
|
||||||
|
get length(): number {
|
||||||
|
const lContainer = this._lContainerNode.data;
|
||||||
|
return lContainer.views.length;
|
||||||
|
}
|
||||||
|
createEmbeddedView<C>(templateRef: viewEngine_TemplateRef<C>, context?: C, index?: number):
|
||||||
|
viewEngine_EmbeddedViewRef<C> {
|
||||||
|
const viewRef = templateRef.createEmbeddedView(context || <any>{});
|
||||||
this.insert(viewRef, index);
|
this.insert(viewRef, index);
|
||||||
return viewRef;
|
return viewRef;
|
||||||
}
|
}
|
||||||
@ -582,36 +603,26 @@ class ViewContainerRef implements viewEngine_ViewContainerRef {
|
|||||||
ngModule?: viewEngine_NgModuleRef<any>|undefined): viewEngine_ComponentRef<C> {
|
ngModule?: viewEngine_NgModuleRef<any>|undefined): viewEngine_ComponentRef<C> {
|
||||||
throw notImplemented();
|
throw notImplemented();
|
||||||
}
|
}
|
||||||
insert(viewRef: viewEngine_ViewRef, index?: number|undefined): viewEngine_ViewRef {
|
insert(viewRef: viewEngine_ViewRef, index?: number): viewEngine_ViewRef {
|
||||||
if (index == null) {
|
const lViewNode = (viewRef as EmbeddedViewRef<any>)._lViewNode;
|
||||||
index = this._node.data.views.length;
|
const adjustedIdx = this._adjustAndAssertIndex(index);
|
||||||
} else {
|
|
||||||
// +1 because it's legal to insert at the end.
|
|
||||||
ngDevMode && assertLessThan(index, this._node.data.views.length + 1, 'index');
|
|
||||||
}
|
|
||||||
const lView = (viewRef as EmbeddedViewRef<any>)._lViewNode;
|
|
||||||
insertView(this._node, lView, index);
|
|
||||||
|
|
||||||
// TODO(pk): this is a temporary index adjustment so imperativelly inserted (through
|
insertView(this._lContainerNode, lViewNode, adjustedIdx);
|
||||||
// ViewContainerRef) views
|
this._viewRefs.splice(adjustedIdx, 0, viewRef);
|
||||||
// are not removed in the containerRefreshEnd instruction.
|
|
||||||
// The final fix will consist of creating a dedicated container node for views inserted through
|
(lViewNode as{parent: LNode}).parent = this._lContainerNode;
|
||||||
// ViewContainerRef.
|
|
||||||
// Such container should not be trimmed as it is the case in the containerRefreshEnd
|
|
||||||
// instruction.
|
|
||||||
this._node.data.nextIndex = this._node.data.views.length;
|
|
||||||
|
|
||||||
// If the view is dynamic (has a template), it needs to be counted both at the container
|
// If the view is dynamic (has a template), it needs to be counted both at the container
|
||||||
// level and at the node above the container.
|
// level and at the node above the container.
|
||||||
if (lView.data.template !== null) {
|
if (lViewNode.data.template !== null) {
|
||||||
// Increment the container view count.
|
// Increment the container view count.
|
||||||
this._node.data.dynamicViewCount++;
|
this._lContainerNode.data.dynamicViewCount++;
|
||||||
|
|
||||||
// Look for the parent node and increment its dynamic view count.
|
// Look for the parent node and increment its dynamic view count.
|
||||||
if (this._node.parent !== null && this._node.parent.data !== null) {
|
if (this._lContainerNode.parent !== null && this._lContainerNode.parent.data !== null) {
|
||||||
ngDevMode &&
|
ngDevMode && assertNodeOfPossibleTypes(
|
||||||
assertNodeOfPossibleTypes(this._node.parent, LNodeType.View, LNodeType.Element);
|
this._lContainerNode.parent, LNodeType.View, LNodeType.Element);
|
||||||
this._node.parent.data.dynamicViewCount++;
|
this._lContainerNode.parent.data.dynamicViewCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return viewRef;
|
return viewRef;
|
||||||
@ -620,8 +631,22 @@ class ViewContainerRef implements viewEngine_ViewContainerRef {
|
|||||||
throw notImplemented();
|
throw notImplemented();
|
||||||
}
|
}
|
||||||
indexOf(viewRef: viewEngine_ViewRef): number { throw notImplemented(); }
|
indexOf(viewRef: viewEngine_ViewRef): number { throw notImplemented(); }
|
||||||
remove(index?: number|undefined): void { throw notImplemented(); }
|
remove(index?: number): void {
|
||||||
|
const adjustedIdx = this._adjustAndAssertIndex(index);
|
||||||
|
removeView(this._lContainerNode, adjustedIdx);
|
||||||
|
this._viewRefs.splice(adjustedIdx, 1);
|
||||||
|
}
|
||||||
detach(index?: number|undefined): viewEngine_ViewRef|null { throw notImplemented(); }
|
detach(index?: number|undefined): viewEngine_ViewRef|null { throw notImplemented(); }
|
||||||
|
|
||||||
|
private _adjustAndAssertIndex(index?: number|undefined) {
|
||||||
|
if (index == null) {
|
||||||
|
index = this._lContainerNode.data.views.length;
|
||||||
|
} else {
|
||||||
|
// +1 because it's legal to insert at the end.
|
||||||
|
ngDevMode && assertLessThan(index, this._lContainerNode.data.views.length + 1, 'index');
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -650,7 +675,7 @@ class TemplateRef<T> implements viewEngine_TemplateRef<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createEmbeddedView(context: T): viewEngine_EmbeddedViewRef<T> {
|
createEmbeddedView(context: T): viewEngine_EmbeddedViewRef<T> {
|
||||||
let viewNode: LViewNode = renderEmbeddedTemplate(null, this._template, context, this._renderer);
|
const viewNode = renderEmbeddedTemplate(null, this._template, context, this._renderer);
|
||||||
return addDestroyable(new EmbeddedViewRef(viewNode, this._template, context));
|
return addDestroyable(new EmbeddedViewRef(viewNode, this._template, context));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -298,6 +298,30 @@ export function createLView(
|
|||||||
return newView;
|
return newView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creation of LNode object is extracted to a separate function so we always create LNode object
|
||||||
|
* with the same shape
|
||||||
|
* (same properties assigned in the same order).
|
||||||
|
*/
|
||||||
|
export function createLNodeObject(
|
||||||
|
type: LNodeType, currentView: LView, parent: LNode, native: RText | RElement | null | undefined,
|
||||||
|
state: any,
|
||||||
|
queries: LQueries | null): LElementNode<extNode&LViewNode&LContainerNode&LProjectionNode {
|
||||||
|
return {
|
||||||
|
type: type,
|
||||||
|
native: native as any,
|
||||||
|
view: currentView,
|
||||||
|
parent: parent as any,
|
||||||
|
child: null,
|
||||||
|
next: null,
|
||||||
|
nodeInjector: parent ? parent.nodeInjector : null,
|
||||||
|
data: state,
|
||||||
|
queries: queries,
|
||||||
|
tNode: null,
|
||||||
|
pNextOrParent: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A common way of creating the LNode to make sure that all of them have same shape to
|
* A common way of creating the LNode to make sure that all of them have same shape to
|
||||||
* keep the execution code monomorphic and fast.
|
* keep the execution code monomorphic and fast.
|
||||||
@ -323,19 +347,8 @@ export function createLNode(
|
|||||||
(isParent ? currentQueries : previousOrParentNode && previousOrParentNode.queries) ||
|
(isParent ? currentQueries : previousOrParentNode && previousOrParentNode.queries) ||
|
||||||
parent && parent.queries && parent.queries.child();
|
parent && parent.queries && parent.queries.child();
|
||||||
const isState = state != null;
|
const isState = state != null;
|
||||||
const node: LElementNode<extNode&LViewNode&LContainerNode&LProjectionNode = {
|
const node =
|
||||||
type: type,
|
createLNodeObject(type, currentView, parent, native, isState ? state as any : null, queries);
|
||||||
native: native as any,
|
|
||||||
view: currentView,
|
|
||||||
parent: parent as any,
|
|
||||||
child: null,
|
|
||||||
next: null,
|
|
||||||
nodeInjector: parent ? parent.nodeInjector : null,
|
|
||||||
data: isState ? state as any : null,
|
|
||||||
queries: queries,
|
|
||||||
tNode: null,
|
|
||||||
pNextOrParent: null
|
|
||||||
};
|
|
||||||
|
|
||||||
if ((type & LNodeType.ViewOrElement) === LNodeType.ViewOrElement && isState) {
|
if ((type & LNodeType.ViewOrElement) === LNodeType.ViewOrElement && isState) {
|
||||||
// Bit of a hack to bust through the readonly because there is a circular dep between
|
// Bit of a hack to bust through the readonly because there is a circular dep between
|
||||||
@ -371,7 +384,7 @@ export function createLNode(
|
|||||||
} else if (previousOrParentNode) {
|
} else if (previousOrParentNode) {
|
||||||
ngDevMode && assertNull(
|
ngDevMode && assertNull(
|
||||||
previousOrParentNode.next,
|
previousOrParentNode.next,
|
||||||
`previousOrParentNode's next property should not have been set.`);
|
`previousOrParentNode's next property should not have been set ${index}.`);
|
||||||
previousOrParentNode.next = node;
|
previousOrParentNode.next = node;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -446,8 +459,9 @@ export function renderEmbeddedTemplate<T>(
|
|||||||
enterView(viewNode.data, viewNode);
|
enterView(viewNode.data, viewNode);
|
||||||
|
|
||||||
template(context, cm);
|
template(context, cm);
|
||||||
refreshDynamicChildren();
|
|
||||||
refreshDirectives();
|
refreshDirectives();
|
||||||
|
refreshDynamicChildren();
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
leaveView(currentView && currentView !.parent !);
|
leaveView(currentView && currentView !.parent !);
|
||||||
isParent = _isParent;
|
isParent = _isParent;
|
||||||
@ -1185,9 +1199,11 @@ function addComponentLogic<T>(
|
|||||||
|
|
||||||
// Only component views should be added to the view tree directly. Embedded views are
|
// Only component views should be added to the view tree directly. Embedded views are
|
||||||
// accessed through their containers because they may be removed / re-added later.
|
// accessed through their containers because they may be removed / re-added later.
|
||||||
const hostView = addToViewTree(createLView(
|
const hostView = addToViewTree(
|
||||||
-1, rendererFactory.createRenderer(previousOrParentNode.native as RElement, def.rendererType),
|
currentView, createLView(
|
||||||
tView, null, null, def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways));
|
-1, rendererFactory.createRenderer(
|
||||||
|
previousOrParentNode.native as RElement, def.rendererType),
|
||||||
|
tView, null, null, def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways));
|
||||||
|
|
||||||
(previousOrParentNode.data as any) = hostView;
|
(previousOrParentNode.data as any) = hostView;
|
||||||
(hostView.node as any) = previousOrParentNode;
|
(hostView.node as any) = previousOrParentNode;
|
||||||
@ -1291,6 +1307,26 @@ function generateInitialInputs(
|
|||||||
//// ViewContainer & View
|
//// ViewContainer & View
|
||||||
//////////////////////////
|
//////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
export function createLContainer(
|
||||||
|
parentLNode: LNode, currentView: LView, template?: ComponentTemplate<any>,
|
||||||
|
host?: LContainerNode | LElementNode): LContainer {
|
||||||
|
ngDevMode && assertNotNull(parentLNode, 'containers should have a parent');
|
||||||
|
return <LContainer>{
|
||||||
|
views: [],
|
||||||
|
nextIndex: 0,
|
||||||
|
// If the direct parent of the container is a view, its views will need to be added
|
||||||
|
// through insertView() when its parent view is being inserted:
|
||||||
|
renderParent: canInsertNativeNode(parentLNode, currentView) ? parentLNode : null,
|
||||||
|
template: template == null ? null : template,
|
||||||
|
next: null,
|
||||||
|
parent: currentView,
|
||||||
|
dynamicViewCount: 0,
|
||||||
|
queries: null,
|
||||||
|
host: host == null ? null : host
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an LContainerNode.
|
* Creates an LContainerNode.
|
||||||
*
|
*
|
||||||
@ -1310,20 +1346,7 @@ export function container(
|
|||||||
currentView.bindingStartIndex, 'container nodes should be created before any bindings');
|
currentView.bindingStartIndex, 'container nodes should be created before any bindings');
|
||||||
|
|
||||||
const currentParent = isParent ? previousOrParentNode : previousOrParentNode.parent !;
|
const currentParent = isParent ? previousOrParentNode : previousOrParentNode.parent !;
|
||||||
ngDevMode && assertNotNull(currentParent, 'containers should have a parent');
|
const lContainer = createLContainer(currentParent, currentView, template);
|
||||||
|
|
||||||
const lContainer = <LContainer>{
|
|
||||||
views: [],
|
|
||||||
nextIndex: 0,
|
|
||||||
// If the direct parent of the container is a view, its views will need to be added
|
|
||||||
// through insertView() when its parent view is being inserted:
|
|
||||||
renderParent: canInsertNativeNode(currentParent, currentView) ? currentParent : null,
|
|
||||||
template: template == null ? null : template,
|
|
||||||
next: null,
|
|
||||||
parent: currentView,
|
|
||||||
dynamicViewCount: 0,
|
|
||||||
queries: null
|
|
||||||
};
|
|
||||||
|
|
||||||
const node = createLNode(index, LNodeType.Container, undefined, lContainer);
|
const node = createLNode(index, LNodeType.Container, undefined, lContainer);
|
||||||
|
|
||||||
@ -1333,7 +1356,7 @@ export function container(
|
|||||||
|
|
||||||
// Containers are added to the current view tree instead of their embedded views
|
// Containers are added to the current view tree instead of their embedded views
|
||||||
// because views can be removed and re-inserted.
|
// because views can be removed and re-inserted.
|
||||||
addToViewTree(node.data);
|
addToViewTree(currentView, node.data);
|
||||||
|
|
||||||
if (firstTemplatePass) cacheMatchingDirectivesForNode(node.tNode);
|
if (firstTemplatePass) cacheMatchingDirectivesForNode(node.tNode);
|
||||||
|
|
||||||
@ -1701,10 +1724,11 @@ function findComponentHost(lView: LView): LElementNode {
|
|||||||
* This structure will be used to traverse through nested views to remove listeners
|
* This structure will be used to traverse through nested views to remove listeners
|
||||||
* and call onDestroy callbacks.
|
* and call onDestroy callbacks.
|
||||||
*
|
*
|
||||||
|
* @param currentView The view where LView or LContainer should be added
|
||||||
* @param state The LView or LContainer to add to the view tree
|
* @param state The LView or LContainer to add to the view tree
|
||||||
* @returns The state passed in
|
* @returns The state passed in
|
||||||
*/
|
*/
|
||||||
export function addToViewTree<T extends LView|LContainer>(state: T): T {
|
export function addToViewTree<T extends LView|LContainer>(currentView: LView, state: T): T {
|
||||||
currentView.tail ? (currentView.tail.next = state) : (currentView.child = state);
|
currentView.tail ? (currentView.tail.next = state) : (currentView.child = state);
|
||||||
currentView.tail = state;
|
currentView.tail = state;
|
||||||
return state;
|
return state;
|
||||||
@ -1887,8 +1911,8 @@ export function detectChangesInternal<T>(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
template(component, creationMode);
|
template(component, creationMode);
|
||||||
refreshDynamicChildren();
|
|
||||||
refreshDirectives();
|
refreshDirectives();
|
||||||
|
refreshDynamicChildren();
|
||||||
} finally {
|
} finally {
|
||||||
leaveView(oldView);
|
leaveView(oldView);
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {ComponentTemplate} from './definition';
|
import {ComponentTemplate} from './definition';
|
||||||
import {LElementNode, LViewNode} from './node';
|
import {LContainerNode, LElementNode, LViewNode} from './node';
|
||||||
import {LQueries} from './query';
|
import {LQueries} from './query';
|
||||||
import {LView, TView} from './view';
|
import {LView, TView} from './view';
|
||||||
|
|
||||||
@ -80,6 +80,13 @@ export interface LContainer {
|
|||||||
* this container are reported to queries referenced here.
|
* this container are reported to queries referenced here.
|
||||||
*/
|
*/
|
||||||
queries: LQueries|null;
|
queries: LQueries|null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a LContainer is created dynamically (by a directive requesting ViewContainerRef) this fields
|
||||||
|
* keeps a reference to a node on which a ViewContainerRef was requested. We need to store this
|
||||||
|
* information to find a next render sibling node.
|
||||||
|
*/
|
||||||
|
host: LContainerNode|LElementNode|null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -281,7 +281,10 @@ export function insertView(
|
|||||||
if (!beforeNode) {
|
if (!beforeNode) {
|
||||||
let containerNextNativeNode = container.native;
|
let containerNextNativeNode = container.native;
|
||||||
if (containerNextNativeNode === undefined) {
|
if (containerNextNativeNode === undefined) {
|
||||||
containerNextNativeNode = container.native = findNextRNodeSibling(container, null);
|
// TODO(pk): this is probably too simplistic, add more tests for various host placements
|
||||||
|
// (dynamic view, projection, ...)
|
||||||
|
containerNextNativeNode = container.native =
|
||||||
|
findNextRNodeSibling(container.data.host ? container.data.host : container, null);
|
||||||
}
|
}
|
||||||
beforeNode = containerNextNativeNode;
|
beforeNode = containerNextNativeNode;
|
||||||
}
|
}
|
||||||
|
@ -170,6 +170,9 @@
|
|||||||
{
|
{
|
||||||
"name": "createLNode"
|
"name": "createLNode"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "createLNodeObject"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "createLView"
|
"name": "createLView"
|
||||||
},
|
},
|
||||||
|
@ -344,6 +344,9 @@
|
|||||||
{
|
{
|
||||||
"name": "checkNoChangesMode"
|
"name": "checkNoChangesMode"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "cleanUpView"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "componentRefresh"
|
"name": "componentRefresh"
|
||||||
},
|
},
|
||||||
@ -359,9 +362,15 @@
|
|||||||
{
|
{
|
||||||
"name": "createInjector"
|
"name": "createInjector"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "createLContainer"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "createLNode"
|
"name": "createLNode"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "createLNodeObject"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "createLView"
|
"name": "createLView"
|
||||||
},
|
},
|
||||||
@ -395,6 +404,9 @@
|
|||||||
{
|
{
|
||||||
"name": "defineInjector"
|
"name": "defineInjector"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "destroyViewTree"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "detectChanges"
|
"name": "detectChanges"
|
||||||
},
|
},
|
||||||
@ -473,6 +485,12 @@
|
|||||||
{
|
{
|
||||||
"name": "executeInitHooks"
|
"name": "executeInitHooks"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "executeOnDestroys"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "executePipeOnDestroys"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "extendStatics"
|
"name": "extendStatics"
|
||||||
},
|
},
|
||||||
@ -542,6 +560,9 @@
|
|||||||
{
|
{
|
||||||
"name": "getOrCreateTemplateRef"
|
"name": "getOrCreateTemplateRef"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "getParentState"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "getPreviousIndex"
|
"name": "getPreviousIndex"
|
||||||
},
|
},
|
||||||
@ -767,6 +788,12 @@
|
|||||||
{
|
{
|
||||||
"name": "refreshDynamicChildren"
|
"name": "refreshDynamicChildren"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "removeListeners"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "removeView"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "renderComponent"
|
"name": "renderComponent"
|
||||||
},
|
},
|
||||||
|
@ -9,12 +9,13 @@
|
|||||||
import {NgForOfContext} from '@angular/common';
|
import {NgForOfContext} from '@angular/common';
|
||||||
|
|
||||||
import {defineComponent} from '../../src/render3/index';
|
import {defineComponent} from '../../src/render3/index';
|
||||||
import {bind, container, containerRefreshEnd, containerRefreshStart, elementEnd, elementProperty, elementStart, text, textBinding} from '../../src/render3/instructions';
|
import {bind, container, elementEnd, elementProperty, elementStart, interpolation3, text, textBinding, tick} from '../../src/render3/instructions';
|
||||||
|
|
||||||
import {NgForOf} from './common_with_def';
|
import {NgForOf} from './common_with_def';
|
||||||
import {renderComponent, toHtml} from './render_util';
|
import {ComponentFixture} from './render_util';
|
||||||
|
|
||||||
describe('@angular/common integration', () => {
|
describe('@angular/common integration', () => {
|
||||||
|
|
||||||
describe('NgForOf', () => {
|
describe('NgForOf', () => {
|
||||||
it('should update a loop', () => {
|
it('should update a loop', () => {
|
||||||
class MyApp {
|
class MyApp {
|
||||||
@ -34,8 +35,6 @@ describe('@angular/common integration', () => {
|
|||||||
elementEnd();
|
elementEnd();
|
||||||
}
|
}
|
||||||
elementProperty(1, 'ngForOf', bind(myApp.items));
|
elementProperty(1, 'ngForOf', bind(myApp.items));
|
||||||
containerRefreshStart(1);
|
|
||||||
containerRefreshEnd();
|
|
||||||
|
|
||||||
function liTemplate(row: NgForOfContext<string>, cm: boolean) {
|
function liTemplate(row: NgForOfContext<string>, cm: boolean) {
|
||||||
if (cm) {
|
if (cm) {
|
||||||
@ -50,9 +49,71 @@ describe('@angular/common integration', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const myApp = renderComponent(MyApp);
|
const fixture = new ComponentFixture(MyApp);
|
||||||
expect(toHtml(myApp)).toEqual('<ul><li>first</li><li>second</li></ul>');
|
expect(fixture.html).toEqual('<ul><li>first</li><li>second</li></ul>');
|
||||||
|
|
||||||
|
// change detection cycle, no model changes
|
||||||
|
fixture.update();
|
||||||
|
expect(fixture.html).toEqual('<ul><li>first</li><li>second</li></ul>');
|
||||||
|
|
||||||
|
// remove the last item
|
||||||
|
fixture.component.items.length = 1;
|
||||||
|
fixture.update();
|
||||||
|
expect(fixture.html).toEqual('<ul><li>first</li></ul>');
|
||||||
|
|
||||||
|
// change an item
|
||||||
|
fixture.component.items[0] = 'one';
|
||||||
|
fixture.update();
|
||||||
|
expect(fixture.html).toEqual('<ul><li>one</li></ul>');
|
||||||
|
|
||||||
|
// add an item
|
||||||
|
fixture.component.items.push('two');
|
||||||
|
fixture.update();
|
||||||
|
expect(fixture.html).toEqual('<ul><li>one</li><li>two</li></ul>');
|
||||||
});
|
});
|
||||||
// TODO: Test inheritance
|
|
||||||
|
it('should support ngForOf context variables', () => {
|
||||||
|
|
||||||
|
class MyApp {
|
||||||
|
items: string[] = ['first', 'second'];
|
||||||
|
|
||||||
|
static ngComponentDef = defineComponent({
|
||||||
|
type: MyApp,
|
||||||
|
factory: () => new MyApp(),
|
||||||
|
selectors: [['my-app']],
|
||||||
|
// <ul>
|
||||||
|
// <li *ngFor="let item of items">{{index}} of {{count}}: {{item}}</li>
|
||||||
|
// </ul>
|
||||||
|
template: (myApp: MyApp, cm: boolean) => {
|
||||||
|
if (cm) {
|
||||||
|
elementStart(0, 'ul');
|
||||||
|
{ container(1, liTemplate, undefined, ['ngForOf', '']); }
|
||||||
|
elementEnd();
|
||||||
|
}
|
||||||
|
elementProperty(1, 'ngForOf', bind(myApp.items));
|
||||||
|
|
||||||
|
function liTemplate(row: NgForOfContext<string>, cm: boolean) {
|
||||||
|
if (cm) {
|
||||||
|
elementStart(0, 'li');
|
||||||
|
{ text(1); }
|
||||||
|
elementEnd();
|
||||||
|
}
|
||||||
|
textBinding(
|
||||||
|
1, interpolation3('', row.index, ' of ', row.count, ': ', row.$implicit, ''));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
directives: () => [NgForOf]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fixture = new ComponentFixture(MyApp);
|
||||||
|
expect(fixture.html).toEqual('<ul><li>0 of 2: first</li><li>1 of 2: second</li></ul>');
|
||||||
|
|
||||||
|
fixture.component.items.splice(1, 0, 'middle');
|
||||||
|
fixture.update();
|
||||||
|
expect(fixture.html)
|
||||||
|
.toEqual('<ul><li>0 of 3: first</li><li>1 of 3: middle</li><li>2 of 3: second</li></ul>');
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -7,10 +7,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {TemplateRef, ViewContainerRef} from '../../src/core';
|
import {TemplateRef, ViewContainerRef} from '../../src/core';
|
||||||
|
import {getOrCreateNodeInjectorForNode, getOrCreateTemplateRef} from '../../src/render3/di';
|
||||||
import {defineComponent, defineDirective, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index';
|
import {defineComponent, defineDirective, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index';
|
||||||
import {bind, container, containerRefreshEnd, containerRefreshStart, loadDirective, text, textBinding} from '../../src/render3/instructions';
|
import {bind, container, elementEnd, elementProperty, elementStart, interpolation1, load, loadDirective, text, textBinding} from '../../src/render3/instructions';
|
||||||
|
|
||||||
import {renderComponent, toHtml} from './render_util';
|
import {ComponentFixture} from './render_util';
|
||||||
|
|
||||||
describe('ViewContainerRef', () => {
|
describe('ViewContainerRef', () => {
|
||||||
class TestDirective {
|
class TestDirective {
|
||||||
@ -18,7 +19,7 @@ describe('ViewContainerRef', () => {
|
|||||||
|
|
||||||
static ngDirectiveDef = defineDirective({
|
static ngDirectiveDef = defineDirective({
|
||||||
type: TestDirective,
|
type: TestDirective,
|
||||||
selectors: [['', 'testDir', '']],
|
selectors: [['', 'testdir', '']],
|
||||||
factory: () => new TestDirective(injectViewContainerRef(), injectTemplateRef(), ),
|
factory: () => new TestDirective(injectViewContainerRef(), injectTemplateRef(), ),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -38,11 +39,9 @@ describe('ViewContainerRef', () => {
|
|||||||
}
|
}
|
||||||
textBinding(0, bind(ctx.$implicit));
|
textBinding(0, bind(ctx.$implicit));
|
||||||
};
|
};
|
||||||
container(0, subTemplate, undefined, ['testDir', '']);
|
container(0, subTemplate, undefined, ['testdir', '']);
|
||||||
}
|
}
|
||||||
containerRefreshStart(0);
|
|
||||||
cmp.testDir = loadDirective<TestDirective>(0);
|
cmp.testDir = loadDirective<TestDirective>(0);
|
||||||
containerRefreshEnd();
|
|
||||||
},
|
},
|
||||||
directives: [TestDirective]
|
directives: [TestDirective]
|
||||||
});
|
});
|
||||||
@ -50,11 +49,137 @@ describe('ViewContainerRef', () => {
|
|||||||
|
|
||||||
|
|
||||||
it('should add embedded view into container', () => {
|
it('should add embedded view into container', () => {
|
||||||
const testCmp = renderComponent(TestComponent);
|
const fixture = new ComponentFixture(TestComponent);
|
||||||
expect(toHtml(testCmp)).toEqual('');
|
expect(fixture.html).toEqual('');
|
||||||
const dir = testCmp.testDir;
|
|
||||||
|
const dir = fixture.component.testDir;
|
||||||
const childCtx = {$implicit: 'works'};
|
const childCtx = {$implicit: 'works'};
|
||||||
const viewRef = dir.viewContainer.createEmbeddedView(dir.template, childCtx);
|
dir.viewContainer.createEmbeddedView(dir.template, childCtx);
|
||||||
expect(toHtml(testCmp)).toEqual('works');
|
expect(fixture.html).toEqual('works');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should add embedded view into a view container on elements', () => {
|
||||||
|
let directiveInstance: TestDirective|undefined;
|
||||||
|
|
||||||
|
class TestDirective {
|
||||||
|
static ngDirectiveDef = defineDirective({
|
||||||
|
type: TestDirective,
|
||||||
|
selectors: [['', 'testdir', '']],
|
||||||
|
factory: () => directiveInstance = new TestDirective(injectViewContainerRef()),
|
||||||
|
inputs: {tpl: 'tpl'}
|
||||||
|
});
|
||||||
|
|
||||||
|
tpl: TemplateRef<{}>;
|
||||||
|
|
||||||
|
constructor(private _vcRef: ViewContainerRef) {}
|
||||||
|
|
||||||
|
insertTpl(ctx?: {}) { this._vcRef.createEmbeddedView(this.tpl, ctx); }
|
||||||
|
|
||||||
|
clear() { this._vcRef.clear(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmbeddedTemplate(ctx: any, cm: boolean) {
|
||||||
|
if (cm) {
|
||||||
|
text(0, 'From a template.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <ng-template #tpl>From a template<ng-template>
|
||||||
|
* before
|
||||||
|
* <div directive [tpl]="tpl"></div>
|
||||||
|
* after
|
||||||
|
*/
|
||||||
|
class TestComponent {
|
||||||
|
testDir: TestDirective;
|
||||||
|
static ngComponentDef = defineComponent({
|
||||||
|
type: TestComponent,
|
||||||
|
selectors: [['test-cmp']],
|
||||||
|
factory: () => new TestComponent(),
|
||||||
|
template: (cmp: TestComponent, cm: boolean) => {
|
||||||
|
if (cm) {
|
||||||
|
container(0, EmbeddedTemplate);
|
||||||
|
text(1, 'before');
|
||||||
|
elementStart(2, 'div', ['testdir', '']);
|
||||||
|
elementEnd();
|
||||||
|
text(3, 'after');
|
||||||
|
}
|
||||||
|
const tpl = getOrCreateTemplateRef(getOrCreateNodeInjectorForNode(
|
||||||
|
load(0))); // TODO(pk): we need proper design / spec for this
|
||||||
|
elementProperty(2, 'tpl', bind(tpl));
|
||||||
|
},
|
||||||
|
directives: [TestDirective]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const fixture = new ComponentFixture(TestComponent);
|
||||||
|
expect(fixture.html).toEqual('before<div testdir=""></div>after');
|
||||||
|
|
||||||
|
directiveInstance !.insertTpl();
|
||||||
|
expect(fixture.html).toEqual('before<div testdir=""></div>From a template.after');
|
||||||
|
|
||||||
|
directiveInstance !.insertTpl();
|
||||||
|
expect(fixture.html)
|
||||||
|
.toEqual('before<div testdir=""></div>From a template.From a template.after');
|
||||||
|
|
||||||
|
directiveInstance !.clear();
|
||||||
|
expect(fixture.html).toEqual('before<div testdir=""></div>after');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add embedded view into a view container on ng-template', () => {
|
||||||
|
let directiveInstance: TestDirective;
|
||||||
|
|
||||||
|
class TestDirective {
|
||||||
|
static ngDirectiveDef = defineDirective({
|
||||||
|
type: TestDirective,
|
||||||
|
selectors: [['', 'testdir', '']],
|
||||||
|
factory: () => directiveInstance =
|
||||||
|
new TestDirective(injectViewContainerRef(), injectTemplateRef())
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor(private _vcRef: ViewContainerRef, private _tplRef: TemplateRef<{}>) {}
|
||||||
|
|
||||||
|
insertTpl(ctx: {}) { this._vcRef.createEmbeddedView(this._tplRef, ctx); }
|
||||||
|
|
||||||
|
remove(index?: number) { this._vcRef.remove(index); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmbeddedTemplate(ctx: any, cm: boolean) {
|
||||||
|
if (cm) {
|
||||||
|
text(0);
|
||||||
|
}
|
||||||
|
textBinding(0, interpolation1('Hello, ', ctx.name, ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* before|<ng-template directive>Hello, {{name}}<ng-template>|after
|
||||||
|
*/
|
||||||
|
class TestComponent {
|
||||||
|
testDir: TestDirective;
|
||||||
|
static ngComponentDef = defineComponent({
|
||||||
|
type: TestComponent,
|
||||||
|
selectors: [['test-cmp']],
|
||||||
|
factory: () => new TestComponent(),
|
||||||
|
template: (cmp: TestComponent, cm: boolean) => {
|
||||||
|
if (cm) {
|
||||||
|
text(0, 'before|');
|
||||||
|
container(1, EmbeddedTemplate, undefined, ['testdir', '']);
|
||||||
|
text(2, '|after');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
directives: [TestDirective]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fixture = new ComponentFixture(TestComponent);
|
||||||
|
expect(fixture.html).toEqual('before||after');
|
||||||
|
|
||||||
|
directiveInstance !.insertTpl({name: 'World'});
|
||||||
|
expect(fixture.html).toEqual('before|Hello, World|after');
|
||||||
|
|
||||||
|
directiveInstance !.remove(0);
|
||||||
|
expect(fixture.html).toEqual('before||after');
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user