fix(ivy): flatten template fns for nested views (#24943)

PR Close #24943
This commit is contained in:
Kara Erickson 2018-07-17 11:45:49 -07:00 committed by Igor Minar
parent 9a6d26e05b
commit 87419097da
9 changed files with 809 additions and 101 deletions

View File

@ -122,6 +122,8 @@ export function renderComponent<T>(
// Create directive instance with factory() and store at index 0 in directives array
rootContext.components.push(
component = baseDirectiveCreate(0, componentDef.factory(), componentDef) as T);
(elementNode.data as LViewData)[CONTEXT] = component;
initChangeDetectorIfExisting(elementNode.nodeInjector, component, elementNode.data !);
opts.hostFeatures && opts.hostFeatures.forEach((feature) => feature(component, componentDef));

View File

@ -24,7 +24,7 @@ import {LInjector} from './interfaces/injector';
import {AttributeMarker, LContainerNode, LElementNode, LNode, LViewNode, TContainerNode, TElementNode, TNodeFlags, TNodeType} from './interfaces/node';
import {LQueries, QueryReadType} from './interfaces/query';
import {Renderer3} from './interfaces/renderer';
import {DIRECTIVES, HOST_NODE, INJECTOR, LViewData, QUERIES, RENDERER, TVIEW, TView} from './interfaces/view';
import {DECLARATION_PARENT, DIRECTIVES, HOST_NODE, INJECTOR, LViewData, QUERIES, RENDERER, TVIEW, TView} from './interfaces/view';
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
import {addRemoveViewFromContainer, appendChild, detachView, getChildLNode, getParentLNode, insertView, removeView} from './node_manipulation';
import {ViewRef} from './view_ref';
@ -728,7 +728,7 @@ export function getOrCreateTemplateRef<T>(di: LInjector): viewEngine.TemplateRef
const hostTNode = hostNode.tNode;
ngDevMode && assertDefined(hostTNode.tViews, 'TView must be allocated');
di.templateRef = new TemplateRef<any>(
getOrCreateElementRef(di), hostTNode.tViews as TView, getRenderer(),
hostNode.view, getOrCreateElementRef(di), hostTNode.tViews as TView, getRenderer(),
hostNode.data[QUERIES]);
}
return di.templateRef;
@ -738,14 +738,15 @@ class TemplateRef<T> implements viewEngine.TemplateRef<T> {
readonly elementRef: viewEngine.ElementRef;
constructor(
elementRef: viewEngine.ElementRef, private _tView: TView, private _renderer: Renderer3,
private _declarationParentView: LViewData, elementRef: viewEngine.ElementRef, private _tView: TView, private _renderer: Renderer3,
private _queries: LQueries|null) {
this.elementRef = elementRef;
}
createEmbeddedView(context: T, containerNode?: LContainerNode, index?: number):
viewEngine.EmbeddedViewRef<T> {
const viewNode = createEmbeddedViewNode(this._tView, context, this._renderer, this._queries);
const viewNode = createEmbeddedViewNode(
this._tView, context, this._declarationParentView, this._renderer, this._queries);
if (containerNode) {
insertView(containerNode, viewNode, index !);
}

View File

@ -22,7 +22,7 @@ import {AttributeMarker, InitialInputData, InitialInputs, LContainerNode, LEleme
import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection';
import {LQueries} from './interfaces/query';
import {ProceduralRenderer3, RComment, RElement, RText, Renderer3, RendererFactory3, RendererStyleFlags3, isProceduralRenderer} from './interfaces/renderer';
import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, CurrentMatchesList, DIRECTIVES, FLAGS, HEADER_OFFSET, HOST_NODE, INJECTOR, LViewData, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RootContext, SANITIZER, TAIL, TData, TVIEW, TView} from './interfaces/view';
import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, CurrentMatchesList, DECLARATION_PARENT, DIRECTIVES, FLAGS, HEADER_OFFSET, HOST_NODE, INJECTOR, LViewData, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RootContext, SANITIZER, TAIL, TData, TVIEW, TView} from './interfaces/view';
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
import {appendChild, appendProjectedNode, canInsertNativeNode, createTextNode, findComponentHost, getChildLNode, getLViewChild, getNextLNode, getParentLNode, insertView, removeView} from './node_manipulation';
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
@ -253,11 +253,13 @@ export function leaveView(newView: LViewData, creationOnly?: boolean): void {
/**
* Refreshes the view, executing the following steps in that order:
* triggers init hooks, refreshes dynamic embedded views, triggers content hooks, sets host
* bindings,
* refreshes child components.
* bindings, refreshes child components.
* Note: view hooks are triggered later when leaving the view.
*/
function refreshView() {
function refreshDescendantViews() {
// This needs to be set before children are processed to support recursive components
tView.firstTemplatePass = firstTemplatePass = false;
if (!checkNoChangesMode) {
executeInitHooks(viewData, tView, creationMode);
}
@ -266,9 +268,6 @@ function refreshView() {
executeHooks(directives !, tView.contentHooks, tView.contentCheckHooks, creationMode);
}
// This needs to be set before children are processed to support recursive components
tView.firstTemplatePass = firstTemplatePass = false;
setHostBindings(tView.hostBindings);
refreshContentQueries(tView);
refreshChildComponents(tView.components);
@ -302,8 +301,8 @@ function refreshContentQueries(tView: TView): void {
/** Refreshes child components in the current view. */
function refreshChildComponents(components: number[] | null): void {
if (components != null) {
for (let i = 0; i < components.length; i += 2) {
componentRefresh(components[i], components[i + 1]);
for (let i = 0; i < components.length; i++) {
componentRefresh(components[i]);
}
}
}
@ -335,6 +334,7 @@ export function createLViewData<T>(
null, // tail
-1, // containerIndex
null, // contentQueries
null // declarationParent
];
}
@ -500,7 +500,8 @@ export function renderTemplate<T>(
* Such lViewNode will then be renderer with renderEmbeddedTemplate() (see below).
*/
export function createEmbeddedViewNode<T>(
tView: TView, context: T, renderer: Renderer3, queries?: LQueries | null): LViewNode {
tView: TView, context: T, declarationParent: LViewData, renderer: Renderer3,
queries?: LQueries | null): LViewNode {
const _isParent = isParent;
const _previousOrParentNode = previousOrParentNode;
isParent = true;
@ -508,6 +509,8 @@ export function createEmbeddedViewNode<T>(
const lView =
createLViewData(renderer, tView, context, LViewFlags.CheckAlways, getCurrentSanitizer());
lView[DECLARATION_PARENT] = declarationParent;
if (queries) {
lView[QUERIES] = queries.createView();
}
@ -544,9 +547,9 @@ export function renderEmbeddedTemplate<T>(
oldView = enterView(viewNode.data !, viewNode);
namespaceHTML();
tView.template !(rf, context);
callTemplateWithContexts(rf, context, tView.template !, viewNode.data ![DECLARATION_PARENT] !);
if (rf & RenderFlags.Update) {
refreshView();
refreshDescendantViews();
} else {
viewNode.data ![TVIEW].firstTemplatePass = firstTemplatePass = false;
}
@ -562,6 +565,99 @@ export function renderEmbeddedTemplate<T>(
return viewNode;
}
/**
* This function calls the template function of a dynamically created view with
* all of its declaration parent contexts (up the view tree) until it reaches the
* component boundary.
*
* Example:
*
* AppComponent template:
* <ul *ngFor="let list of lists">
* <li *ngFor="let item of list"> {{ item }} </li>
* </ul>
*
* function AppComponentTemplate(rf, ctx) {
* // instructions
* function ulTemplate(rf, ulCtx, appCtx) {...}
* function liTemplate(rf, liCtx, ulCtx, appCtx) {...}
* }
*
* The ul view's template must be called with its own context and its declaration
* parent, AppComponent. The li view's template must be called with its own context, its
* parent (the ul), and the ul's parent (AppComponent).
*
* Note that a declaration parent is NOT always the same as the insertion parent. Templates
* can be declared in different views than they are used.
*
* @param rf The RenderFlags for this template invocation
* @param context The context for this template
* @param template The template function to call
* @param parent1 The declaration parent of the dynamic view
*/
function callTemplateWithContexts(
rf: RenderFlags, context: any, template: ComponentTemplate<any>, parent1: LViewData): void {
const parent2 = parent1[DECLARATION_PARENT];
// Calling a function with extra arguments has a VM cost, so only call with necessary args
if (!parent2) return template(rf, context, parent1[CONTEXT]);
const parent3 = parent2[DECLARATION_PARENT];
if (!parent3) return template(rf, context, parent1[CONTEXT], parent2[CONTEXT]);
const parent4 = parent3[DECLARATION_PARENT];
if (!parent4) {
return template(rf, context, parent1[CONTEXT], parent2[CONTEXT], parent3[CONTEXT]);
}
const parent5 = parent4[DECLARATION_PARENT];
if (!parent5) {
return template(
rf, context, parent1[CONTEXT], parent2[CONTEXT], parent3[CONTEXT], parent4[CONTEXT]);
}
const parent6 = parent5[DECLARATION_PARENT];
if (!parent6) {
return template(
rf, context, parent1[CONTEXT], parent2[CONTEXT], parent3[CONTEXT], parent4[CONTEXT],
parent5[CONTEXT]);
}
const parent7 = parent6[DECLARATION_PARENT];
if (!parent7) {
return template(
rf, context, parent1[CONTEXT], parent2[CONTEXT], parent3[CONTEXT], parent4[CONTEXT],
parent5[CONTEXT], parent6[CONTEXT]);
}
const parent8 = parent7[DECLARATION_PARENT];
if (!parent8) {
return template(
rf, context, parent1[CONTEXT], parent2[CONTEXT], parent3[CONTEXT], parent4[CONTEXT],
parent5[CONTEXT], parent6[CONTEXT], parent7[CONTEXT]);
}
const parent9 = parent8[DECLARATION_PARENT];
if (!parent9) {
return template(
rf, context, parent1[CONTEXT], parent2[CONTEXT], parent3[CONTEXT], parent4[CONTEXT],
parent5[CONTEXT], parent6[CONTEXT], parent7[CONTEXT], parent8[CONTEXT]);
}
// We support up to 8 nesting levels in embedded views before we give up and call apply()
const contexts = [
parent1[CONTEXT], parent2[CONTEXT], parent3[CONTEXT], parent4[CONTEXT], parent5[CONTEXT],
parent6[CONTEXT], parent7[CONTEXT], parent8[CONTEXT], parent9[CONTEXT]
];
let currentView: LViewData = parent9;
while (currentView[DECLARATION_PARENT]) {
contexts.push(currentView[DECLARATION_PARENT] ![CONTEXT]);
currentView = currentView[DECLARATION_PARENT] !;
}
tView.template !(rf, context, ...contexts);
}
export function renderComponentOrTemplate<T>(
node: LElementNode, hostView: LViewData, componentOrContext: T,
template?: ComponentTemplate<T>) {
@ -573,14 +669,14 @@ export function renderComponentOrTemplate<T>(
if (template) {
namespaceHTML();
template(getRenderFlags(hostView), componentOrContext !);
refreshView();
refreshDescendantViews();
} else {
executeInitAndContentHooks();
// Element was stored at 0 in data and directive was stored at 0 in directives
// in renderComponent()
setHostBindings(_ROOT_DIRECTIVE_INDICES);
componentRefresh(0, HEADER_OFFSET);
componentRefresh(HEADER_OFFSET);
}
} finally {
if (rendererFactory.end) {
@ -770,9 +866,9 @@ export function resolveDirective(
}
/** Stores index of component's host element so it will be queued for view refresh during CD. */
function queueComponentIndexForCheck(dirIndex: number): void {
function queueComponentIndexForCheck(): void {
if (firstTemplatePass) {
(tView.components || (tView.components = [])).push(dirIndex, viewData.length - 1);
(tView.components || (tView.components = [])).push(viewData.length - 1);
}
}
@ -1543,7 +1639,7 @@ function addComponentLogic<T>(
viewData, previousOrParentNode.tNode.index as number,
createLViewData(
rendererFactory.createRenderer(previousOrParentNode.native as RElement, def.rendererType),
tView, null, def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways,
tView, instance, def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways,
getCurrentSanitizer()));
// We need to set the host node/data here because when the component LNode was created,
@ -1553,7 +1649,7 @@ function addComponentLogic<T>(
initChangeDetectorIfExisting(previousOrParentNode.nodeInjector, instance, componentView);
if (firstTemplatePass) queueComponentIndexForCheck(directiveIndex);
if (firstTemplatePass) queueComponentIndexForCheck();
}
/**
@ -1914,7 +2010,7 @@ function getOrCreateEmbeddedTView(viewIndex: number, parent: LContainerNode): TV
/** Marks the end of an embedded view. */
export function embeddedViewEnd(): void {
refreshView();
refreshDescendantViews();
isParent = false;
previousOrParentNode = viewData[HOST_NODE] as LViewNode;
leaveView(viewData[PARENT] !);
@ -1927,10 +2023,9 @@ export function embeddedViewEnd(): void {
/**
* Refreshes components by entering the component view and processing its bindings, queries, etc.
*
* @param directiveIndex Directive index in LViewData[DIRECTIVES]
* @param adjustedElementIndex Element index in LViewData[] (adjusted for HEADER_OFFSET)
*/
export function componentRefresh<T>(directiveIndex: number, adjustedElementIndex: number): void {
export function componentRefresh<T>(adjustedElementIndex: number): void {
ngDevMode && assertDataInRange(adjustedElementIndex);
const element = viewData[adjustedElementIndex] as LElementNode;
ngDevMode && assertNodeType(element, TNodeType.Element);
@ -1940,8 +2035,7 @@ export function componentRefresh<T>(directiveIndex: number, adjustedElementIndex
// Only attached CheckAlways components or attached, dirty OnPush components should be checked
if (viewAttached(hostView) && hostView[FLAGS] & (LViewFlags.CheckAlways | LViewFlags.Dirty)) {
ngDevMode && assertDataInRange(directiveIndex, directives !);
detectChangesInternal(hostView, element, directives ![directiveIndex]);
detectChangesInternal(hostView, element, hostView[CONTEXT]);
}
}
@ -2267,7 +2361,7 @@ export function detectChangesInternal<T>(
namespaceHTML();
createViewQuery(viewQuery, hostView[FLAGS], component);
template(getRenderFlags(hostView), component);
refreshView();
refreshDescendantViews();
updateViewQuery(viewQuery, component);
} finally {
leaveView(oldView);

View File

@ -15,7 +15,7 @@ import {CssSelectorList} from './projection';
* Definition of what a template rendering function should look like.
*/
export type ComponentTemplate<T> = {
(rf: RenderFlags, ctx: T): void; ngPrivateData?: never;
(rf: RenderFlags, ctx: T, ...parentCtx: ({} | null)[]): void; ngPrivateData?: never;
};
/**

View File

@ -17,7 +17,7 @@ import {LQueries} from './query';
import {Renderer3} from './renderer';
/** Size of LViewData's header. Necessary to adjust for it when setting slots. */
export const HEADER_OFFSET = 16;
export const HEADER_OFFSET = 17;
// Below are constants for LViewData indices to help us look up LViewData members
// without having to remember the specific indices.
@ -38,6 +38,7 @@ export const SANITIZER = 12;
export const TAIL = 13;
export const CONTAINER_INDEX = 14;
export const CONTENT_QUERIES = 15;
export const DECLARATION_PARENT = 16;
/**
* `LViewData` stores all of the information needed to process the instructions as
@ -61,6 +62,9 @@ export interface LViewData extends Array<any> {
* The parent view is needed when we exit the view and must restore the previous
* `LViewData`. Without this, the render method would have to keep a stack of
* views as it is recursively rendering templates.
*
* This is also the "insertion" parent for embedded views. This allows us to properly
* destroy embedded views.
*/
[PARENT]: LViewData|null;
@ -143,7 +147,6 @@ export interface LViewData extends Array<any> {
* The tail allows us to quickly add a new state to the end of the view list
* without having to propagate starting from the first child.
*/
// TODO: replace with global
[TAIL]: LViewData|LContainer|null;
/**
@ -162,6 +165,32 @@ export interface LViewData extends Array<any> {
* be refreshed.
*/
[CONTENT_QUERIES]: QueryList<any>[]|null;
/**
* Parent view where this view's template was declared.
*
* Only applicable for dynamically created views. Will be null for inline/component views.
*
* The template for a dynamically created view may be declared in a different view than
* it is inserted. We already track the "insertion parent" (view where the template was
* inserted) in LViewData[PARENT], but we also need access to the "declaration parent"
* (view where the template was declared). Otherwise, we wouldn't be able to call the
* view's template function with the proper contexts. Context should be inherited from
* the declaration parent tree, not the insertion parent tree.
*
* Example (AppComponent template):
*
* <ng-template #foo></ng-template> <-- declared here -->
* <some-comp [tpl]="foo"></some-comp> <-- inserted inside this component -->
*
* The <ng-template> above is declared in the AppComponent template, but it will be passed into
* SomeComp and inserted there. In this case, the declaration parent would be the AppComponent,
* but the insertion parent would be SomeComp. When we are removing views, we would want to
* traverse through the insertion parent to clean up listeners. When we are calling the
* template function during change detection, we need the declaration parent to get inherited
* context.
*/
[DECLARATION_PARENT]: LViewData|null;
}
/** Flags associated with an LView (saved in LViewData[FLAGS]) */
@ -404,11 +433,10 @@ export interface TView {
cleanup: any[]|null;
/**
* A list of directive and element indices for child components that will need to be
* refreshed when the current view has finished its check.
* A list of element indices for child components that will need to be
* refreshed when the current view has finished its check. These indices have
* already been adjusted for the HEADER_OFFSET.
*
* Even indices: Directive indices
* Odd indices: Element indices (adjusted for LViewData header offset)
*/
components: number[]|null;

View File

@ -10,7 +10,7 @@ import {NgForOfContext} from '@angular/common';
import {getOrCreateNodeInjectorForNode, getOrCreateTemplateRef} from '../../src/render3/di';
import {AttributeMarker, defineComponent} from '../../src/render3/index';
import {bind, container, elementEnd, elementProperty, elementStart, interpolation1, interpolation3, listener, load, text, textBinding} from '../../src/render3/instructions';
import {bind, container, elementEnd, elementProperty, elementStart, interpolation1, interpolation2, interpolation3, interpolationV, listener, load, text, textBinding} from '../../src/render3/instructions';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {NgForOf, NgIf, NgTemplateOutlet} from './common_with_def';
@ -40,7 +40,7 @@ describe('@angular/common integration', () => {
elementProperty(1, 'ngForOf', bind(myApp.items));
}
function liTemplate(rf1: RenderFlags, row: NgForOfContext<string>) {
function liTemplate(rf1: RenderFlags, row: NgForOfContext<string>, parent: MyApp) {
if (rf1 & RenderFlags.Create) {
elementStart(0, 'li');
{ text(1); }
@ -100,7 +100,7 @@ describe('@angular/common integration', () => {
elementProperty(1, 'ngForOf', bind(myApp.items));
}
function liTemplate(rf1: RenderFlags, row: NgForOfContext<string>) {
function liTemplate(rf1: RenderFlags, row: NgForOfContext<string>, parent: MyApp) {
if (rf1 & RenderFlags.Create) {
elementStart(0, 'li');
{ text(1); }
@ -164,7 +164,7 @@ describe('@angular/common integration', () => {
elementProperty(3, 'ngForOf', bind(myApp.items));
}
function liTemplate(rf1: RenderFlags, row: NgForOfContext<string>) {
function liTemplate(rf1: RenderFlags, row: NgForOfContext<string>, parent: MyApp) {
if (rf1 & RenderFlags.Create) {
elementStart(0, 'li');
{ text(1); }
@ -203,14 +203,14 @@ describe('@angular/common integration', () => {
it('should support multiple levels of embedded templates', () => {
/**
* <ul *ngFor="let outterItem of items.">
* <li *ngFor="let item of items">
* <span>{{item}}</span>
* <ul>
* <li *ngFor="let row of items">
* <span *ngFor="let cell of row.data">{{cell}} - {{ row.value }}</span>
* </li>
* </ul>
*/
class MyApp {
items: string[] = ['1', '2'];
items: any[] = [{data: ['1', '2'], value: 'first'}, {data: ['3', '4'], value: 'second'}];
static ngComponentDef = defineComponent({
type: MyApp,
@ -226,25 +226,27 @@ describe('@angular/common integration', () => {
elementProperty(1, 'ngForOf', bind(myApp.items));
}
function liTemplate(rf1: RenderFlags, row: NgForOfContext<string>) {
function liTemplate(rf1: RenderFlags, row: any, myApp: MyApp) {
if (rf1 & RenderFlags.Create) {
elementStart(0, 'li');
{ container(1, spanTemplate, null, ['ngForOf', '']); }
elementEnd();
}
if (rf1 & RenderFlags.Update) {
elementProperty(1, 'ngForOf', bind(myApp.items));
const r1 = row.$implicit as any;
elementProperty(1, 'ngForOf', bind(r1.data));
}
}
function spanTemplate(rf1: RenderFlags, row: NgForOfContext<string>) {
function spanTemplate(rf1: RenderFlags, cell: any, row: any, myApp: MyApp) {
if (rf1 & RenderFlags.Create) {
elementStart(0, 'span');
{ text(1); }
elementEnd();
}
if (rf1 & RenderFlags.Update) {
textBinding(1, bind(row.$implicit));
textBinding(
1, interpolation2('', cell.$implicit, ' - ', (row.$implicit as any).value, ''));
}
}
},
@ -258,25 +260,283 @@ describe('@angular/common integration', () => {
fixture.update();
expect(fixture.html)
.toEqual(
'<ul><li><span>1</span><span>2</span></li><li><span>1</span><span>2</span></li></ul>');
'<ul><li><span>1 - first</span><span>2 - first</span></li><li><span>3 - second</span><span>4 - second</span></li></ul>');
// Remove the last item
fixture.component.items.length = 1;
fixture.update();
expect(fixture.html).toEqual('<ul><li><span>1</span></li></ul>');
expect(fixture.html)
.toEqual('<ul><li><span>1 - first</span><span>2 - first</span></li></ul>');
// Change an item
fixture.component.items[0] = 'one';
fixture.component.items[0].data[0] = 'one';
fixture.update();
expect(fixture.html).toEqual('<ul><li><span>one</span></li></ul>');
expect(fixture.html)
.toEqual('<ul><li><span>one - first</span><span>2 - first</span></li></ul>');
// Add an item
fixture.component.items.push('two');
fixture.component.items[1] = {data: ['three', '4'], value: 'third'};
fixture.update();
expect(fixture.html)
.toEqual(
'<ul><li><span>one</span><span>two</span></li><li><span>one</span><span>two</span></li></ul>');
'<ul><li><span>one - first</span><span>2 - first</span></li><li><span>three - third</span><span>4 - third</span></li></ul>');
});
it('should support context for 9+ levels of embedded templates', () => {
/**
*
* <span *ngFor="let item0 of items">
* <span *ngFor="let item1 of item0.data">
* <span *ngFor="let item2 of item1.data">
* <span *ngFor="let item3 of item2.data">
* <span *ngFor="let item4 of item3.data">
* <span *ngFor="let item5 of item4.data">
* <span *ngFor="let item6 of item5.data">
* <span *ngFor="let item7 of item6.data">
* <span *ngFor="let item8 of item7.data">
* {{ item8 }} - {{ item7.value }} - {{ item6.value }}...
* </span>
* </span>
* </span>
* </span>
* </span>
* </span>
* </span>
* </span>
* </span>
*/
class MyApp {
value = 'App';
items: any[] = [
{
// item0
data: [{
// item1
data: [{
// item2
data: [{
// item3
data: [{
// item4
data: [{
// item5
data: [{
// item6
data: [{
// item7
data: [
'1', '2' // item8
],
value: 'h'
}],
value: 'g'
}],
value: 'f'
}],
value: 'e'
}],
value: 'd'
}],
value: 'c'
}],
value: 'b'
}],
value: 'a'
},
{
// item0
data: [{
// item1
data: [{
// item2
data: [{
// item3
data: [{
// item4
data: [{
// item5
data: [{
// item6
data: [{
// item7
data: [
'3', '4' // item8
],
value: 'H'
}],
value: 'G'
}],
value: 'F'
}],
value: 'E'
}],
value: 'D'
}],
value: 'C'
}],
value: 'B'
}],
value: 'A'
}
];
static ngComponentDef = defineComponent({
type: MyApp,
factory: () => new MyApp(),
selectors: [['my-app']],
template: (rf: RenderFlags, myApp: MyApp) => {
if (rf & RenderFlags.Create) {
container(0, itemTemplate0, null, ['ngForOf', '']);
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'ngForOf', bind(myApp.items));
}
function itemTemplate0(rf1: RenderFlags, item0: any, myApp: MyApp) {
if (rf1 & RenderFlags.Create) {
elementStart(0, 'span');
{ container(1, itemTemplate1, null, ['ngForOf', '']); }
elementEnd();
}
if (rf1 & RenderFlags.Update) {
const item = item0.$implicit as any;
elementProperty(1, 'ngForOf', bind(item.data));
}
}
function itemTemplate1(rf1: RenderFlags, item1: any, item0: any, myApp: MyApp) {
if (rf1 & RenderFlags.Create) {
elementStart(0, 'span');
{ container(1, itemTemplate2, null, ['ngForOf', '']); }
elementEnd();
}
if (rf1 & RenderFlags.Update) {
const item = item1.$implicit as any;
elementProperty(1, 'ngForOf', bind(item.data));
}
}
function itemTemplate2(
rf1: RenderFlags, item2: any, item1: any, item0: any, myApp: MyApp) {
if (rf1 & RenderFlags.Create) {
elementStart(0, 'span');
{ container(1, itemTemplate3, null, ['ngForOf', '']); }
elementEnd();
}
if (rf1 & RenderFlags.Update) {
const item = item2.$implicit as any;
elementProperty(1, 'ngForOf', bind(item.data));
}
}
function itemTemplate3(
rf1: RenderFlags, item3: any, item2: any, item1: any, item0: any, myApp: MyApp) {
if (rf1 & RenderFlags.Create) {
elementStart(0, 'span');
{ container(1, itemTemplate4, null, ['ngForOf', '']); }
elementEnd();
}
if (rf1 & RenderFlags.Update) {
const item = item3.$implicit as any;
elementProperty(1, 'ngForOf', bind(item.data));
}
}
function itemTemplate4(
rf1: RenderFlags, item4: any, item3: any, item2: any, item1: any, item0: any,
myApp: MyApp) {
if (rf1 & RenderFlags.Create) {
elementStart(0, 'span');
{ container(1, itemTemplate5, null, ['ngForOf', '']); }
elementEnd();
}
if (rf1 & RenderFlags.Update) {
const item = item4.$implicit as any;
elementProperty(1, 'ngForOf', bind(item.data));
}
}
function itemTemplate5(
rf1: RenderFlags, item5: any, item4: any, item3: any, item2: any, item1: any,
item0: any, myApp: MyApp) {
if (rf1 & RenderFlags.Create) {
elementStart(0, 'span');
{ container(1, itemTemplate6, null, ['ngForOf', '']); }
elementEnd();
}
if (rf1 & RenderFlags.Update) {
const item = item5.$implicit as any;
elementProperty(1, 'ngForOf', bind(item.data));
}
}
function itemTemplate6(
rf1: RenderFlags, item6: any, item5: any, item4: any, item3: any, item2: any,
item1: any, item0: any, myApp: MyApp) {
if (rf1 & RenderFlags.Create) {
elementStart(0, 'span');
{ container(1, itemTemplate7, null, ['ngForOf', '']); }
elementEnd();
}
if (rf1 & RenderFlags.Update) {
const item = item6.$implicit as any;
elementProperty(1, 'ngForOf', bind(item.data));
}
}
function itemTemplate7(
rf1: RenderFlags, item7: any, item6: any, item5: any, item4: any, item3: any,
item2: any, item1: any, item0: any, myApp: MyApp) {
if (rf1 & RenderFlags.Create) {
elementStart(0, 'span');
{ container(1, itemTemplate8, null, ['ngForOf', '']); }
elementEnd();
}
if (rf1 & RenderFlags.Update) {
const item = item7.$implicit as any;
elementProperty(1, 'ngForOf', bind(item.data));
}
}
function itemTemplate8(
rf1: RenderFlags, item8: any, item7: any, item6: any, item5: any, item4: any,
item3: any, item2: any, item1: any, item0: any, myApp: MyApp) {
if (rf1 & RenderFlags.Create) {
elementStart(0, 'span');
{ text(1); }
elementEnd();
}
if (rf1 & RenderFlags.Update) {
textBinding(1, interpolationV([
'', item8.$implicit, '.', item7.$implicit.value,
'.', item6.$implicit.value, '.', item5.$implicit.value,
'.', item4.$implicit.value, '.', item3.$implicit.value,
'.', item2.$implicit.value, '.', item1.$implicit.value,
'.', item0.$implicit.value, '.', myApp.value,
''
]));
}
}
},
directives: () => [NgForOf]
});
}
const fixture = new ComponentFixture(MyApp);
expect(fixture.html)
.toEqual(
'<span><span><span><span><span><span><span><span>' +
'<span>1.h.g.f.e.d.c.b.a.App</span>' +
'<span>2.h.g.f.e.d.c.b.a.App</span>' +
'</span></span></span></span></span></span></span></span>' +
'<span><span><span><span><span><span><span><span>' +
'<span>3.H.G.F.E.D.C.B.A.App</span>' +
'<span>4.H.G.F.E.D.C.B.A.App</span>' +
'</span></span></span></span></span></span></span></span>');
});
});
describe('ngIf', () => {
@ -304,7 +564,7 @@ describe('@angular/common integration', () => {
elementProperty(1, 'ngIf', bind(myApp.showing));
}
function templateOne(rf: RenderFlags, ctx: any) {
function templateOne(rf: RenderFlags, ctx: any, parent: MyApp) {
if (rf & RenderFlags.Create) {
elementStart(0, 'div');
{ text(1); }
@ -314,7 +574,7 @@ describe('@angular/common integration', () => {
textBinding(1, bind(myApp.valueOne));
}
}
function templateTwo(rf: RenderFlags, ctx: any) {
function templateTwo(rf: RenderFlags, ctx: any, parent: MyApp) {
if (rf & RenderFlags.Create) {
elementStart(0, 'div');
{ text(1); }

View File

@ -7,15 +7,16 @@
*/
import {DoCheck, ViewEncapsulation, createInjector, defineInjectable, defineInjector} from '../../src/core';
import {DoCheck, Input, TemplateRef, ViewContainerRef, ViewEncapsulation, createInjector, defineInjectable, defineInjector} from '../../src/core';
import {getRenderedText} from '../../src/render3/component';
import {ComponentFactory, LifecycleHooksFeature, defineComponent, directiveInject, markDirty} from '../../src/render3/index';
import {AttributeMarker, ComponentFactory, LifecycleHooksFeature, defineComponent, directiveInject, markDirty} from '../../src/render3/index';
import {bind, container, containerRefreshEnd, containerRefreshStart, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, text, textBinding, tick} from '../../src/render3/instructions';
import {ComponentDefInternal, DirectiveDefInternal, RenderFlags} from '../../src/render3/interfaces/definition';
import {createRendererType2} from '../../src/view/index';
import {NgIf} from './common_with_def';
import {getRendererFactory2} from './imported_renderer2';
import {ComponentFixture, containerEl, renderComponent, renderToHtml, requestAnimationFrame, toHtml} from './render_util';
import {ComponentFixture, containerEl, createComponent, renderComponent, renderToHtml, requestAnimationFrame, toHtml} from './render_util';
describe('component', () => {
class CounterComponent {
@ -281,8 +282,13 @@ describe('encapsulation', () => {
});
describe('recursive components', () => {
let events: string[] = [];
let count = 0;
let events: string[];
let count: number;
beforeEach(() => {
events = [];
count = 0;
});
class TreeNode {
constructor(
@ -290,11 +296,23 @@ describe('recursive components', () => {
public right: TreeNode|null) {}
}
/**
* {{ data.value }}
*
* % if (data.left != null) {
* <tree-comp [data]="data.left"></tree-comp>
* % }
* % if (data.right != null) {
* <tree-comp [data]="data.right"></tree-comp>
* % }
*/
class TreeComponent {
data: TreeNode = _buildTree(0);
ngDoCheck() { events.push('check' + this.data.value); }
ngOnDestroy() { events.push('destroy' + this.data.value); }
static ngComponentDef = defineComponent({
type: TreeComponent,
selectors: [['tree-comp']],
@ -344,6 +362,58 @@ describe('recursive components', () => {
(TreeComponent.ngComponentDef as ComponentDefInternal<TreeComponent>).directiveDefs =
() => [TreeComponent.ngComponentDef];
/**
* {{ data.value }}
* <ng-if-tree [data]="data.left" *ngIf="data.left"></ng-if-tree>
* <ng-if-tree [data]="data.right" *ngIf="data.right"></ng-if-tree>
*/
class NgIfTree {
data: TreeNode = _buildTree(0);
ngOnDestroy() { events.push('destroy' + this.data.value); }
static ngComponentDef = defineComponent({
type: NgIfTree,
selectors: [['ng-if-tree']],
factory: () => new NgIfTree(),
template: (rf: RenderFlags, ctx: NgIfTree) => {
if (rf & RenderFlags.Create) {
text(0);
container(1, IfTemplate, '', [AttributeMarker.SelectOnly, 'ngIf']);
container(2, IfTemplate2, '', [AttributeMarker.SelectOnly, 'ngIf']);
}
if (rf & RenderFlags.Update) {
textBinding(0, bind(ctx.data.value));
elementProperty(1, 'ngIf', bind(ctx.data.left));
elementProperty(2, 'ngIf', bind(ctx.data.right));
}
function IfTemplate(rf1: RenderFlags, left: any, parent: NgIfTree) {
if (rf1 & RenderFlags.Create) {
elementStart(0, 'ng-if-tree');
elementEnd();
}
if (rf1 & RenderFlags.Update) {
elementProperty(0, 'data', bind(parent.data.left));
}
}
function IfTemplate2(rf1: RenderFlags, right: any, parent: NgIfTree) {
if (rf1 & RenderFlags.Create) {
elementStart(0, 'ng-if-tree');
elementEnd();
}
if (rf1 & RenderFlags.Update) {
elementProperty(0, 'data', bind(parent.data.right));
}
}
},
inputs: {data: 'data'},
});
}
(NgIfTree.ngComponentDef as ComponentDefInternal<NgIfTree>).directiveDefs =
() => [NgIfTree.ngComponentDef, NgIf.ngDirectiveDef];
function _buildTree(currDepth: number): TreeNode {
const children = currDepth < 2 ? _buildTree(currDepth + 1) : null;
const children2 = currDepth < 2 ? _buildTree(currDepth + 1) : null;
@ -360,6 +430,76 @@ describe('recursive components', () => {
expect(events).toEqual(['check6', 'check2', 'check0', 'check1', 'check5', 'check3', 'check4']);
});
// This tests that the view tree is set up properly for recursive components
it('should call onDestroys properly', () => {
/**
* % if (!skipContent) {
* <tree-comp></tree-comp>
* % }
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
container(0);
}
if (rf & RenderFlags.Update) {
containerRefreshStart(0);
if (!ctx.skipContent) {
const rf0 = embeddedViewStart(0);
if (rf0 & RenderFlags.Create) {
elementStart(0, 'tree-comp');
elementEnd();
}
embeddedViewEnd();
}
containerRefreshEnd();
}
}, [TreeComponent]);
const fixture = new ComponentFixture(App);
expect(getRenderedText(fixture.component)).toEqual('6201534');
events = [];
fixture.component.skipContent = true;
fixture.update();
expect(events).toEqual(
['destroy0', 'destroy1', 'destroy2', 'destroy3', 'destroy4', 'destroy5', 'destroy6']);
});
it('should call onDestroys properly with ngIf', () => {
/**
* % if (!skipContent) {
* <ng-if-tree></ng-if-tree>
* % }
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
container(0);
}
if (rf & RenderFlags.Update) {
containerRefreshStart(0);
if (!ctx.skipContent) {
const rf0 = embeddedViewStart(0);
if (rf0 & RenderFlags.Create) {
elementStart(0, 'ng-if-tree');
elementEnd();
}
embeddedViewEnd();
}
containerRefreshEnd();
}
}, [NgIfTree]);
const fixture = new ComponentFixture(App);
expect(getRenderedText(fixture.component)).toEqual('6201534');
events = [];
fixture.component.skipContent = true;
fixture.update();
expect(events).toEqual(
['destroy0', 'destroy1', 'destroy2', 'destroy3', 'destroy4', 'destroy5', 'destroy6']);
});
it('should map inputs minified & unminified names', async() => {
class TestInputsComponent {
// TODO(issue/24571): remove '!'.

View File

@ -15,6 +15,7 @@ import {AttributeMarker, detectChanges} from '../../src/render3/index';
import {bind, container, containerRefreshEnd, containerRefreshStart, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, loadDirective, projection, projectionDef, text} from '../../src/render3/instructions';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {NgIf} from './common_with_def';
import {ComponentFixture, createComponent, renderComponent, toHtml} from './render_util';
describe('content projection', () => {
@ -804,22 +805,6 @@ describe('content projection', () => {
});
it('should project into dynamic views (with createEmbeddedView)', () => {
class NgIf {
constructor(public vcr: ViewContainerRef, public template: TemplateRef<any>) {}
@Input()
set ngIf(value: boolean) {
value ? this.vcr.createEmbeddedView(this.template) : this.vcr.clear();
}
static ngDirectiveDef = defineDirective({
type: NgIf,
selectors: [['', 'ngIf', '']],
inputs: {'ngIf': 'ngIf'},
factory: () => new NgIf(injectViewContainerRef(), injectTemplateRef())
});
}
/**
* Before-
* <ng-template [ngIf]="showing">
@ -838,7 +823,7 @@ describe('content projection', () => {
elementProperty(1, 'ngIf', bind(ctx.showing));
}
function IfTemplate(rf1: RenderFlags, ctx1: any) {
function IfTemplate(rf1: RenderFlags, ctx1: any, child: any) {
if (rf1 & RenderFlags.Create) {
projectionDef();
projection(0);
@ -884,27 +869,6 @@ describe('content projection', () => {
});
it('should project into dynamic views (with insertion)', () => {
class NgIf {
constructor(public vcr: ViewContainerRef, public template: TemplateRef<any>) {}
@Input()
set ngIf(value: boolean) {
if (value) {
const viewRef = this.template.createEmbeddedView({});
this.vcr.insert(viewRef);
} else {
this.vcr.clear();
}
}
static ngDirectiveDef = defineDirective({
type: NgIf,
selectors: [['', 'ngIf', '']],
inputs: {'ngIf': 'ngIf'},
factory: () => new NgIf(injectViewContainerRef(), injectTemplateRef())
});
}
/**
* Before-
* <ng-template [ngIf]="showing">
@ -923,7 +887,7 @@ describe('content projection', () => {
elementProperty(1, 'ngIf', bind(ctx.showing));
}
function IfTemplate(rf1: RenderFlags, ctx1: any) {
function IfTemplate(rf1: RenderFlags, ctx1: any, child: any) {
if (rf1 & RenderFlags.Create) {
projectionDef();
projection(0);

View File

@ -8,11 +8,12 @@
import {Component, ComponentFactoryResolver, Directive, EmbeddedViewRef, NgModuleRef, Pipe, PipeTransform, RendererFactory2, TemplateRef, ViewContainerRef, createInjector, defineInjector, ɵAPP_ROOT as APP_ROOT, ɵNgModuleDef as NgModuleDef} from '../../src/core';
import {getOrCreateNodeInjectorForNode, getOrCreateTemplateRef} from '../../src/render3/di';
import {NgOnChangesFeature, defineComponent, defineDirective, definePipe, injectComponentFactoryResolver, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index';
import {bind, container, containerRefreshEnd, containerRefreshStart, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, load, loadDirective, projection, projectionDef, reserveSlots, text, textBinding} from '../../src/render3/instructions';
import {AttributeMarker,NgOnChangesFeature, defineComponent, defineDirective, definePipe, injectComponentFactoryResolver, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index';
import {bind, container, containerRefreshEnd, containerRefreshStart, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation3, load, loadDirective, projection, projectionDef, reserveSlots, text, textBinding} from '../../src/render3/instructions';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {NgModuleFactory} from '../../src/render3/ng_module_ref';
import {pipe, pipeBind1} from '../../src/render3/pipe';
import {NgForOf} from '../../test/render3/common_with_def';
import {getRendererFactory2} from './imported_renderer2';
import {ComponentFixture, TemplateFixture, createComponent} from './render_util';
@ -447,6 +448,224 @@ describe('ViewContainerRef', () => {
});
});
describe('insertion points and declaration points', () => {
class InsertionDir {
// @Input()
set tplDir(tpl: TemplateRef<any>|null) {
tpl ? this.vcr.createEmbeddedView(tpl) : this.vcr.clear();
}
constructor(public vcr: ViewContainerRef) {}
static ngDirectiveDef = defineDirective({
type: InsertionDir,
selectors: [['', 'tplDir', '']],
factory: () => new InsertionDir(injectViewContainerRef()),
inputs: {tplDir: 'tplDir'}
});
}
// see running stackblitz example: https://stackblitz.com/edit/angular-w3myy6
it('should work with a template declared in a different component view from insertion',
() => {
let child: Child|null = null;
/**
* <div [tplDir]="tpl">{{ name }}</div>
* // template insertion point
*/
class Child {
name = 'Child';
tpl: TemplateRef<any>|null = null;
static ngComponentDef = defineComponent({
type: Child,
selectors: [['child']],
factory: () => child = new Child(),
template: function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementStart(0, 'div', [AttributeMarker.SelectOnly, 'tplDir']);
{ text(1); }
elementEnd();
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'tplDir', bind(ctx.tpl));
textBinding(1, bind(ctx.name));
}
},
inputs: {tpl: 'tpl'},
directives: () => [InsertionDir]
});
}
/**
* // template declaration point
* <ng-template #foo>
* <div> {{ name }} </div>
* </ng-template>
*
* <child [tpl]="foo"></child> <-- template insertion inside
*/
const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
container(0, template);
elementStart(1, 'child');
elementEnd();
}
if (rf & RenderFlags.Update) {
// Hack until we have local refs for templates
const tplRef = getOrCreateTemplateRef(getOrCreateNodeInjectorForNode(load(0)));
elementProperty(1, 'tpl', bind(tplRef));
}
function template(rf1: RenderFlags, ctx1: any, parent: any) {
if (rf1 & RenderFlags.Create) {
elementStart(0, 'div');
{ text(1); }
elementEnd();
}
if (rf1 & RenderFlags.Update) {
textBinding(1, bind(parent.name));
}
}
}, [Child]);
const fixture = new ComponentFixture(Parent);
fixture.component.name = 'Parent';
fixture.update();
// Context should be inherited from the declaration point, not the insertion point,
// so the template should read 'Parent'.
expect(fixture.html).toEqual(`<child><div>Child</div><div>Parent</div></child>`);
child !.tpl = null;
fixture.update();
expect(fixture.html).toEqual(`<child><div>Child</div></child>`);
});
// see running stackblitz example: https://stackblitz.com/edit/angular-3vplec
it('should work with nested for loops with different declaration / insertion points', () => {
/**
* <ng-template ngFor [ngForOf]="rows" [ngForTemplate]="tpl">
* // insertion point for templates (both row and cell)
* </ng-template>
*/
class LoopComp {
name = 'Loop';
// @Input()
tpl !: TemplateRef<any>;
// @Input()
rows !: any[];
static ngComponentDef = defineComponent({
type: LoopComp,
selectors: [['loop-comp']],
factory: () => new LoopComp(),
template: function(rf: RenderFlags, loop: any) {
if (rf & RenderFlags.Create) {
container(0, () => {}, null, [AttributeMarker.SelectOnly, 'ngForOf']);
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'ngForOf', bind(loop.rows));
elementProperty(0, 'ngForTemplate', bind(loop.tpl));
}
},
inputs: {tpl: 'tpl', rows: 'rows'},
directives: () => [NgForOf]
});
}
/**
* // row declaration point
* <ng-template #rowTemplate let-row>
*
* // cell declaration point
* <ng-template #cellTemplate let-cell>
* <div> {{ cell }} - {{ row.value }} - {{ name }} </div>
* </ng-template>
*
* <loop-comp [tpl]="cellTemplate" [rows]="row.data"></loop-comp> <-- cell insertion
* </ng-template>
*
* <loop-comp [tpl]="rowTemplate" [rows]="rows"> <-- row insertion
* </loop-comp>
*/
const Parent = createComponent('parent', function(rf: RenderFlags, parent: any) {
if (rf & RenderFlags.Create) {
container(0, rowTemplate);
elementStart(1, 'loop-comp');
elementEnd();
}
if (rf & RenderFlags.Update) {
// Hack until we have local refs for templates
const rowTemplateRef = getOrCreateTemplateRef(getOrCreateNodeInjectorForNode(load(0)));
elementProperty(1, 'tpl', bind(rowTemplateRef));
elementProperty(1, 'rows', bind(parent.rows));
}
function rowTemplate(rf1: RenderFlags, row: any, parent: any) {
if (rf1 & RenderFlags.Create) {
container(0, cellTemplate);
elementStart(1, 'loop-comp');
elementEnd();
}
if (rf1 & RenderFlags.Update) {
// Hack until we have local refs for templates
const cellTemplateRef =
getOrCreateTemplateRef(getOrCreateNodeInjectorForNode(load(0)));
elementProperty(1, 'tpl', bind(cellTemplateRef));
elementProperty(1, 'rows', bind(row.$implicit.data));
}
}
function cellTemplate(rf1: RenderFlags, cell: any, row: any, parent: any) {
if (rf1 & RenderFlags.Create) {
elementStart(0, 'div');
{ text(1); }
elementEnd();
}
if (rf1 & RenderFlags.Update) {
textBinding(
1, interpolation3(
'', cell.$implicit, ' - ', row.$implicit.value, ' - ', parent.name, ''));
}
}
}, [LoopComp]);
const fixture = new ComponentFixture(Parent);
fixture.component.name = 'Parent';
fixture.component.rows =
[{data: ['1', '2'], value: 'one'}, {data: ['3', '4'], value: 'two'}];
fixture.update();
expect(fixture.html)
.toEqual(
'<loop-comp>' +
'<loop-comp><div>1 - one - Parent</div><div>2 - one - Parent</div></loop-comp>' +
'<loop-comp><div>3 - two - Parent</div><div>4 - two - Parent</div></loop-comp>' +
'</loop-comp>');
fixture.component.rows = [{data: ['5', '6'], value: 'three'}, {data: ['7'], value: 'four'}];
fixture.component.name = 'New name!';
fixture.update();
expect(fixture.html)
.toEqual(
'<loop-comp>' +
'<loop-comp><div>5 - three - New name!</div><div>6 - three - New name!</div></loop-comp>' +
'<loop-comp><div>7 - four - New name!</div></loop-comp>' +
'</loop-comp>');
});
});
const rendererFactory = getRendererFactory2(document);
describe('detach', () => {