I've been googling for a long time to investigate the way of creating dynamic dialog components in Angular 2. Almost all of them suggest the following:
ComponentFactoryResolver
for dynamical creating components for displaying in opened dialogentryComponents
in app module for compliler to be aware of your custom component's factoriesThat is all great but in my project I have to implement a standalone modal service just like shlomiassaf/angular2-modal or angular2-material but without large amount of customizations and settings those libraries offer to end users. What could be the steps to create such functionality?
I figured out how to create a simple dialog box with angular 2 service. The main concept is to programmatically create an overlay then attach its hostView
to application then append it to the document.body
. Then create dialog box itself and using overlay reference append the dialog box to newly created overlay.
Overlay
import {
Component,
ChangeDetectionStrategy,
EventEmitter,
Output,
ViewChild,
ViewContainerRef
} from "@angular/core";
@Component ({
moduleId: module.id,
selector: "dialog-overlay",
template: `
<div class="dialog-overlay" (click)="onOverlayClick($event)">
<div #dialogPlaceholder ></div>
</div>
`,
styleUrls: ["./overlay.component.css"],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OverlayComponent {
@ViewChild("dialogPlaceholder", {read: ViewContainerRef})
public dialogPlaceholder: ViewContainerRef;
@Output()
public overlayClick: EventEmitter<any> = new EventEmitter();
public isOverlayOpen: boolean = false;
public onOverlayClick($event: Event): void {
const target: HTMLElement = <HTMLElement>$event.target;
if (target.className.indexOf("dialog-overlay") !== -1) {
this.overlayClick.emit();
}
}
}
Here we have a simple component with overlay styling (not included in the example) and a template variable dialogPlaceholder
which we will use for placing our dialog box.
Dialog component
import {
Component,
EventEmitter,
HostBinding,
Input,
Output,
ViewChild,
ViewContainerRef,
ChangeDetectionStrategy
} from "@angular/core";
@Component ({
moduleId: module.id,
selector: "dialog",
template: `
<div class="your-dialog-class">
<div class="your-dialog-title-class">{{title}}</div>
... whatever
<div #dynamicContent ></div>
... whatever
</div>
`
styleUrls: ["./dialog.component.css"],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DialogComponent {
@ViewChild("dynamicContent", {read: ViewContainerRef})
public dynamicContent: ViewContainerRef;
@Input()
public isOpen: boolean = false;
@Input()
public title: string;
@Output()
public isOpenChange: EventEmitter<any> = new EventEmitter();
public onCloseClick(event: Event): void {
event.preventDefault();
this.isOpenChange.emit(false);
}
}
This component will be programmatically created with service and its #dynamicContent
will be used as a placeholder for dialog content
Service
I won't include the whole listing of the service, only create
method just to show the main concept of dynamically created dialogs
public create(content: Type<any>,
params?: DialogParams): DialogService {
if (this.dialogComponentRef) {
this.closeDialog();
}
const dialogParams: DialogParams = Object.assign({}, this.dialogDefaultParams, params || {});
const overlayFactory: ComponentFactory<any> = this.componentFactoryResolver.resolveComponentFactory(OverlayComponent);
const dialogFactory: ComponentFactory<any> = this.componentFactoryResolver.resolveComponentFactory(DialogComponent);
// create an overlay
this.overlayComponentRef = overlayFactory.create(this.injector);
this.appRef.attachView(this.overlayComponentRef.hostView);
this.document.body.appendChild(
this.overlayComponentRef.location.nativeElement
);
this.overlayComponentRef.instance.isOverlayOpen = dialogParams.isModal;
// create dialog box inside an overlay
this.dialogComponentRef = this.overlayComponentRef
.instance
.dialogPlaceholder
.createComponent(dialogFactory);
this.applyParams(dialogParams);
this.dialogComponentRef.changeDetectorRef.detectChanges();
// content
this.contentRef = content ? this.attachContent(content, contentContext) : undefined;
const subscription: Subscription = this.dialogComponentRef.instance.isOpenChange.subscribe(() => {
this.closeDialog();
});
const overlaySubscription: Subscription = this.overlayComponentRef.instance.overlayClick.subscribe(() => {
if (dialogParams.closeOnOverlayClick) {
this.closeDialog();
}
});
this.subscriptionsForClose.push(subscription);
this.subscriptionsForClose.push(overlaySubscription);
return this;
}
// this method takes a component class with its context and attaches it to dialog box
private attachContent(content: any,
context: {[key: string]: any} = undefined): ComponentRef<any> {
const containerRef: ViewContainerRef = this.dialogComponentRef.instance.dynamicContent;
const factory: ComponentFactory<any> = this.componentFactoryResolver.resolveComponentFactory(content);
const componentRef: ComponentRef<any> = containerRef.createComponent(factory);
this.applyParams(context, componentRef);
componentRef.changeDetectorRef.detectChanges();
const { instance } = componentRef;
if (instance.closeEvent) {
const subscription: Subscription = componentRef.instance.closeEvent.subscribe(() => {
this.closeDialog();
});
this.subscriptionsForClose.push(subscription);
}
return componentRef;
}
// this method applies dialog parameters to dialog component.
private applyParams(inputs: {[key: string]: any}, component: ComponentRef<any> = this.dialogComponentRef): void {
if (inputs) {
const inputsKeys: Array<string> = Object.getOwnPropertyNames(inputs);
inputsKeys.forEach((name: string) => {
component.instance[name] = inputs[name];
});
}
}
public closeDialog(): void {
this.subscriptionsForClose.forEach(sub => {
sub.unsubscribe();
});
this.dialogComponentRef.destroy();
this.overlayComponentRef.destroy();
this.dialogComponentRef = undefined;
this.overlayComponentRef = undefined;
}
Although this approach is much simpler than in shlomiassaf/angular2-modal
or angular2-material
it requires a lot of work to be done. The whole method of dynamically creating components in service violates the separation of concerns principle, however talking about dynamically created dialogs it is more handy to create them on the fly than keeping them somewhere in templates.