Modal windows and notification in Angular

In Angular (version 2+), I was faced with the task of creating modal windows, but ready-made solutions did not suit me either because of the hard-coded functionality (inflexible), or they were not updated to the latest version and for this reason do not work. But wading through the jungle of official documentation, I decided to talk about two ways of working with modal windows (or notifications), which I considered the best.

In this article I want to talk about two ways to work with modal windows:

  1. "Normal" adding components
  2. Add components dynamically

In one of the articles on Habré, a good way, in my opinion, is given to solve this problem, but it stopped working after the introduction of NgModule (or maybe earlier). Material will overlap with this article, so I advise you to familiarize yourself with it.

I must say right away that there are several more ways to add modal windows, such as adding in the bootstrap style (it seems like 1 way, only in 1 way the modal window was taken out in a separate component), as well as no one bothers using typescript to add any directly to the dom modal window, although I do not like this method, but it exists.

In all examples, I will omit css and html in places where this will not affect the logic. A link to the source repository will be provided at the end of the article.

"Normal" adding components


First, create a component that will be a simple dialog box for confirmation:

@Component({
	           selector     : 'modal-dialog',
...
           })
export class ModalDialogComponent {
	@Input() header: string;
	@Input() description: string;
	@Output() isConfirmed: EventEmitter = new EventEmitter();
	private confirm() {
		this.isConfirmed.emit(true);
	}
	private close() {
		this.isConfirmed.emit(false);
	}
}

We created a component with the input values ​​header and description and in response we get one value of the boolean variable with the result of the window. If there is a need to return any values ​​from the modal window to the component that called it, you can create a class to represent the result of execution:

export class ModalDialogResult {
	public isConfirmed: boolean;
        public data:any;
}

And return data through it.

Now, to use the dialog box, we need to add it to a module. There are several ways:

  1. Combine modal windows into one module
  2. Add to the module where it will be used

For this method of creating a modal window, I chose to add it to the module where it will be used:

@NgModule({
	          imports     : [BrowserModule],
	          declarations: [SimpleModalPageComponent, ModalDialogComponent],
	          bootstrap   : [SimpleModalPageComponent]
          })
export class SimpleModalPageModule {
}

ModalDialogComponent is the dialog box component itself.
SimpleModalPageComponent - this is the component (hereinafter the components that have the word Page in the title I will call the pages), where we will display the dialog box.

Now add a modal window to the page template:


We will control the visibility of the modal window through ngIf. If desired, this logic can be moved inside the dialog box, or you can combine the button to display the window with the window itself in one component.

Page code for displaying the dialog box:

....
export class SimpleModalPageComponent {
	private isModalDialogVisible: boolean = false;
	public showDialog() {
		this.isModalDialogVisible = true;
	}
	public closeModal(isConfirmed: boolean) {
		this.isModalDialogVisible = false;
		...
	}
}

The dialog box is ready for use. To work with pop-up notifications (Toast, popup, etc.), the work will occur in a slightly different way. To work with notifications, you need a stack (if you need more than one pop-up message on the screen), which should be common to the entire application. Next, we will consider how this can be done.

To get started, let's create a service that will be responsible for access to the notification and the notification model:

@Injectable()
export class TinyNotificationService {
	private notifications: Subject = new Subject();
	public getNotifications(): Subject {
		return this.notifications;
	}
	public showToast(info: TinyNotificationModel) {
		this.notifications.next(info);
	}
}
export class TinyNotificationModel {
	public header: string;
	public description: string;
	constructor(header: string, description: string) {
		this.header      = header;
		this.description = description;
	}
}

In the model, we define the title and description. In the service, we defined a method for displaying notifications and a method for obtaining notification models.

Now we define the notification component:

@Component({
	           selector     : "notifications",
	           template     : `

{{notification.header}}

x
{{notification.description}}
` }) export class TinyNotificationComponent { notifications: Set = new Set(); constructor(private _notificationService: TinyNotificationService) { this._notificationService.getNotification() .subscribe((notification: TinyNotificationModel)=> { this.notifications.add(notification); setTimeout(()=> { this.closeNotification(notification); }, 5000); }); } public closeNotification(notification: TinyNotificationModel) { this.notifications.delete(notification); } }

In the constructor, we subscribe to the addition of the notification and set the automatic closing of the notification after 5 seconds.

To use such a notification, it is necessary to add a notification component, preferably as high as possible in the component hierarchy (in the main component).

For use, add pages to the template (SimpleModalPageComponent)


After that, it will be possible to call a notification through the service, for example, in the following way

...
	constructor(private notificationService: TinyNotificationService) {}
	public showToast(header: string, description: string) {
		this.notificationService.showToast(new TinyNotificationModel(header, description));
	}
...

