📜 ⬆️ ⬇️

Modal windows and notifications in Angular

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

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

  1. “Normal” add components
  2. Dynamic Add Components

In one of the articles on Habré, a good, in my opinion, way to solve this problem is given, but it stopped working after the introduction of the NgModule (and maybe earlier). With this article, the material will overlap, so I advise you to get acquainted with it.
')
I’ll say at once that there are several more ways to add modal windows, such as adding in bootstrap style (it looks like 1 method, only 1 method rendered a modal window into a separate component), just as no one bothers to use typescript to directly add any modal window, although this method does not like me, but it exists.

In all examples I will omit css and html in those places where it will not affect the logic. The link to the source code repository will be provided at the end of the article.

“Normal” add components


Create, for starters, 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<boolean> = new EventEmitter<boolean>(); private confirm() { this.isConfirmed.emit(true); } private close() { this.isConfirmed.emit(false); } } 

We have created a component with input values ​​header and description and in response we get one boolean variable with the result of the window. If you 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 the 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 actual component of the dialog box.
SimpleModalPageComponent is a component (hereinafter the components that have the name Page in the title will call pages), where we will display the dialog box.

Now add a modal window to the template page:

 <div class="container"> <div class="configuration"> <div> <label for="header">:</label> <input type="text" id="header" #header class="simple-input"> </div> <label for="description">:</label> <input type="text" id="description" #description content="description" class="simple-input"> </div> <div> <button class="simple-button" (click)="showDialog()">Show Dialog</button> </div> </div> <modal-dialog *ngIf="isModalDialogVisible" [header]="header.value" [description]="description.value" (isConfirmed)="closeModal($event)"></modal-dialog> 

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 into one component.

Page code to display 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 notification (Toast, popup, etc.) work will be a little different. To work with the notification, 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, consider how this can be done.

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

 @Injectable() export class TinyNotificationService { private notifications: Subject<TinyNotificationModel> = new Subject<TinyNotificationModel>(); public getNotifications(): Subject<TinyNotificationModel> { 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 a notification and a method for obtaining models of notification.

Now we define the notification component:

 @Component({ selector : "notifications", template : ` <div class="tiny-notification-panel"> <div *ngFor="let notification of notifications" class="tiny-notification"> <div class="header-block"> <h3 class="header-title">{{notification.header}}</h3> <a class="close-button" (click)="closeNotification(notification)">x</a> </div> <div class="content"> <span>{{notification.description}}</span> </div> </div> </div>` }) export class TinyNotificationComponent { notifications: Set<TinyNotificationModel> = new Set<TinyNotificationModel>(); 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 auto-close notification after 5 seconds.

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

To use, add to the template page (SimpleModalPageComponent)

 <notifications></notifications> 

After that, it will be possible to call the 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.

Dynamic Add Components


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

And now let's move on to why I started writing this article. Add a component dynamically "out of the air" in Angular will not work (most likely it is possible, but it is difficult and often breaks with updates). Therefore, everything must be somewhere clearly defined (in my opinion this is good).

To dynamically add components, we need to know where we plan to add them. To do this, we need to get a ViewContainerRef object.

You can get it as follows:

 @Component({ ... template: ` ... <section #notificationBlock></section> `, ... }) 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 a ViewContainerRef object. As you can see, in addition to this object, we use NotificationManager and initialize it with the value of 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<T>(componentType: {new (...args: any[]): T;}): ComponentRef<T> { const injector = ReflectiveInjector.fromResolvedProviders([], this.notificationBlock.parentInjector); const factory = this.componentFactoryResolver.resolveComponentFactory(componentType); return factory.create(injector); } private createNotificationWithData<T>(componentType: {new (...args: any[]): T;}, data: any): ComponentRef<T> { const component = this.createComponent(componentType); Object.assign(component.instance, data); return component; } } 

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

Consider the createComponent method:

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

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

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

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

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

 @Injectable() export class NotificationManager { ... public showDialog<T extends ModalDialogBase>(componentType: {new (...args: any[]): T;}, header: string, description: string): Subject<ModalDialogResult> { 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'll hide it under the spoiler with ModalDialogResult

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


The showDialog method takes a component class, data for its initialization, and returns a Subject to get the result of executing a modal window.

To add a component, use the insert method of the 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 a result and delete this dialog box from dom after closing:

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

If you call the ComponentRef object's destroy method, the component is removed not only from the dom, but also from the notificationBlock, which is very convenient.

Under the spoiler code modal window:

Modaldialog
 @Component({ selector : 'modal-dialog', template : ` <div class="modal-background"> <div class="container"> <div class="header-block"> <h3 class="header-title">{{header}}</h3> <a class="close-button" (click)="close()">x</a> </div> <div class="content"> <span>{{description}}</span> </div> <div class="action-block"> <button class="simple-button" (click)="confirm()"></button> <button class="simple-button" (click)="close()"></button> </div> </div> </div> ` }) export class ModalDialogComponent extends ModalDialogBase { private header: string; private description: string; private modalState: Subject<ModalDialogResult>; constructor() { super(); this.modalState = new Subject(); } public getDialogState(): Subject<ModalDialogResult> { return this.modalState; } private confirm() { this.modalState.next(ModalDialogResult.Confirmed); } private close() { this.modalState.next(ModalDialogResult.Closed); } } 


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

 @Component({ selector : 'notification-panel', template : ` <div class="notification-panel"> <div #notifications></div> </div> }) export class NotificationPanelComponent { @ViewChild('notifications', { read: ViewContainerRef }) notificationBlock: ViewContainerRef; public showNotification<T extends NotificationBase>(componentRef: ComponentRef<T>, 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<T extends NotificationBase>(componentRef: ComponentRef<T>, subscription: Subscription) { componentRef.destroy(); subscription.unsubscribe(); } } 

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

All notifications must be inherited from the NotificationBase class.

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


And we give the code of the notification component itself:

 @Component({ selector : 'tiny-notification', template : ` <div class="container"> <div class="header-block"> <h3 class="header-title">{{header}}</h3> <a class="close-button" (click)="close()">x</a> </div> <div class="content"> <span>{{description}}</span> </div> </div>` }) export class TinyNotificationComponent extends NotificationBase { public header: string; public description: string; close() { this.closedEvent.next(); this.closedEvent.complete(); } } 

To use notification, you need to add the showToast method and NotificationPanelComponent 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>(TinyNotificationComponent, { header : header, description: description }); this.notificationPanel.showNotification(component, timeOut); } ... 

If you try to do everything that was given before, 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 anywhere except in the office. documentation on NgModule , you run the risk of not seeing information about such a thing as entryComponents .

In the office. documentation is written:

 entryComponents : Array<Type<any>|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 the ComponentFactory and ComponentFactoryResolver, we need to specify our components in addition to declarations also in the entryComponents.

Module example:

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

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

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

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 .

Source: https://habr.com/ru/post/319386/


All Articles