I need to create a DRY modal abstraction using the angular material dialog component and wanted to use a factory function, creating a new material dialog with: new Modal(component, config, data)
I would like:
function signature I'd like to use to create these modals/dialogs:
openModal(): void {
const testData = { name: "dummy name", address: "dummy address" };
this.modalService.createModal(
new Modal(
// component to inject:
ChildComponent,
{
// modal options:
title: "the custom title"
},
{
// modal config:
data: { item: testData },
hasBackdrop: false,
autoFocus: false
},
{
// modal action "templates":
actionsRight: componentInstance.instance.modalFooterRightRef,
actionsLeft: componentInstance.instance.modalFooterLeftRef,
}
)
);
}
}
Can this be done with a material dialog?
Here's a stackblitz of the current effort: https://angular-dialog-abstraction.stackblitz.io
There's a feature module containing the implementation and a shared module for the abstraction.
Currently the modal:
√ opens
√ injects the component
√ receives data as expected
x works as expected: there is no title or action buttons.
import { Type } from "@angular/core";
import { MatDialogConfig } from "@angular/material/dialog";
import { ModalOptions, ModalTemplates } from "../interfaces/modal";
import { ModalSizeOptions } from "../enums/modal-size-options.enum";
export class Modal {
static defaultModalOptions: ModalOptions = {
cancel: true,
close: false,
footer: true,
size: ModalSizeOptions.MEDIUM,
title: "Attention"
};
static defaultModalConfig: MatDialogConfig = {
data: null,
ariaDescribedBy: null,
ariaLabel: null,
ariaLabelledBy: null,
autoFocus: true,
backdropClass: null,
closeOnNavigation: false,
componentFactoryResolver: null,
direction: "ltr",
disableClose: true,
hasBackdrop: false,
height: "",
id: "",
maxHeight: null,
maxWidth: null,
minHeight: null,
minWidth: null,
panelClass: "",
position: { top: "", bottom: "", left: "", right: "" },
restoreFocus: true,
role: null,
scrollStrategy: null,
viewContainerRef: null,
width: ""
};
static defaultTemplates: ModalTemplates = {
error: null,
header: null,
content: null,
actionsLeft: null,
actionsRight: null
};
public component: Type<any>;
public options = Modal.defaultModalOptions;
public dialog = Modal.defaultModalConfig;
public templates: ModalTemplates;
constructor(
component: Type<any>,
options?: ModalOptions,
dialog?: MatDialogConfig,
templates?: ModalTemplates
) {
this.component = component;
this.options = options
? Object.assign({}, Modal.defaultModalOptions, options)
: Modal.defaultModalOptions;
this.dialog = dialog
? Object.assign({}, Modal.defaultModalConfig, dialog)
: Modal.defaultModalConfig;
this.templates = templates
? Object.assign({}, Modal.defaultTemplates, templates)
: Modal.defaultTemplates;
}
}
Modal Component
import { Component, ComponentFactoryResolver, OnInit, Input } from "@angular/core";
import { Modal } from "../../models/modal";
@Component({
selector: "app-modal",
styleUrls: ["./modal.component.css"],
template: `
<div>
<header>
<h1 mat-dialog-title>{{ modal.options.title }}</h1>
<button mat-icon-button mat-dialog-close="true">
<i class="material-icons">clear</i>
</button>
</header>
<mat-dialog-content>
<ng-template #component></ng-template>
</mat-dialog-content>
<div *ngIf="modal.options.footer">
<footer class="flex justify-between w-full">
<div class="modalFooterLeft">
<mat-dialog-actions>
<ng-container
[ngTemplateOutlet]="modal.templates.actionsLeft"
></ng-container>
</mat-dialog-actions>
</div>
<div class="modalFooterRight">
<mat-dialog-actions>
<ng-container
[ngTemplateOutlet]="modal.templates.actionsRight"
></ng-container>
<button
*ngIf="modal.options.cancel"
mat-button
mat-dialog-close
cdkFocusInitial
> Cancel </button>
<button
*ngIf="modal.options.close"
mat-button
mat-dialog-close
> Close </button>
</mat-dialog-actions>
</div>
</footer>
</div>
</div>
`
})
export class ModalComponent implements OnInit {
@Input() public modal: Modal;
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
) {}
ngOnInit() {}
}
Modal Service
import { Injectable } from "@angular/core";
import { MatDialog, MatDialogConfig } from "@angular/material/dialog";
import { Modal } from "../models/modal";
@Injectable()
export class ModalService {
constructor(public dialog: MatDialog) {}
public createModal(modal?: Modal): any {
return this.openModal(modal.component, modal.dialog);
}
private openModal(component: any, config: MatDialogConfig): any {
return this.dialog.open(component, config);
}
}
Example of passing the action buttons as templates from the child component that is injected into the modal component:
<ng-template #actionsRight>
<button
mat-button color="primary"
[disabled]="f.pristine || !f.valid"
[loading]="this.state.loading"
(click)="createItem()"
>Create</button>
</ng-template>
<ng-template #actionsLeft>
<button
mat-button color="primary"
[disabled]="f.pristine || !f.valid"
[loading]="this.state.loading"
(click)="deleteItem()"
>Delete</button>
</ng-template>
Really appreciate any help, thanks.
I noticed that you're opening ChildComponent
directly.
this.openModal(modal.component, modal.dialog);
If you want to use ModalComponent
as a base wrapper for all dynamically created dialogs then you should open that component instead.
modal.service.ts
this.dialog.open(ModalComponent, {
...modal.dialog,
data: modal
});
Here I'm passing dialog related options to MatDialog as well as the whole your Modal
model as a data
. This data
will be available in ModalComponent
though DI.
modal.component.ts
export class ModalComponent implements OnInit {
constructor(
...
@Inject(MAT_DIALOG_DATA) public modal: Modal
) {}
Now, we're inside ModalComponent where we can dynamically create passed ChildComponent
into <ng-template #component></ng-template>
by using low-level Angular API:
export class ModalComponent implements OnInit {
@ViewChild("component", { read: ViewContainerRef, static: true })
componentTarget: ViewContainerRef;
actionsLeft: TemplateRef<any>;
actionsRight: TemplateRef<any>;
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
private inj: Injector,
@Inject(MAT_DIALOG_DATA) public modal: Modal
) {}
ngOnInit() {
const factory = this.componentFactoryResolver.resolveComponentFactory(
this.modal.component
);
const componentRef = this.componentTarget.createComponent(
factory,
null,
Injector.create({
providers: [
{
provide: MAT_DIALOG_DATA,
useValue: this.modal.dialog.data
}
],
parent: this.inj
})
);
After that you can event action buttons defined as templates in ChildComponent:
export class ModalComponent implements OnInit {
actionsLeft: TemplateRef<any>;
actionsRight: TemplateRef<any>;
...
ngOnInit() {
...
const componentRef = this.componentTarget.createComponent(
factory,
...
);
this.actionsLeft = componentRef.instance.actionsLeftRef;
this.actionsRight = componentRef.instance.actionsRightRef;
}
}