Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import { Directive, EmbeddedViewRef, Input, OnChanges, ChangeDetectorRef, SimpleChange, SimpleChanges, TemplateRef, ViewContainerRef, NgZone, Output, EventEmitter, inject } from '@angular/core';

import {
Directive,
EmbeddedViewRef,
Input,
OnChanges,
SimpleChange,
SimpleChanges,
TemplateRef,
ViewContainerRef,
Output,
EventEmitter,
inject
} from '@angular/core';
import { IBaseEventArgs } from 'igniteui-angular/core';

/**
Expand All @@ -10,9 +21,17 @@ import { IBaseEventArgs } from 'igniteui-angular/core';
standalone: true
})
export class IgxTemplateOutletDirective implements OnChanges {
public _viewContainerRef = inject(ViewContainerRef);
private _zone = inject(NgZone);
public cdr = inject(ChangeDetectorRef);
private readonly _viewContainerRef = inject(ViewContainerRef);

/**
* The embedded views cache. Collection is key-value paired.
* Key is the template type, value is another key-value paired collection
* where the key is the template id and value is the embedded view for the related template.
*/
private readonly _embeddedViewsMap: Map<string, Map<any, EmbeddedViewRef<any>>> = new Map();

private _viewRef!: EmbeddedViewRef<any>;


@Input() public igxTemplateOutletContext !: any;