Do not forget to add components and services to the modules.

Add components dynamically


I think I need to immediately say why I decided not to create the next fashionable and youth package in npm and just describe the approach for creating modal windows. The reason is that creating a universal package is difficult and still it will be suitable for a small number of users (I recall the story that an average and universal solution risks not being suitable for anyone).

And now let's move on to why I started writing this article. It is not possible to add a component dynamically "out of thin air" in Angular (most likely, but it is difficult and often runs the risk of breaking with updates). Therefore, everything should be explicitly defined somewhere (in my opinion, this is good).

To add components dynamically, it should be known where we plan to add them. To do this, we need to get an objectViewContainerRef .

You can get it in the following way:

@Component({
...
	           template: `
...
`, ... }) export class DynamicModalPageComponent implements OnInit { @ViewChild('notificationBlock', { read: ViewContainerRef }) notificationBlock: ViewContainerRef; constructor(private notificationManager: NotificationManager) { } public ngOnInit(): void { this.notificationManager.init(this.notificationBlock); } .. }

So we get the ViewContainerRef object. As you may have noticed, in addition to this object, we use the NotificationManager and initialize it with the value ViewContainerRef.

NotificationManager is designed to work with modal windows and notifications. Next we define this class:

@Injectable()
export class NotificationManager {
	private notificationBlock: ViewContainerRef;
...
	constructor(private componentFactoryResolver: ComponentFactoryResolver) { }
	public init(notificationBlock: ViewContainerRef) {
		this.notificationBlock = notificationBlock;
...
	}
...
	private createComponent(componentType: {new (...args: any[]): T;}): ComponentRef {
		const injector = ReflectiveInjector.fromResolvedProviders([], this.notificationBlock.parentInjector);
		const factory  = this.componentFactoryResolver.resolveComponentFactory(componentType);
		return factory.create(injector);
	}
	private createNotificationWithData(componentType: {new (...args: any[]): T;}, data: any): ComponentRef {
		const component = this.createComponent(componentType);
		Object.assign(component.instance, data);
		return component;
	}
}

In the previous listing, I intentionally skipped some parts of the code to introduce them after some explanation. Before adding a component anywhere we need to create it first. The createComponent and createNotificationWithData methods are internal methods of the class and are designed to create a component and initialize it with some data, respectively.

Consider the createComponent method:

private createComponent(componentType: {new (...args: any[]): T;}): ComponentRef {
		const injector = ReflectiveInjector.fromResolvedProviders([], this.notificationBlock.parentInjector);
		const factory  = this.componentFactoryResolver.resolveComponentFactory(componentType);
		return factory.create(injector);
	}

We input the component class, then we use the fromResolvedProviders method from ReflectiveInjector to get the ReflectiveInjector object. Next, through the ComponentFactoryResolver, create a factory for the component and actually create the component.

The createNotificationWithData method creates a component and adds data to it:

private createNotificationWithData(componentType: {new (...args: any[]): T;}, data: any): ComponentRef {
		const component = this.createComponent(componentType);
		Object.assign(component.instance, data);
		return component;
	}

After we have examined the methods for creating components, we need to consider how to use these objects. Add a method in the NotificationManager to display the modal window:

@Injectable()
export class NotificationManager {
...
	public showDialog(componentType: {new (...args: any[]): T;},
	                                             header: string,
	                                             description: string): Subject {
		const dialog = this.createNotificationWithData(componentType, {
			header     : header,
			description: description
		});
		this.notificationBlock.insert(dialog.hostView);
		const subject = dialog.instance.getDialogState();
		const sub     = subject.subscribe(x=> {
			dialog.destroy();
			sub.unsubscribe();
		});
		return subject;
	}
...
}

ModalDialogBase is the base class for the model. I will hide it under the spoiler along with ModalDialogResult

ModalDialogBase and ModalDialogResult
export abstract class ModalDialogBase {
	public abstract getDialogState(): Subject;
}
export enum ModalDialogResult{
	Opened,
	Confirmed,
	Closed
}


The showDialog method accepts a component class, data for its initialization, and returns Subject to obtain the result of modal window execution.

To add a component, use the insert method of notificationBlock

this.notificationBlock.insert(dialog.hostView);

This method adds a component and after that it will be displayed to the user. Through dialog.instance we get the component object and can access its methods and fields. For example, we can subscribe to receive a result and remove this dialog from dom after closing:

const subject = dialog.instance.getDialogState();
const sub     = subject.subscribe(x=> {
			dialog.destroy();
			sub.unsubscribe();
		});

If you call the destroy method on the ComponentRef object, the component will be deleted not only from dom, but also from notificationBlock, which is very convenient.

Under the spoiler modal window code:

Modalialog
@Component({
	           selector     : 'modal-dialog',
	           template     : `

`
           })
export class ModalDialogComponent extends ModalDialogBase {
	private header: string;
	private description: string;
	private modalState: Subject;
	constructor() {
		super();
		this.modalState = new Subject();
	}
	public getDialogState(): Subject {
		return this.modalState;
	}
	private confirm() {
		this.modalState.next(ModalDialogResult.Confirmed);
	}
	private close() {
		this.modalState.next(ModalDialogResult.Closed);
	}
}


Next, let's look at the creation of a notification. We can add it in the same way as modal windows, but in my opinion it is better to select them in a separate place, so let's create the NotificationPanelComponent component:

@Component({
	           selector     : 'notification-panel',
	           template     : `
}) export class NotificationPanelComponent { @ViewChild('notifications', { read: ViewContainerRef }) notificationBlock: ViewContainerRef; public showNotification(componentRef: ComponentRef, timeOut: number) { const toast = componentRef; this.notificationBlock.insert(toast.hostView); let subscription = toast.instance.getClosedEvent() .subscribe(()=> { this.destroyComponent(toast, subscription); }); setTimeout(()=> { toast.instance.close(); }, timeOut); } private destroyComponent(componentRef: ComponentRef, subscription: Subscription) { componentRef.destroy(); subscription.unsubscribe(); } }

In the showNotification method, we add a component to display, subscribe to the window close event and set a timeout to close the window. For simplicity, closure is implemented through the close method of the notification component.

All notifications must inherit from the NotificationBase class.

NotificationBase
export abstract class NotificationBase {
	protected closedEvent = new Subject();
	public getClosedEvent(){
		return this.closedEvent;
	}
	public abstract close(): void;
}


And here is the code of the notification component itself:

@Component({
	           selector     : 'tiny-notification',
	           template     : `

{{header}}

x
{{description}}
` }) export class TinyNotificationComponent extends NotificationBase { public header: string; public description: string; close() { this.closedEvent.next(); this.closedEvent.complete(); } }

To use notification, you must add the showToast and NotificationPanelComponent methods to the NotificationManager:

@Injectable()
export class NotificationManager {
	private notificationBlock: ViewContainerRef;
	private notificationPanel: NotificationPanelComponent;
	constructor(private componentFactoryResolver: ComponentFactoryResolver) { }
	public init(notificationBlock: ViewContainerRef) {
		this.notificationBlock = notificationBlock;
		const component          = this.createComponent(NotificationPanelComponent);
		this.notificationPanel = component.instance;
		this.notificationBlock.insert(component.hostView);
	}
...
	public showToast(header: string, description: string, timeOut: number = 3000) {
		const component = this.createNotificationWithData(TinyNotificationComponent, {
			header     : header,
			description: description
		});
		this.notificationPanel.showNotification(component, timeOut);
	}
...

If you try to do everything that was brought before this, then nothing will work, because there is a nuance, namely, how to combine all this into modules. For example, if you try to find information elsewhere, except for. documentation on NgModule , then you risk not seeing information about such a thing as entryComponents .

In the office. The documentation says:

entryComponents : Array|any[]>
Specifies a list of components that should be compiled when this module is defined. For each component listed here, Angular will create a ComponentFactory and store it in the ComponentFactoryResolver.

That is, if we want to create components through ComponentFactory and ComponentFactoryResolver, we need to specify our components in addition to declarations also in entryComponents.

Module example:

@NgModule({
 declarations   : [TinyNotificationComponent, NotificationPanelComponent, ModalDialogComponent],
entryComponents: [TinyNotificationComponent, NotificationPanelComponent, ModalDialogComponent],
providers      : [NotificationManager]
          })
export class NotificationModule {
}

Regarding the integration into modules. I consider it a good option to combine similar functionality of modal windows into modules and import them into NotificationModule.

Now to use modal windows you only need to specify the NotificationModule in imports and you can use it.

Usage example:

...
export class DynamicModalPageComponent implements OnInit {
	....
	constructor(private notificationManager: NotificationManager) { }
	public ngOnInit(): void {
		this.notificationManager.init(this.notificationBlock);
	}
	public showToast(header: string, description: string) {
		this.notificationManager.showToast(header, description, 3000);
	}
	public showDialog(header: string, description: string) {
		this.notificationManager.showDialog(ModalDialogComponent, header, description)
		    .subscribe((x: ModalDialogResult)=> {
			    if (x == ModalDialogResult.Confirmed) {
				    this.showToast(header, "modal dialog is confirmed");
			    }
			    else {
				    this.showToast(header, "modal dialog is closed");
			    }
		    });
	}
}

In this article, we looked at ways to create modal windows, including dynamically.

→ The source code for the article is in this repository .

Also popular now: