How to create a reusable Modal Dialog component in Angular 8

José Fernando Costa
Level Up Coding
Published in
14 min readDec 30, 2019

--

Last month I wrote an article on how to create a modal dialog component in Angular 8. Through a simple demo, I explained how to create a dialog that “locks” the user into choosing one of two options when trying to log out: confirm the action or cancel and return to the normal use of the application.

Today, we will be making that dialog component reusable, under the same premise: clicking a button opens the modal dialog with confirmation and cancellation options. This means that in the future when the application needs a similar confirmation dialog for another purpose, we can just change the data and information received by the modal and we’re good to go, no need to create a new component.

Let’s get into the details of this demo!

About this demo

Instead of having a single logout button in the app-root component, we’ll have two buttons: one for the logout and another to delete an imaginary product. Each button opens the exact same modal dialog component, but with different information displayed. This component can be opened from any parent component, but for the sake of simplicity, we’ll have both the logout and the product deletion modals in the app-root.

app-root component with the new buttons
app-root component with the new buttons
modal component for the logout confirmation
modal component for the logout confirmation
modal component for the product deletion confirmation
modal component for the product deletion confirmation

When the confirmation buttons are clicked in the modal, an external service is called, modal-actions. This service is then responsible for calling a second service that fulfills the action confirmed by the user. If this first explanation is confusing, please look at the following diagram to see how data flows in this process:

Flow of the modal data
flow of the modal dialog MatDialogConfig.data object

As you can see, the component that calls the modal (app-root in our case) passes it information through the data object attribute of the dialog configurations (MatDialogConfig). When the confirmation button is clicked, the modal passes the same data object to the modal-actions service so that it can read the name of the modal, an attribute included in the object. The point of reading the name is so that modal-actions knows which service will be responsible for finally executing the action desired by the user.

modal-actionsthen passes thedata object to this final service so that it has access to all the information it needs (user id, product id, names, whatever was included in the object). In this demo, either the mock-serv-1 or mock-serv-2 will be the responsible service for fulfilling the action confirmed by the user.

If it still seems confusing, try thinking of the modal-actions service as a front that hides everything beyond the modal component to execute the action desired by the user (yes, I am making an allusion to the façade design pattern). Instead of the modal component communicating directly with multiple services when only one of them is needed per use of the modal, we can let modal-actions take care of everything. In other words, the modal component only needs to communicate with this one service.

On one hand you need to be passing the data object multiple times. But, on the other hand, if more use cases arise for this modal dialog, you only need to create a new service for that new action and then it to the façade, the modal-actions service. The code in the modal component is independent of these changes to the services. And, regarding the data object, to the component it only matters that the object contains the modal’s name, a title, description, and text for the buttons. The other attributes are worries for the services.

Now that we’ve talked about the theory, let’s start building the application!

Step 0: Project setup

This part is where we’ll create the application and install Angular Material.

Let’s start with the obvious, creating a new Angular application:

ng new reusable-modal
cd reusable-modal
code .

The first command creates a new application called reusable-modal and the second moves us inside its directory. When prompted for routing, choose whichever you prefer and for stylesheet format, we’ll use CSS.

The third command is just a shortcut to open the current working directory in Visual Studio Code. Pretty handy if you’re using it as your code editor.

To install Angular Material, return to the terminal and enter:

ng add @angular/material

This will then prompt you for three questions: theme, set up of HammerJS and set up of Material animations. You can choose whichever theme you like as it doesn’t affect the functionality of this demo, but we don’t need HammerJS. On the other hand, choose to set up Material animations or it will break the application.

We are just missing one thing for the setup, which is to import the Angular Material MatButtonModule and MatDialogModule modules. For this, it will be convenient if we already have our modal component created, so return to the terminal and enter:

ng generate component components\modal

This creates our modal dialog component, modal, inside a components folder. Since the folder doesn’t exist, this command creates the folder too. And please don’t close your terminal as we’ll keep coming back to it later.

To finish the setup, we’ll add some code to the app.module.ts file:

app.module.ts

We’ve imported the already mentioned Angular Material modules (lines 9 and 10) and added them to the imports of the @NgModule. Also, note that if you didn’t set up routing, then you need to delete lines 4 and 18. The last change was made on line 26, entryComponents: [ModalComponent].

As it is described in the documentation, entryComponents is “The set of components to compile when this NgModule is defined, so that they can be dynamically loaded into the view”. In practice, without declaring the entryComponents, the dialog wouldn’t work.

And with this, we have successfully finished setting up our project!

Step 1: Creating a one-use modal

Now we’ll focus on the app-root component and creating a one-use version of the modal dialog component.

By the end of this step we’ll be ready to make the component reusable, that is, the objective of this article. Thus, I won’t go into as much detail as before in this part because it was already explained in my previous article. If you have any doubts about creating the modal dialog component that are not clarified here, please refer to my previous article.

First, we’ll write the global styles, styles.css, as it involves the application-wide styles and styles specific to the Angular Material Dialog.

styles.css

While our modal dialog uses the modal component we have created, this component is opened in the context of an Angular Material Dialog overlay, mat-dialog-container. This means that, to style the apperance of the dialog itself, we need to style mat-dialog-container.

But, we need to be careful with these modifications because we are dealing with an application-wide CSS file, we need to make our selector as specific as possible. Thus, we use the name of the element we are targeting, but specify that we only want to modify those that have an id of modal-component, which corresponds to the modal component we are building (we’ll get back to this id soon). This way, no matter how many mat-dialog-containers the application has, only those that have the modal-component id will be affected: mat-dialog-container#modal-component.

Moving on to the app-root, we need to change its HTML, CSS and TypeScript files.

app.component.html (first version)

The page has two buttons, one to logout and another to delete a product. For now, the same function will be called when either button is clicked. In other words, both buttons will open the same modal dialog.

app.component.css

For the CSS, we only need color changes for the buttons.

app.component.ts (first version)

As you can see, the openModal() method opens the dialog using the modal component we have created. We also make use of MatDialogConfig to configure the dialog. Notice dialogConfig.id = “modal-component", as that ‘s where we get the id to use in the global styles.css file.

For now, all the information will be hard-coded in the modal’s HTML, hence why we are not including the data object in the configurations. Though, if we were passing it any user ids, product ids or whatever was needed to actually execute the logout or the product deletion operations, we would need data. We’ll get to it when we make the component reusable.

app-root is finished for this first phase, let’s move on to the modal component.

modal.component.html (first version)

As you can see, for now the modal will have hard-coded text. When it receives information from data, we’ll use that information instead.

modal.component.css

The CSS is pretty simple as well, we turn the modal’s content into a grid, which automatically sets three rows, one for the title, one for the description and another for the buttons.

modal.component.ts (first version)

Lastly, we have the TypeScript. MatDialogRef is injected in the component through the constructor so that the modal has access to the methods of the Angular Material Dialog in which the component is opened. Remember, the dialog itself is a component of Angular Material, modal is the component we’ve created to be used in the dialog, hence why it seems like our component is the modal dialog all by itself.

Then, we create two methods to handle the click events from the modal buttons: one that executes the confirmed operation and one that simply returns the user to the normal flow of the application. For the sake of this demo, the confirmations will result in a humble alert dialog to confirm the action was executed.

And that’s it. By now we have a working modal dialog and we can move on to making it reusable. Before that though, let’s review what we have so far:

app-root component with the new buttons
app-root component

The finalized app-root layout, with a work-in-progress TypeScript file.

modal dialog (first version)

And the current state of our modal. For now, this is the modal that opens when you click either of the buttons in the main page.

If you want to run the demo yourself locally, return to the terminal and enter

ng serve --open

to automatically compile and open the application in your browser.

Next we’ll be editing app-root and modal to include the data object in the dialog configurations. This will allow us to make the modal display different information depending on which button opened the modal.

Step 2: Create the MatDialogConfig.data object

In this step we’ll create the MatDialogConfig.data object and modify the modal component so it is able to receive the data and pass it to the façade service. By the end of this step the app-root will be finished and the modal will be missing only the call to the façade service, modal-actions. But first, let’s implement that data object.

First off, let’s edit the openModal() method of app-root. It will include the data object in the dialog configurations which holds the name/type of the modal and the text to be displayed (title, description and the action button’s text).

app.component.ts (second version)

As you can see, starting on line 20, now the dialog configuration includes the information we have just discussed. Also, note that we still haven’t created a new method, both buttons pass the exact same information to the modal dialog at the moment. Let’s create a new function that is pretty much a duplicate of the one we have, so that we have one function for each button, with all the information needed for each operation, including user id and product id, respectively.

app.component.html (final version)

Now each button calls a different function when clicked.

app.component.ts (final version)

And each function has its distinct information stored in the data object. We are including an userId and a productId so that the services at the end of the process which log out the user or delete a product, respectively, have the necessary information. And do keep in mind that you can include has many attributes as you need, these were the ones needed for this demo.

However, the modal component currently does not receive any information. Yes, the data object is included in the dialog configurations, but the modal component can’t read it. We need to inject MAT_DIALOG_DATA in the component’s constructor to be able to access the data passed to the component like any other variable inside the class.

modal.component.ts (second version)