Expand All @@ -30,67 +49,68 @@ export class IgxTemplateOutletDirective implements OnChanges {
@Output()
public beforeViewDetach = new EventEmitter<IViewChangeEventArgs>();

private _viewRef !: EmbeddedViewRef<any>;

/**
* The embedded views cache. Collection is key-value paired.
* Key is the template type, value is another key-value paired collection
* where the key is the template id and value is the embedded view for the related template.
*/
private _embeddedViewsMap: Map<string, Map<any, EmbeddedViewRef<any>>> = new Map();

public ngOnChanges(changes: SimpleChanges) {
const actionType: TemplateOutletAction = this._getActionType(changes);
const { actionType, cachedView } = this._getActionType(changes);

switch (actionType) {
case TemplateOutletAction.CreateView: this._recreateView(); break;
case TemplateOutletAction.MoveView: this._moveView(); break;
case TemplateOutletAction.UseCachedView: this._useCachedView(); break;
case TemplateOutletAction.UseCachedView: this._useCachedView(cachedView); break;
case TemplateOutletAction.UpdateViewContext: this._updateExistingContext(this.igxTemplateOutletContext); break;
}
}

public cleanCache() {
this._embeddedViewsMap.forEach((collection) => {
collection.forEach((item => {
public cleanCache(): void {
for (const collection of this._embeddedViewsMap.values()) {
for (const item of collection.values()) {
if (!item.destroyed) {
item.destroy();
}
}));
}
collection.clear();
});
}

this._embeddedViewsMap.clear();
}

public cleanView(tmplID) {
const embViewCollection = this._embeddedViewsMap.get(tmplID.type);
const embView = embViewCollection?.get(tmplID.id);
if (embView) {
embView.destroy();
this._embeddedViewsMap.get(tmplID.type).delete(tmplID.id);
public cleanView(templateId: { type: string; id: any }): void {
const viewCollection = this._embeddedViewsMap.get(templateId.type);
const view = viewCollection?.get(templateId.id);

if (view) {
view.destroy();
this._embeddedViewsMap.get(templateId.type).delete(templateId.id);
}
}

private _recreateView() {
const prevIndex = this._viewRef ? this._viewContainerRef.indexOf(this._viewRef) : -1;
private _recreateView(): void {
const prevIndex = this._viewContainerRef.indexOf(this._viewRef);

// detach old and create new
if (prevIndex !== -1) {
this.beforeViewDetach.emit({ owner: this, view: this._viewRef, context: this.igxTemplateOutletContext });
this._viewContainerRef.detach(prevIndex);
}

if (this.igxTemplateOutlet) {
this._viewRef = this._viewContainerRef.createEmbeddedView(
this.igxTemplateOutlet, this.igxTemplateOutletContext);
this.viewCreated.emit({ owner: this, view: this._viewRef, context: this.igxTemplateOutletContext });
const tmplId = this.igxTemplateOutletContext['templateID'];
if (tmplId) {
const templateId = this.igxTemplateOutletContext['templateID'];

if (templateId) {
// if context contains a template id, check if we have a view for that template already stored in the cache
// if not create a copy and add it to the cache in detached state.
// Note: Views in detached state do not appear in the DOM, however they remain stored in memory.
const resCollection = this._embeddedViewsMap.get(this.igxTemplateOutletContext['templateID'].type);
const res = resCollection?.get(this.igxTemplateOutletContext['templateID'].id);
if (!res) {
this._embeddedViewsMap.set(this.igxTemplateOutletContext['templateID'].type,
new Map([[this.igxTemplateOutletContext['templateID'].id, this._viewRef]]));
let resCollection = this._embeddedViewsMap.get(templateId.type);

if (!resCollection) {
resCollection = new Map();
this._embeddedViewsMap.set(templateId.type, resCollection);
}

if (!resCollection.has(templateId.id)) {
resCollection.set(templateId.id, this._viewRef);
}
}
}
Expand All @@ -100,16 +120,22 @@ export class IgxTemplateOutletDirective implements OnChanges {
// using external view and inserting it in current view.
const view = this.igxTemplateOutletContext['moveView'];
const owner = this.igxTemplateOutletContext['owner'];

if (view !== this._viewRef) {
if (owner._viewContainerRef.indexOf(view) !== -1) {
const viewIndex = owner._viewContainerRef.indexOf(view);
const viewRefIndex = this._viewContainerRef.indexOf(this._viewRef);

if (viewIndex !== -1) {
// detach in case view it is attached somewhere else at the moment.
this.beforeViewDetach.emit({ owner: this, view: this._viewRef, context: this.igxTemplateOutletContext });
owner._viewContainerRef.detach(owner._viewContainerRef.indexOf(view));
owner._viewContainerRef.detach(viewIndex);
}
if (this._viewRef && this._viewContainerRef.indexOf(this._viewRef) !== -1) {

if (this._viewRef && viewRefIndex !== -1) {
this.beforeViewDetach.emit({ owner: this, view: this._viewRef, context: this.igxTemplateOutletContext });
this._viewContainerRef.detach(this._viewContainerRef.indexOf(this._viewRef));
this._viewContainerRef.detach(viewRefIndex);
}

this._viewRef = view;
this._viewContainerRef.insert(view, 0);
this._updateExistingContext(this.igxTemplateOutletContext);
Expand All @@ -118,12 +144,9 @@ export class IgxTemplateOutletDirective implements OnChanges {
this._updateExistingContext(this.igxTemplateOutletContext);
}
}
private _useCachedView() {

private _useCachedView(cachedView: EmbeddedViewRef<any>) {
// use view for specific template cached in the current template outlet
const tmplID = this.igxTemplateOutletContext['templateID'];
const cachedView = tmplID ?
this._embeddedViewsMap.get(tmplID.type)?.get(tmplID.id) :
null;
// if view exists, but template has been changed and there is a view in the cache with the related template
// then detach old view and insert the stored one with the matching template
// after that update its context.
Expand All @@ -133,7 +156,7 @@ export class IgxTemplateOutletDirective implements OnChanges {
}

this._viewRef = cachedView;
const oldContext = this._cloneContext(cachedView.context);
const oldContext = {...cachedView.context};
this._viewContainerRef.insert(this._viewRef, 0);
this._updateExistingContext(this.igxTemplateOutletContext);
this.cachedViewLoaded.emit({ owner: this, view: this._viewRef, context: this.igxTemplateOutletContext, oldContext });
Expand All @@ -145,54 +168,41 @@ export class IgxTemplateOutletDirective implements OnChanges {
}

private _hasContextShapeChanged(ctxChange: SimpleChange): boolean {
const prevCtxKeys = Object.keys(ctxChange.previousValue || {});
const currCtxKeys = Object.keys(ctxChange.currentValue || {});
const prevKeys = new Set(Object.keys(ctxChange.previousValue || {}));
const currKeys = new Set(Object.keys(ctxChange.currentValue || {}));

if (prevCtxKeys.length === currCtxKeys.length) {
for (const propName of currCtxKeys) {
if (prevCtxKeys.indexOf(propName) === -1) {
return true;
}
}
return false;
} else {

if (prevKeys.size !== currKeys.size) {
return true;
}
}

private _updateExistingContext(ctx: any): void {
for (const propName of Object.keys(ctx)) {
this._viewRef.context[propName] = this.igxTemplateOutletContext[propName];
}
return currKeys.difference(prevKeys).size > 0;
}

private _cloneContext(ctx: any): any {
const clone = {};
for (const propName of Object.keys(ctx)) {
clone[propName] = ctx[propName];
}
return clone;
private _updateExistingContext(ctx: any): void {
Object.assign(this._viewRef.context, ctx);
}

private _getActionType(changes: SimpleChanges) {
private _getActionType(changes: SimpleChanges): { actionType: TemplateOutletAction; cachedView: EmbeddedViewRef<any> | null } {
const movedView = this.igxTemplateOutletContext['moveView'];
const tmplID = this.igxTemplateOutletContext['templateID'];
const cachedView = tmplID ?
this._embeddedViewsMap.get(tmplID.type)?.get(tmplID.id) :
const templateId = this.igxTemplateOutletContext['templateID'];
const cachedView = templateId ?
this._embeddedViewsMap.get(templateId.type)?.get(templateId.id) :
null;
const shouldRecreate = this._shouldRecreateView(changes);

if (movedView) {
// view is moved from external source
return TemplateOutletAction.MoveView;
return { actionType: TemplateOutletAction.MoveView, cachedView };
} else if (shouldRecreate && cachedView) {
// should recreate (template or context change) and there is a matching template in cache
return TemplateOutletAction.UseCachedView;
return { actionType: TemplateOutletAction.UseCachedView, cachedView };
} else if (!this._viewRef || shouldRecreate) {
// no view or should recreate
return TemplateOutletAction.CreateView;
return { actionType: TemplateOutletAction.CreateView, cachedView };
} else if (this.igxTemplateOutletContext) {
// has context, update context
return TemplateOutletAction.UpdateViewContext;
return { actionType: TemplateOutletAction.UpdateViewContext, cachedView };
}
}
}
Expand All @@ -212,8 +222,3 @@ export interface IViewChangeEventArgs extends IBaseEventArgs {
export interface ICachedViewLoadedEventArgs extends IViewChangeEventArgs {
oldContext: any;
}

/**
* @hidden
*/

1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"target": "ES2022",
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"typeRoots": [
"node_modules/@types"
Expand Down
Loading