diff --git a/packages/core/src/view/provider.ts b/packages/core/src/view/provider.ts index 7aa37d9162..83f1725caf 100644 --- a/packages/core/src/view/provider.ts +++ b/packages/core/src/view/provider.ts @@ -14,7 +14,7 @@ import {ViewContainerRef} from '../linker/view_container_ref'; import {ViewEncapsulation} from '../metadata/view'; import {Renderer as RendererV1, Renderer2, RendererFactory2, RendererType2} from '../render/api'; -import {createChangeDetectorRef, createInjector, createRendererV1, createTemplateRef, createViewContainerRef} from './refs'; +import {createChangeDetectorRef, createInjector, createRendererV1} from './refs'; import {BindingDef, BindingType, DepDef, DepFlags, DisposableFn, NodeData, NodeDef, NodeFlags, OutputDef, OutputType, ProviderData, QueryBindingType, QueryDef, QueryValueType, RootData, Services, ViewData, ViewDefinition, ViewFlags, ViewState, asElementData, asProviderData} from './types'; import {checkBinding, dispatchEvent, filterQueryId, isComponentView, splitMatchedQueriesDsl, tokenKey, viewParentEl} from './util'; @@ -356,10 +356,10 @@ export function resolveDep( case ElementRefTokenKey: return new ElementRef(asElementData(view, elDef.index).renderElement); case ViewContainerRefTokenKey: - return createViewContainerRef(view, elDef); + return asElementData(view, elDef.index).viewContainer; case TemplateRefTokenKey: { if (elDef.element.template) { - return createTemplateRef(view, elDef); + return asElementData(view, elDef.index).template; } break; } diff --git a/packages/core/src/view/query.ts b/packages/core/src/view/query.ts index b59b3ac103..f38bb1c7fc 100644 --- a/packages/core/src/view/query.ts +++ b/packages/core/src/view/query.ts @@ -11,7 +11,6 @@ import {QueryList} from '../linker/query_list'; import {TemplateRef} from '../linker/template_ref'; import {ViewContainerRef} from '../linker/view_container_ref'; -import {createTemplateRef, createViewContainerRef} from './refs'; import {NodeDef, NodeFlags, QueryBindingDef, QueryBindingType, QueryDef, QueryValueType, Services, ViewData, asElementData, asProviderData, asQueryList} from './types'; import {declaredViewContainer, filterQueryId, isEmbeddedView, viewParentEl} from './util'; @@ -141,8 +140,8 @@ function calcQueryValues( (nodeDef.element.template.nodeMatchedQueries & queryDef.filterId) === queryDef.filterId) { // check embedded views that were attached at the place of their template. const elementData = asElementData(view, i); - const embeddedViews = elementData.embeddedViews; - if (embeddedViews) { + if (nodeDef.flags & NodeFlags.EmbeddedViews) { + const embeddedViews = elementData.viewContainer._embeddedViews; for (let k = 0; k < embeddedViews.length; k++) { const embeddedView = embeddedViews[k]; const dvc = declaredViewContainer(embeddedView); @@ -151,7 +150,7 @@ function calcQueryValues( } } } - const projectedViews = elementData.projectedViews; + const projectedViews = elementData.template._projectedViews; if (projectedViews) { for (let k = 0; k < projectedViews.length; k++) { const projectedView = projectedViews[k]; @@ -180,10 +179,10 @@ export function getQueryValue( value = new ElementRef(asElementData(view, nodeDef.index).renderElement); break; case QueryValueType.TemplateRef: - value = createTemplateRef(view, nodeDef); + value = asElementData(view, nodeDef.index).template; break; case QueryValueType.ViewContainerRef: - value = createViewContainerRef(view, nodeDef); + value = asElementData(view, nodeDef.index).viewContainer; break; case QueryValueType.Provider: value = asProviderData(view, nodeDef.index).instance; diff --git a/packages/core/src/view/refs.ts b/packages/core/src/view/refs.ts index 6eb66f1159..1cc13a002b 100644 --- a/packages/core/src/view/refs.ts +++ b/packages/core/src/view/refs.ts @@ -18,7 +18,7 @@ import {Renderer as RendererV1, Renderer2} from '../render/api'; import {Type} from '../type'; import {VERSION} from '../version'; -import {ArgumentType, BindingType, DebugContext, DepFlags, ElementData, NodeCheckFn, NodeData, NodeDef, NodeFlags, RootData, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewState, asElementData, asProviderData, asTextData} from './types'; +import {ArgumentType, BindingType, DebugContext, DepFlags, ElementData, NodeCheckFn, NodeData, NodeDef, NodeFlags, RootData, Services, TemplateData, ViewContainerData, ViewData, ViewDefinition, ViewDefinitionFactory, ViewState, asElementData, asProviderData, asTextData} from './types'; import {isComponentView, markParentViewsForCheck, renderNode, resolveViewDefinition, rootRenderNodes, splitNamespace, tokenKey, viewParentEl} from './util'; import {attachEmbeddedView, detachEmbeddedView, moveEmbeddedView, renderDetachView} from './view_attach'; @@ -84,15 +84,17 @@ class ComponentRef_ extends ComponentRef { onDestroy(callback: Function): void { this._viewRef.onDestroy(callback); } } -export function createViewContainerRef(view: ViewData, elDef: NodeDef): ViewContainerRef { - return new ViewContainerRef_(view, elDef); +export function createViewContainerData( + view: ViewData, elDef: NodeDef, elData: ElementData): ViewContainerData { + return new ViewContainerRef_(view, elDef, elData); } -class ViewContainerRef_ implements ViewContainerRef { - private _data: ElementData; - constructor(private _view: ViewData, private _elDef: NodeDef) { - this._data = asElementData(_view, _elDef.index); - } +class ViewContainerRef_ implements ViewContainerData { + /** + * @internal + */ + _embeddedViews: ViewData[] = []; + constructor(private _view: ViewData, private _elDef: NodeDef, private _data: ElementData) {} get element(): ElementRef { return new ElementRef(this._data.renderElement); } @@ -109,7 +111,7 @@ class ViewContainerRef_ implements ViewContainerRef { } clear(): void { - const len = this._data.embeddedViews.length; + const len = this._embeddedViews.length; for (let i = len - 1; i >= 0; i--) { const view = detachEmbeddedView(this._data, i); Services.destroyView(view); @@ -117,7 +119,7 @@ class ViewContainerRef_ implements ViewContainerRef { } get(index: number): ViewRef { - const view = this._data.embeddedViews[index]; + const view = this._embeddedViews[index]; if (view) { const ref = new ViewRef_(view); ref.attachToViewContainerRef(this); @@ -126,7 +128,7 @@ class ViewContainerRef_ implements ViewContainerRef { return null; } - get length(): number { return this._data.embeddedViews.length; }; + get length(): number { return this._embeddedViews.length; }; createEmbeddedView(templateRef: TemplateRef, context?: C, index?: number): EmbeddedViewRef { @@ -153,13 +155,13 @@ class ViewContainerRef_ implements ViewContainerRef { } move(viewRef: ViewRef_, currentIndex: number): ViewRef { - const previousIndex = this._data.embeddedViews.indexOf(viewRef._view); + const previousIndex = this._embeddedViews.indexOf(viewRef._view); moveEmbeddedView(this._data, previousIndex, currentIndex); return viewRef; } indexOf(viewRef: ViewRef): number { - return this._data.embeddedViews.indexOf((viewRef)._view); + return this._embeddedViews.indexOf((viewRef)._view); } remove(index?: number): void { @@ -240,11 +242,16 @@ export class ViewRef_ implements EmbeddedViewRef, InternalViewRef { } } -export function createTemplateRef(view: ViewData, def: NodeDef): TemplateRef { +export function createTemplateData(view: ViewData, def: NodeDef): TemplateData { return new TemplateRef_(view, def); } -class TemplateRef_ extends TemplateRef { +class TemplateRef_ extends TemplateRef implements TemplateData { + /** + * @internal + */ + _projectedViews: ViewData[]; + constructor(private _parentView: ViewData, private _def: NodeDef) { super(); } createEmbeddedView(context: any): EmbeddedViewRef { @@ -273,11 +280,8 @@ class Injector_ implements Injector { export function nodeValue(view: ViewData, index: number): any { const def = view.def.nodes[index]; if (def.flags & NodeFlags.TypeElement) { - if (def.element.template) { - return createTemplateRef(view, def); - } else { - return asElementData(view, def.index).renderElement; - } + const elData = asElementData(view, def.index); + return def.element.template ? elData.template : elData.renderElement; } else if (def.flags & NodeFlags.TypeText) { return asTextData(view, def.index).renderText; } else if (def.flags & (NodeFlags.CatProvider | NodeFlags.TypePipe)) { diff --git a/packages/core/src/view/types.ts b/packages/core/src/view/types.ts index a734b6fbb0..c46fa05b22 100644 --- a/packages/core/src/view/types.ts +++ b/packages/core/src/view/types.ts @@ -355,12 +355,18 @@ export function asTextData(view: ViewData, index: number): TextData { export interface ElementData { renderElement: any; componentView: ViewData; - embeddedViews: ViewData[]; + viewContainer: ViewContainerData; + template: TemplateData; +} + +export interface ViewContainerData extends ViewContainerRef { _embeddedViews: ViewData[]; } + +export interface TemplateData extends TemplateRef { // views that have been created from the template // of this element, // but inserted into the embeddedViews of another element. // By default, this is undefined. - projectedViews: ViewData[]; + _projectedViews: ViewData[]; } /** diff --git a/packages/core/src/view/util.ts b/packages/core/src/view/util.ts index 885f7e197f..8b89bdbca8 100644 --- a/packages/core/src/view/util.ts +++ b/packages/core/src/view/util.ts @@ -293,11 +293,9 @@ function visitRenderNode( const rn = renderNode(view, nodeDef); execRenderNodeAction(view, rn, action, parentNode, nextSibling, target); if (nodeDef.flags & NodeFlags.EmbeddedViews) { - const embeddedViews = asElementData(view, nodeDef.index).embeddedViews; - if (embeddedViews) { - for (let k = 0; k < embeddedViews.length; k++) { - visitRootRenderNodes(embeddedViews[k], action, parentNode, nextSibling, target); - } + const embeddedViews = asElementData(view, nodeDef.index).viewContainer._embeddedViews; + for (let k = 0; k < embeddedViews.length; k++) { + visitRootRenderNodes(embeddedViews[k], action, parentNode, nextSibling, target); } } if (nodeDef.flags & NodeFlags.TypeElement && !nodeDef.element.name) { diff --git a/packages/core/src/view/view.ts b/packages/core/src/view/view.ts index d62fc3f0b6..26aaa08723 100644 --- a/packages/core/src/view/view.ts +++ b/packages/core/src/view/view.ts @@ -15,6 +15,7 @@ import {appendNgContent} from './ng_content'; import {callLifecycleHooksChildrenFirst, checkAndUpdateDirectiveDynamic, checkAndUpdateDirectiveInline, createDirectiveInstance, createPipeInstance, createProviderInstance} from './provider'; import {checkAndUpdatePureExpressionDynamic, checkAndUpdatePureExpressionInline, createPureExpression} from './pure_expression'; import {checkAndUpdateQuery, createQuery, queryDef} from './query'; +import {createTemplateData, createViewContainerData} from './refs'; import {checkAndUpdateTextDynamic, checkAndUpdateTextInline, createText} from './text'; import {ArgumentType, CheckType, ElementData, ElementDef, NodeData, NodeDef, NodeFlags, ProviderData, ProviderDef, RootData, Services, TextDef, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewHandleEventFn, ViewState, ViewUpdateFn, asElementData, asProviderData, asPureExpressionData, asQueryList, asTextData} from './types'; import {checkBindingNoChanges, isComponentView, resolveViewDefinition, viewParentEl} from './util'; @@ -255,9 +256,12 @@ function createViewNodes(view: ViewData) { nodeData = { renderElement: el, componentView, - embeddedViews: (nodeDef.flags & NodeFlags.EmbeddedViews) ? [] : undefined, - projectedViews: undefined + viewContainer: undefined, + template: nodeDef.element.template ? createTemplateData(view, nodeDef) : undefined }; + if (nodeDef.flags & NodeFlags.EmbeddedViews) { + nodeData.viewContainer = createViewContainerData(view, nodeDef, nodeData); + } break; case NodeFlags.TypeText: nodeData = createText(view, renderHost, nodeDef) as any; @@ -525,11 +529,9 @@ function execEmbeddedViewsAction(view: ViewData, action: ViewAction) { const nodeDef = def.nodes[i]; if (nodeDef.flags & NodeFlags.EmbeddedViews) { // a leaf - const embeddedViews = asElementData(view, i).embeddedViews; - if (embeddedViews) { - for (let k = 0; k < embeddedViews.length; k++) { - callViewAction(embeddedViews[k], action); - } + const embeddedViews = asElementData(view, i).viewContainer._embeddedViews; + for (let k = 0; k < embeddedViews.length; k++) { + callViewAction(embeddedViews[k], action); } } else if ((nodeDef.childFlags & NodeFlags.EmbeddedViews) === 0) { // a parent with leafs diff --git a/packages/core/src/view/view_attach.ts b/packages/core/src/view/view_attach.ts index 8e6ac818b4..82be6dd500 100644 --- a/packages/core/src/view/view_attach.ts +++ b/packages/core/src/view/view_attach.ts @@ -11,7 +11,7 @@ import {RenderNodeAction, declaredViewContainer, isComponentView, renderNode, ro export function attachEmbeddedView( parentView: ViewData, elementData: ElementData, viewIndex: number, view: ViewData) { - let embeddedViews = elementData.embeddedViews; + let embeddedViews = elementData.viewContainer._embeddedViews; if (viewIndex == null) { viewIndex = embeddedViews.length; } @@ -19,9 +19,9 @@ export function attachEmbeddedView( addToArray(embeddedViews, viewIndex, view); const dvcElementData = declaredViewContainer(view); if (dvcElementData && dvcElementData !== elementData) { - let projectedViews = dvcElementData.projectedViews; + let projectedViews = dvcElementData.template._projectedViews; if (!projectedViews) { - projectedViews = dvcElementData.projectedViews = []; + projectedViews = dvcElementData.template._projectedViews = []; } projectedViews.push(view); } @@ -33,7 +33,7 @@ export function attachEmbeddedView( } export function detachEmbeddedView(elementData: ElementData, viewIndex: number): ViewData { - const embeddedViews = elementData.embeddedViews; + const embeddedViews = elementData.viewContainer._embeddedViews; if (viewIndex == null || viewIndex >= embeddedViews.length) { viewIndex = embeddedViews.length - 1; } @@ -46,7 +46,7 @@ export function detachEmbeddedView(elementData: ElementData, viewIndex: number): const dvcElementData = declaredViewContainer(view); if (dvcElementData && dvcElementData !== elementData) { - const projectedViews = dvcElementData.projectedViews; + const projectedViews = dvcElementData.template._projectedViews; removeFromArray(projectedViews, projectedViews.indexOf(view)); } @@ -59,7 +59,7 @@ export function detachEmbeddedView(elementData: ElementData, viewIndex: number): export function moveEmbeddedView( elementData: ElementData, oldViewIndex: number, newViewIndex: number): ViewData { - const embeddedViews = elementData.embeddedViews; + const embeddedViews = elementData.viewContainer._embeddedViews; const view = embeddedViews[oldViewIndex]; removeFromArray(embeddedViews, oldViewIndex); if (newViewIndex == null) { diff --git a/packages/core/test/linker/regression_integration_spec.ts b/packages/core/test/linker/regression_integration_spec.ts index ada1445d2c..3c8bc89bff 100644 --- a/packages/core/test/linker/regression_integration_spec.ts +++ b/packages/core/test/linker/regression_integration_spec.ts @@ -6,8 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {ANALYZE_FOR_ENTRY_COMPONENTS, Component, InjectionToken, Injector, Pipe, PipeTransform, Provider, Renderer2} from '@angular/core'; +import {ANALYZE_FOR_ENTRY_COMPONENTS, Component, Directive, InjectionToken, Injector, Input, Pipe, PipeTransform, Provider, QueryList, Renderer2, TemplateRef, ViewChildren, ViewContainerRef} from '@angular/core'; import {TestBed} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; export function main() { @@ -222,6 +223,52 @@ function declareTests({useJit}: {useJit: boolean}) { const txtNode = ctx.componentInstance.renderer.createText('test'); expect(txtNode).toHaveText('test'); }); + + it('should not recreate TemplateRef references during dirty checking', () => { + @Component({template: '
'}) + class MyComp { + } + + @Directive({selector: '[someDir]'}) + class MyDir { + @Input('someDir') template: TemplateRef; + } + + const ctx = + TestBed.configureTestingModule({declarations: [MyComp, MyDir]}).createComponent(MyComp); + const dir = ctx.debugElement.query(By.directive(MyDir)).injector.get(MyDir); + + expect(dir.template).toBeUndefined(); + + ctx.detectChanges(); + const template = dir.template; + expect(template).toBeDefined(); + + ctx.detectChanges(); + expect(dir.template).toBe(template); + }); + + it('should not recreate ViewContainerRefs in queries', () => { + @Component({template: '
'}) + class MyComp { + @ViewChildren('vc', {read: ViewContainerRef}) + viewContainers: QueryList; + + show = true; + } + + const ctx = TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp); + + ctx.componentInstance.show = true; + ctx.detectChanges(); + expect(ctx.componentInstance.viewContainers.length).toBe(2); + const vc = ctx.componentInstance.viewContainers.first; + expect(vc).toBeDefined(); + + ctx.componentInstance.show = false; + ctx.detectChanges(); + expect(ctx.componentInstance.viewContainers.first).toBe(vc); + }); }); } diff --git a/packages/core/test/view/embedded_view_spec.ts b/packages/core/test/view/embedded_view_spec.ts index dd0699b772..9e0203582e 100644 --- a/packages/core/test/view/embedded_view_spec.ts +++ b/packages/core/test/view/embedded_view_spec.ts @@ -101,7 +101,7 @@ export function main() { moveEmbeddedView(viewContainerData, 0, 1); - expect(viewContainerData.embeddedViews).toEqual([childView1, childView0]); + expect(viewContainerData.viewContainer._embeddedViews).toEqual([childView1, childView0]); // 2 anchors + 2 elements const rootChildren = getDOM().childNodes(rootNodes[0]); expect(rootChildren.length).toBe(4); diff --git a/packages/core/test/view/ng_content_spec.ts b/packages/core/test/view/ng_content_spec.ts index 1ef265c66f..38f5694132 100644 --- a/packages/core/test/view/ng_content_spec.ts +++ b/packages/core/test/view/ng_content_spec.ts @@ -96,7 +96,7 @@ export function main() { const anchor = asElementData(view, 2); expect((getDOM().childNodes(getDOM().firstChild(rootNodes[0]))[0])) .toBe(anchor.renderElement); - const embeddedView = anchor.embeddedViews[0]; + const embeddedView = anchor.viewContainer._embeddedViews[0]; expect((getDOM().childNodes(getDOM().firstChild(rootNodes[0]))[1])) .toBe(asTextData(embeddedView, 0).renderText); }); diff --git a/packages/core/test/view/query_spec.ts b/packages/core/test/view/query_spec.ts index faa287bbce..4417abfc6b 100644 --- a/packages/core/test/view/query_spec.ts +++ b/packages/core/test/view/query_spec.ts @@ -353,7 +353,8 @@ export function main() { } const {view} = createAndGetRootNodes(compViewDef([ - anchorDef(NodeFlags.None, [[someQueryId, QueryValueType.ViewContainerRef]], null, 2), + anchorDef( + NodeFlags.EmbeddedViews, [[someQueryId, QueryValueType.ViewContainerRef]], null, 2), directiveDef(NodeFlags.None, null, 1, QueryService, []), queryDef( NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,