UNPKG

igniteui-angular-sovn

Version:

Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps

222 lines (195 loc) 9.21 kB
import { Directive, EmbeddedViewRef, Input, OnChanges, ChangeDetectorRef, SimpleChange, SimpleChanges, TemplateRef, ViewContainerRef, NgZone, Output, EventEmitter } from '@angular/core'; import { IBaseEventArgs } from '../../core/utils'; /** * @hidden */ @Directive({ selector: '[igxTemplateOutlet]', standalone: true }) export class IgxTemplateOutletDirective implements OnChanges { @Input() public igxTemplateOutletContext !: any; @Input() public igxTemplateOutlet !: TemplateRef<any>; @Output() public viewCreated = new EventEmitter<IViewChangeEventArgs>(); @Output() public viewMoved = new EventEmitter<IViewChangeEventArgs>(); @Output() public cachedViewLoaded = new EventEmitter<ICachedViewLoadedEventArgs>(); @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(); constructor(public _viewContainerRef: ViewContainerRef, private _zone: NgZone, public cdr: ChangeDetectorRef) { } public ngOnChanges(changes: SimpleChanges) { const actionType: TemplateOutletAction = 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.UpdateViewContext: this._updateExistingContext(this.igxTemplateOutletContext); break; } } public cleanCache() { this._embeddedViewsMap.forEach((collection) => { collection.forEach((item => { 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); } } private _recreateView() { const prevIndex = this._viewRef ? this._viewContainerRef.indexOf(this._viewRef) : -1; // 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) { // 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]])); } } } } private _moveView() { // 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) { // 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)); } if (this._viewRef && this._viewContainerRef.indexOf(this._viewRef) !== -1) { this.beforeViewDetach.emit({ owner: this, view: this._viewRef, context: this.igxTemplateOutletContext }); this._viewContainerRef.detach(this._viewContainerRef.indexOf(this._viewRef)); } this._viewRef = view; this._viewContainerRef.insert(view, 0); this._updateExistingContext(this.igxTemplateOutletContext); this.viewMoved.emit({ owner: this, view: this._viewRef, context: this.igxTemplateOutletContext }); } else { this._updateExistingContext(this.igxTemplateOutletContext); } } private _useCachedView() { // 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. if (this._viewContainerRef.length > 0) { this.beforeViewDetach.emit({ owner: this, view: this._viewRef, context: this.igxTemplateOutletContext }); this._viewContainerRef.detach(this._viewContainerRef.indexOf(this._viewRef)); } this._viewRef = cachedView; const oldContext = this._cloneContext(cachedView.context); this._viewContainerRef.insert(this._viewRef, 0); this._updateExistingContext(this.igxTemplateOutletContext); this.cachedViewLoaded.emit({ owner: this, view: this._viewRef, context: this.igxTemplateOutletContext, oldContext }); } private _shouldRecreateView(changes: SimpleChanges): boolean { const ctxChange = changes['igxTemplateOutletContext']; return !!changes['igxTemplateOutlet'] || (ctxChange && this._hasContextShapeChanged(ctxChange)); } private _hasContextShapeChanged(ctxChange: SimpleChange): boolean { const prevCtxKeys = Object.keys(ctxChange.previousValue || {}); const currCtxKeys = Object.keys(ctxChange.currentValue || {}); if (prevCtxKeys.length === currCtxKeys.length) { for (const propName of currCtxKeys) { if (prevCtxKeys.indexOf(propName) === -1) { return true; } } return false; } else { return true; } } private _updateExistingContext(ctx: any): void { for (const propName of Object.keys(ctx)) { this._viewRef.context[propName] = this.igxTemplateOutletContext[propName]; } } private _cloneContext(ctx: any): any { const clone = {}; for (const propName of Object.keys(ctx)) { clone[propName] = ctx[propName]; } return clone; } private _getActionType(changes: SimpleChanges) { const movedView = this.igxTemplateOutletContext['moveView']; const tmplID = this.igxTemplateOutletContext['templateID']; const cachedView = tmplID ? this._embeddedViewsMap.get(tmplID.type)?.get(tmplID.id) : null; const shouldRecreate = this._shouldRecreateView(changes); if (movedView) { // view is moved from external source return TemplateOutletAction.MoveView; } else if (shouldRecreate && cachedView) { // should recreate (template or context change) and there is a matching template in cache return TemplateOutletAction.UseCachedView; } else if (!this._viewRef || shouldRecreate) { // no view or should recreate return TemplateOutletAction.CreateView; } else if (this.igxTemplateOutletContext) { // has context, update context return TemplateOutletAction.UpdateViewContext; } } } enum TemplateOutletAction { CreateView, MoveView, UseCachedView, UpdateViewContext } export interface IViewChangeEventArgs extends IBaseEventArgs { owner: IgxTemplateOutletDirective; view: EmbeddedViewRef<any>; context: any; } export interface ICachedViewLoadedEventArgs extends IViewChangeEventArgs { oldContext: any; } /** * @hidden */