How to create a reusable Modal Dialog component in Angular 8
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
.
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:
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-actions
then 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:
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.
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-container
s 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.
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.
For the CSS, we only need color changes for the buttons.
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.
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.
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.
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:
The finalized app-root
layout, with a work-in-progress TypeScript file.
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).
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.
Now each button calls a different function when clicked.
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.
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.
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
.
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).
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.
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.
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
.
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:
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 :)