At COYO we are using Angular Material as a technical basis for our UI. The library provides a variety of different directives and components to help you implement a solid frontend based on the Material Design guidelines. One of the library’s core components are native
<a> elements enhanced with Material Design styling and ink ripples. Unfortunately these buttons are missing a loading state to indicate ongoing asynchronous actions. So let’s build our very own loading directive to add this functionality to Angular Material buttons.
Our goal is to add a loading attribute to the
<button> element so that we can easily switch on/off the loading state. While loading, the button should display a spinner and not accept any user interaction. The dimensions of the button should not change when the loading state is toggled.
Step 1: Starting with the groundwork
Let’s start by creating a new directive called
MatButtonLoadingDirective. As we want to extend Angular Material’s existing button component, we use the same selector as the component. You can find the selector in the source code at GitHub. We simply copy the selector and append a loading attribute to each one. This ensures that our directive works with all kinds of Material buttons and that we can simply set a
loading attribute on each of them.
The directive itself needs to implement Angular’s
OnChanges lifecycle hook to perform actions when the loading attribute changes. When
loading is active, we create a new spinner within the button. It is removed again when the
loading state changes back to
false. To apply custom styling we are also adding a
.mat-loading class to the button.
Step 2: Implementing the spinner
This is the tricky part. We don’t want to build our own spinner into the existing button. Much more we want to use the already existing
MatProgressSpinner component from the Angular Library. We will use a
ComponentFactory to dynamically create a new instance of this component and append it to the existing button. If you want to read more about dynamic components you will find a guide in the Angular documentation.
First of all we change the constructor of our directive and inject all the stuff that we need. We also create a new instance of a component factory for the
MatProgressSpinner so that we can easily create new instances in the
Now we can use the factory to create the spinner and also destroy the spinner when it is not needed anymore.
Finally, we will hide the button content whenever the button is in a loading state. We will do this via
visibility: hidden in order to not change the dimensions of the button. The spinner itself will be placed in the center of the button.
Step 3: Disabling the button
This is already a very cool result. We can now toggle the button’s loading state via the corresponding attribute. It works for every button that Angular Material provides. However, the button can still be clicked if it is in the loading state. We could just set the disabled state directly in our lifecycle hook but this would result in some faulty behavior. We would override the disabled state that can be set on the button itself. Thus, we use another
disabled . This will be the same input that the button component itself is using to determine its disabled state. If the button is loading, we override the disabled state and set it to
true . Otherwise we will fall back to the disabled state on the button itself.
Step 4: Choosing the correct theme palette
One minor detail that is still missing is the correct color of the spinner. Both components — button and spinner — support the use of a color palette. So we can easily make the color of the spinner align with the color of the button. To do this, we add another
@Input to the directive and call it
color. Again, this is the same input that the button will use to determine its color. We can then simply set the color on the spinner component that we are creating in out
That’s it. We are done! We have created an easy-to-use loading state for Angular Material buttons. There are a couple of minor tweaks that one could add to the directive (e.g. a progress value) but this is out of the scope of this article. You can find the final result in the StackBlitz below. Have fun!