If you have ever used Angular for more than a few minutes, you have definitely used the *ngIf
directive. It allows you to display, or not, a block of code in a view, based on whether the expression that you pass evaluates to false or not.
The main use I have for the directive is to hide some parts of the template until the data is loaded from the webservice. This works great, but one issue is that an empty page isn’t very attractive, and it is always better to display a loading spinner while waiting for the data to come back. This is why I made this very simple *ngLoaded
directive, which has the same exact functionality as *ngIf
, but displays a spinner when needed.
Note that the code has been tested on Angular versions 6 to 15 included and works properly. It will probably keep working in new versions to come too.
A reminder about false values
As a reminder, all the following values will evaluate to false, but you can also do a more complex expression, like calling a function:
false
"" // an empty string
0 // a number equal to 0
null
undefinied // the value of a variable before you assigned anything to it
NaN // not a number
This means that whenever the expression that you put in the *ngIf
is evaluated to one of those values, the spinner will be displayed.
The *ngLoaded directive
It will be a bit special, because unlike a lot of directives, this one also requires a component, in order to display the spinner itself. For that reason, I encourage you to have a dedicated module for the directive, to separate it from the rest of your application and make it easily reusable in another project.
The directive itself
The type of directive that we will be creating is a structural directive, because it changes the layout of the page. We need to inject 3 services here:
- The
TemplateRef
represents the element that has the directive on it, so your<div>
or<ng-container>
for example, - The
ViewContainerRef
is used to manipulate the view part of the element, to manipulate the DOM. - The
ComponentFactoryResolver
is used when you want to instantiate new components on the fly, directly using code, rather than letting the framework handle that part.
@Directive({
selector: '[ngLoaded]'
})
export class LoadedDirective {
@Input()
public set ngLoaded(data) {
if (data) {
this.viewContainer.clear();
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
this.viewContainer.createComponent(this.componentFactoryResolver.resolveComponentFactory(LoadedComponent));
}
}
constructor(private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
private componentFactoryResolver: ComponentFactoryResolver) {
}
}
It might look strange, but the ngLoaded
setter is marked as @Input
, because it will receive the result of the expression that determines whether or not the data is loaded.
What it does inside, is that if the expression evaluates to true
(which means the webservice returned the data already), it will clear what is currently displayed, and then append the real content (represented by this.templateRef
). However, when the expression evaluates to false
, and the data isn’t ready, it will also clear the current display, but this time it will create a new instance of LoadedComponent
, which we will create to display the spinner!
The component
The component is extremely simple, the only thing it does is display a spinner. In this example I will only use a GIF image and embed it, but you can choose to use any spinner that you like, for example some cool CSS spinners.
@Component({
selector: 'app-loaded',
template: '<img src="/images/spinner.gif" alt="The data is loading" />'
})
export class LoadedComponent {
constructor() {
}
}
The component literally just displays the spinner, but you can add a <div>
for example to display it as a block. The last step remaining before it can be used it to register those two elements in a module.
The module
For this last part you have the choice, either you can use the following imports and declarations in your AppModule
, or you can create a new LoadedModule
, as followed, which will be better if you want to be able to copy and paste your directive in a new project without any hassle.
@NgModule({
imports: [
CommonModule
],
declarations: [
LoadedComponent,
LoadedDirective
],
entryComponents: [
LoadedComponent
],
exports: [
LoadedDirective
]
})
export class LoadedModule {
}
Note that if you use versions 9 and above of the Angular framework, you will not need to register the component in the entryComponents
array, which has been deprecated.
Using our new loading spinner directive
Now that we have our new very practical directive, it’s time to put it to work! It is very simple, just make sure that the module is imported in your AppModule
, and you can then use it in any component’s view:
<div *ngLoaded="myDataArray != null">
<div *ngFor="let d of myDataArray">
...
</div>
</div>
I use this directive all the time in all of my Angular projects. Insist on the Highest Standard is one of the 14 Amazon leadership principles, and not displaying an empty page while your content is loading is definitely a better customer experience.
I hope this article has provided you with a useful directive, and maybe even gave you ideas on how to leverage this system for new features!