{tocify} $title={Table of Content}
Code Snippet
popup.service.tsimport { ComponentFactoryResolver, ComponentRef, Type, ViewContainerRef } from '@angular/core';
/**
* This service is used to dynamically render the component
*/
export class PopupService<T> {
private _componentRef: ComponentRef<T> | null = null;
constructor(
private _component: Type<T>,
private _viewContainerRef: ViewContainerRef,
private _componentFactoryResolver: ComponentFactoryResolver,
) {
}
open(): ComponentRef<T> {
if (!this._componentRef) {
this._componentRef = this._viewContainerRef.createComponent(
this._componentFactoryResolver.resolveComponentFactory<T>(this._component),
this._viewContainerRef.length,
this._viewContainerRef.injector
);
(this._componentRef.instance as any).dismiss = this.close.bind(this);
}
return this._componentRef;
}
close(): void {
if (this._componentRef) {
this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._componentRef.hostView));
this._componentRef = null;
}
}
closeAll(): void {
if (this._componentRef) {
this._viewContainerRef.clear();
this._componentRef = null;
}
}
}
Cách sử dụng
1. Trong Dynamic Component khai báo một dismiss property sử dụng để destroy Dynamic Component
dialog.component.tsdismiss!: () => void;
2. Load Dynamic Component từ Container Component
Cách 1: Load Component theo cách thông thường
app.component.tsimport { ChangeDetectionStrategy, Component, ComponentFactoryResolver, OnInit, ViewContainerRef } from '@angular/core';
import { Observable } from 'rxjs';
import { DialogComponent } from './components/dialog/dialog.component';
import { PopupService } from './popup.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
changeDetection: ChangeDetectionStrategy.Default
})
export class AppComponent implements OnInit {
dialogComponent!: PopupService<any>;
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
private viewContainerRef: ViewContainerRef,
) {
}
ngOnInit() { }
openDialogComponent() {
if (!this.dialogComponent) {
this.dialogComponent = new PopupService<DialogComponent>(
DialogComponent, this.viewContainerRef, this.componentFactoryResolver
);
}
const dialogComponentRef = this.dialogComponent.open();
// Pass data to @Input() data of Dialog Component
dialogComponentRef.instance.data = 'Test Dialog';
// Get data from @Output() onSubmit: EventEmitter<any> = new EventEmitter<any>(); of Dialog Component
(dialogComponentRef.instance.onSubmit as Observable<any>).subscribe((value: any) => {
console.log(value);
})
}
closeDialogComponent() {
if (this.dialogComponent) {
this.dialogComponent.close();
}
}
}
Cách 2: Lazy load dynamic component dùng async/await 💨 Remove import Dialog Component ở đầu file và Update openDialogComponent ở cách 1 như sau:
app.component.ts async openDialogComponent() {
// Lazy load component dynamic
const { DialogComponent } = await import('./components/dialog/dialog.component');
this.dialogComponent = new PopupService(
DialogComponent, this.viewContainerRef, this.componentFactoryResolver
);
const dialogComponentRef = this.dialogComponent.open();
// Pass data to @Input() data of Dialog Component
dialogComponentRef.instance.data = 'Test Dialog';
// Get data from @Output() onSubmit: EventEmitter<any> = new EventEmitter<any>(); of Dialog Component
(dialogComponentRef.instance.onSubmit as Observable<any>).subscribe((value: any) => {
console.log(value);
})
}
Cách 3: Lazy load dynamic component sử dụng Observable:
app.component.tsimport { ChangeDetectionStrategy, Component, ComponentFactoryResolver, ElementRef, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import { from, Observable, Subject, Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
import { PopupService } from './popup.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
changeDetection: ChangeDetectionStrategy.Default
})
export class AppComponent implements OnInit {
@ViewChild('btnOpen', { static: true }) btnOpen!: ElementRef;
dynamicComponent: PopupService<any> | null = null;
stop$: Subject<Event> = new Subject();
dynamicSubs!: Subscription;
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
private viewContainerRef: ViewContainerRef,
) {
}
ngOnInit() {
}
loadDialogComponent(): Promise<any> {
return import('./components/dialog/dialog.component').then(
m => m.DialogComponent
);
}
openDialogComponent() {
// Lazy load component dynamic
from(this.loadDialogComponent()).pipe(
map((component: any) => new PopupService(component, this.viewContainerRef, this.componentFactoryResolver))
).subscribe(popupService => {
if (!this.dynamicComponent) {
this.dynamicComponent = popupService;
}
const dialogComponentRef = this.dynamicComponent.open();
this.dynamicSubs && this.dynamicSubs.unsubscribe();
if (dialogComponentRef) {
// Pass data to @Input() data of Dialog Component
dialogComponentRef.instance.data = 'Test Dialog';
// Get data from @Output() onSubmit: EventEmitter<any> = new EventEmitter<any>(); of Dialog Component
this.dynamicSubs = (dialogComponentRef.instance.onSubmit as Observable<any>).subscribe((value: any) => {
console.log(value);
});
}
});
}
closeDialogComponent() {
if (this.dynamicComponent) {
this.dynamicComponent.closeAll();
}
}
ngOnDestroy() {
this.dynamicComponent = null;
}
}
3. Cập nhật popup.service.ts để truyền projection content vào dynamic component.
popup.service.tsimport {
ApplicationRef,
ComponentFactoryResolver,
ComponentRef,
Injectable,
Injector,
Renderer2,
TemplateRef,
ViewContainerRef,
ViewRef
} from '@angular/core';
export interface ProjectionContent {
templateRef: TemplateRef<any>;
context: any;
}
export class ContentRef {
constructor(public nodes: any[], public viewRef?: ViewRef[], public componentRef?: ComponentRef<any>) { }
}
@Injectable({
providedIn: 'root'
})
/**
* This service is used to dynamically render the component
*/
export class PopupService {
private _componentRef: ComponentRef<any> | null = null;
private _contentRef: ContentRef = new ContentRef([]);
private _component: any;
private _viewContainerRef: ViewContainerRef | null = null;
private _renderer: Renderer2 | null = null;
private _applicationRef: ApplicationRef | null = null;
constructor(
private _componentFactoryResolver: ComponentFactoryResolver,
private _injector: Injector,
) {
}
/**
* Lazy load dynamic component. You need to use async function to lazy load component.
* @param componentLoader
* componentLoader is a Promise to lazy load component
*
* Example:
*
* loadDialogComponent(): Promise<any> {
* return import('./components/dialog/dialog.component').then(
* m => m.DialogComponent
* );
* }
*
* @param containerRef : Change viewContainerRef
*/
async loadComponent(componentLoader: Promise<any>, containerRef?: ViewContainerRef) {
this._component = await componentLoader;
if (containerRef) {
this._viewContainerRef = containerRef;
}
}
/**
* You have to load this function first in case you need to run loadProjectionContent function.
* @param renderer Renderer2 is injected to contructor of component container.
* @param applicationRef ApplicationRef is injected to contructor of component container.
*/
initializeContentService(renderer: Renderer2, applicationRef: ApplicationRef) {
this._renderer = renderer;
this._applicationRef = applicationRef;
}
/**
* Add the projection content to component
* @param projectionContent
* This parameter is an array of the ProjectionContent[{templateRef, context}] in the order of ng-content defined in dynamic component
*
* @usageNote
* In dynamic component template you defined:
*
* Example 1:
* **************************************************************************
* * <ng-content></ng-content>
* *
* * You will pass [TemplateRef]
* * Where:
* * TemplateRef is a reference to the template defined by ng-template
* **************************************************************************
*
* Example 2:
*
* **************************************************************************
* * <ng-content selector="header"></ng-content>
* * <ng-content selector="content"></ng-content>
* * <ng-content selector="footer"></ng-content>
* *
* * You will pass [TemplateRef of <ng-template #header></ng-template>, TemplateRef of <ng-template #header></ng-template>, TemplateRef of <ng-template #header></ng-template>]
* *
* * If you only want to pass projection content for header and footer:
* * [TemplateRef of <ng-template #header let-value="value"></ng-template>, null, TemplateRef of <ng-template #header></ng-template>]
* * ===> [{templateRef: headerRef, context: {value: 'a'}}, null, {templateRef: footerRef}]
* **************************************************************************
*
*/
loadProjectionContent(projectionContent?: string | ProjectionContent[] | any[]) {
if (!this._renderer || !this._applicationRef) {
console.warn('You have to call "initializeContentService" function before running "loadProjectionContent" function.');
} else {
this._contentRef = this._getContentRef(projectionContent);
}
}
open(): ComponentRef<any> {
if (!this._component) {
console.warn('You have to call "loadComponent" function before running "open" function.');
}
if (!this._componentRef && this._component && this._viewContainerRef) {
this._componentRef = this._viewContainerRef.createComponent(
this._componentFactoryResolver.resolveComponentFactory<any>(this._component),
this._viewContainerRef?.length,
this._injector,
this._contentRef.nodes,
);
(this._componentRef.instance as any).dismiss = this.close.bind(this);
}
return this._componentRef as ComponentRef<any>;
}
close(): void {
if (this._componentRef && this._viewContainerRef) {
this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._componentRef.hostView));
this._componentRef = null;
}
if (this._contentRef?.viewRef) {
this._contentRef.viewRef?.forEach((viewRef: ViewRef) => {
this._applicationRef?.detachView(viewRef);
viewRef?.destroy();
});
this._contentRef = new ContentRef([]);;
}
}
closeAll(): void {
if (this._componentRef && this._viewContainerRef) {
this._viewContainerRef.clear();
this._componentRef = null;
}
if (this._contentRef?.viewRef) {
this._contentRef.viewRef?.forEach((viewRef: ViewRef) => {
this._applicationRef?.detachView(viewRef);
viewRef?.destroy();
});
this._contentRef = new ContentRef([]);;
}
}
clear() {
this._componentRef = null;
this._contentRef = new ContentRef([]);;
this._component = null;
this._viewContainerRef = null;
this._renderer = null;
this._applicationRef = null;
}
private _getContentRef(content?: string | ProjectionContent[] | any[]): ContentRef {
if (!content) { // If content is false
return new ContentRef([]);
} else if (content instanceof Array) {
const viewRefs: any[] = [];
const projectionNodes = content.map((projectionContent: ProjectionContent) => {
if (!projectionContent) {
return projectionContent;
}
const { templateRef, context = {} } = projectionContent;
// Instantiates an embedded view and return a reference as ViewRef
const viewRef = templateRef.createEmbeddedView(context);
// ApplicationRef contains reference to the root view and can be used to manually run change detection using tick function.
// But since this component is not child of any other component, you have to manually attach it to ApplicationRef so you get change detection.
// Attach the view to the ApplicationRef so that you get change detection. You will still have no Input and ngOnChanges operations, but the DOM update will be working fine.
this._applicationRef?.attachView(viewRef);
return viewRef.rootNodes;
});
// Return the root node under which it should be added.
return new ContentRef(projectionNodes, viewRefs);
} else { // If content is a string
return new ContentRef([[this._renderer?.createText(`${content}`)]]);
}
}
}
💨 Cách sử dụng:
app.component.tsimport { ApplicationRef, ChangeDetectionStrategy, Component, ComponentFactoryResolver, ElementRef, EventEmitter, Injector, OnInit, Renderer2, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core';
import { Observable, Subject, Subscription } from 'rxjs';
import { PopupService } from './popup.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
changeDetection: ChangeDetectionStrategy.Default
})
export class AppComponent implements OnInit {
@ViewChild('btnOpen', { static: true }) btnOpen!: ElementRef;
@ViewChild('dialogHeader', { static: true }) dialogHeader!: TemplateRef<any>;
@ViewChild('dialogContent', { static: true, read: TemplateRef }) dialogContent!: TemplateRef<any>;
@ViewChild('dialogFooter', { static: true, read: TemplateRef }) dialogFooter!: TemplateRef<any>;
stop$: Subject<Event> = new Subject();
dynamicSubs!: Subscription;
submitDialog: EventEmitter<string> = new EventEmitter();
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
private viewContainerRef: ViewContainerRef,
private injector: Injector,
private renderer: Renderer2,
private applicationRef: ApplicationRef,
private popupService: PopupService
) {
}
ngOnInit() {
}
get modalService() {
return this.popupService;
}
loadDialogComponent(): Promise<any> {
return import('./components/dialog/dialog.component').then(
m => m.DialogComponent
);
}
async openDialogComponent() {
await this.popupService.loadComponent(this.loadDialogComponent(), this.viewContainerRef);
this.popupService.initializeContentService(this.renderer, this.applicationRef);
this.popupService.loadProjectionContent(
[
{ templateRef: this.dialogHeader },
{ templateRef: this.dialogContent },
{ templateRef: this.dialogFooter },
null, null
]
);
const dialogComponentRef = this.popupService.open();
this.dynamicSubs && this.dynamicSubs.unsubscribe();
if (dialogComponentRef) {
// Pass data to @Input() data of Dialog Component
dialogComponentRef.instance.data = 'Test Dialog';
// Get data from @Output() onSubmit: EventEmitter<any> = new EventEmitter<any>(); of Dialog Component
this.dynamicSubs = (dialogComponentRef.instance.onSubmit as Observable<any>).subscribe((value: any) => {
console.log(value);
});
}
}
closeDialogComponent() {
if (this.popupService) {
this.popupService.closeAll();
}
}
ngOnDestroy() {
this.popupService.clear();
}
}
app.component.html<button #btnOpen (click)="openDialogComponent()">Open Dialog</button>
<button (click)="closeDialogComponent()">Close Dialog</button>
<button>counter</button>
<ng-template #dialogHeader let-value="value">Dialog header {{value}}</ng-template>
<ng-template #dialogContent>
<div>This is a content body of the dialog</div>
</ng-template>
<ng-template #dialogFooter>
<button (click)="modalService.close()">Close</button>
</ng-template>
dialog.component.html<div #dialogTest class="modal modal-show" tabindex="-1" role="dialog">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<ng-content select=".header"></ng-content>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" (click)="dismiss()"></button>
</div>
<div class="modal-body">
{{data}}
<ng-content select=".content"></ng-content>
</div>
<div class="modal-footer">
<button (click)="submit()">Submit</button>
<ng-content select=".footer"></ng-content>
</div>
<div>
<ng-content select="[custom-attribute]"></ng-content>
</div>
</div>
</div>
<ng-content></ng-content>
</div>
dialog.component.tsimport { ChangeDetectionStrategy, Component, ContentChild, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { AlertService } from 'src/app/alert.service';
@Component({
selector: 'app-dialog',
templateUrl: './dialog.component.html',
styleUrls: ['./dialog.component.css'],
exportAs: 'appDialog',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DialogComponent implements OnInit {
_data: any = '';
@Input()
get data() {
return this._data;
}
set data(value: any) {
this._data = value;
}
@Output() onSubmit: EventEmitter<any> = new EventEmitter<any>();
dismiss!: () => void;
constructor(private alertService: AlertService) {
}
ngOnInit() { }
submit() {
this.onSubmit.emit('Submitted. Please wait...');
}
}
Demo: Stackblitz