We add the import of MAT_DIALOG_DATA on line 2 and then add the new dependency injection on line 13. To be more specific, we inject the private property modalData which in turn has the MAT_DIALOG_DATA injected into it, that is, whatever data was passed to the dialog when it was opened in the parent component (app-root) is then injected into the modalData property of the modal.

To prove that the modal component now has access to the complete data object, we include a console.log() in the contructor body. If you want to try it out yourself, open the console in the DevTools of your browser and then click one of the buttons of the app-root. You should see the complete data object logged in the console. Nice!

To finish this step of the article, we just need to edit the modal‘s HTML so the text it displays is what comes from data. Since we can now treat data as another variable of the component, known under the modalData name, the HTML changes are trivial.

modal.component.html (final version)

We swap the hard-coded text for the attributes saved in modalData and voilà, the same modal dialog component now displays different information depending which button called it in the app-root.

modal component for the logout confirmation
modal component for the logout confirmation
modal component for the product deletion confirmation
modal component for the product deletion confirmation

Visually, the modal is now reusable. Technically, it isn’t. We still need to make the action button in the modal work. For that, we need to write the services.

Let’s move on to the service that will connect the modal component with the services that realize the operations, modal-actions.

Step 3: Write the façade service (the modal-actions service)

Now, we’ll finally finish the modal component and start writing the modal-actions service. It won’t be functional in this step as we need the other services to do that. Instead, by the end of this step the service will use console.log() to log the name of the modal to the console.

First off, if we want to write modal-actions we have to create it. Return to the terminal and enter:

ng generate service services\modal-actions

The modal-actions service now exists inside the services folder (it was created along with the service).

modal-actions.service.ts (first version)

What you see is very close to the final version of modal-actions, it is missing only the calls to the other services.

And so, it has three methods: modalAction(), logout(), and deleteProduct(). The first method is the only one available to other classes in the application (no access modifier defaults to public access, i.e., any class can access it). logout() and deleteProduct() are meant to be used only by the class they are defined in to help send data from modal-actions to the services that fulfill the operation desired by the user. As the names suggest, the former is used for the user logout and the latter for the product deletion. Setting their access modifier to private makes the code cleaner and ensures the service exposes only what needs to be exposed, nothing more.

modalAction() is the crux of redirecting the information sent by the modal to the correct service that executes the user operation. This is done using a switch statement which chooses the service to call based on the name of the modal (the name attribute passed inside of data).

While we could call the services directly in the switch, the private methods (logout() and deleteProduct()) could be helpful if there is a need to modify what is passed to the services. For example, instead of sending the complete data object, you could create a new object in these methods with just the information needed and send that object to the services instead.

Before moving on to the next step, we need to inject the modal-actions service in the modal component and call it.

modal.component.ts (final version)

And with this we wrap up the modal component. When the action button is clicked inside the modal, that is, the user confirms their intention, the component will call the façade service, modal-actions, which dispatchs the operation to the respective service. Right now, when that button is clicked, it only logs a message to the console.

We will be looking at those services next to finalize the demo!

Step 4: Write the mock services (mock-serv-1 and mock-serv-2)

For the finishing touches, we need to create those mock services that represent services responsible for executing the user operations and then call them in the façade service. In our case, these mock services represent an authentication service to log out the user and another to communicate with the back end to delete a product in the database, respectively.

Return to the terminal one more time and enter:

ng generate service services\mock-serv-1
ng generate service services\mock-serv-2

For the sake of this demo, each service will have a single method: it receives the data object as a parameter and creates an alert dialog with the id of the logged out user/deleted product.

mock-serv-1.service.ts
mock-serv-2.service.ts

One note about choosing any as the type of the parameter passed to the methods. In a real-world application, because the data object can have different attributes, you will probably end up creating an interface for each use case of the object, so that you can ensure the logout modal doesn’t receive less information than it needs or that the product deletion data object won’t have its attributes mistyped. For more on TypeScript interfaces, please refer to the official documentation on the matter.

Ok, one more change to the code and we can call it a day: add the calls to the mock services in modal-actions.

modal-actions.service.ts (final version)

Now, if you’ve been writing your code locally, open the terminal, type

ng serve --open

and the application will compile and open in your browser.

When the logout or the product deletion operations are confirmed in the modal dialog, this is the result for each of them:

logout modal confirmation
logout modal confirmation
product deletion modal confirmation
product deletion modal confirmation

Conclusion

And that’s it for this article on how to create a reusable modal dialog component in Angular. We started by creating a single-use version of the component and then built upon it to allow its reuse in different scenarios.

Thank you for the read! I tried to follow best practices and leave some tips on how to implement this component in a real-world application. Please let me know your thoughts, suggestions, critiques or whatever you want to say about this article.

The complete code is available on GitHub here and you can play around with a live version of the code on Stackblitz here.

Hope this code helps in your projects :)

--

--