So, you've read the first article (if you haven't, click here http://blog.avenuecode.com/angular-2-should-i-learn-it),and you’re now wondering what the next step is. You've created your own classes, fiddled with Angular components, and learned how to fetch data using services.
Now, you’re in need of something fancier to help you add a little more spice to the mixture. Well, you came to the right place. Modern Web Development is now focused on the creation of Components and employing them like reusable Lego blocks.
In order to easily create these blocks, we'll be using the well-known Angular-CLI which is new to the official Angular project (https://github.com/angular/angular-cli). This way, you'll be able to scaffold useful snippets of magical Angular code without having to deal with syntax specificities for the time being.
Let's start with the so called "blueprints" you can use with Angular-CLI.
Scaffold |
Usage |
ng g component my-new-component |
|
ng g directive my-new-directive |
|
ng g pipe my-new-pipe |
|
ng g service my-new-service |
|
ng g class my-new-class |
|
ng g guard my-new-guard |
|
ng g interface my-new-interface |
|
ng g enum my-new-enum |
|
ng g module my-module |
As you might remember from the previous article, you'll be using the Component, Service, Directive, and Pipe blueprints a lot.
You've learned how to use components to create interactions using the Input Output flow and Event Emitters, but in order to further understand what Components are truly made of, you need to learn about Directives as well.
Directives
Directives are multi-purpose classes that are meant to interact with the visual part of your application, namely what the user sees in front of him.
Back in Angular JS 1.x, we were taught that directives were meant to be the only way one should "Touch the DOM the Angular Way". That was because all versions of Angular have specific lifecycle hooks, and trying to mess with elements using plain javascript or JQuery alongside Angular used to result in unstable code.
In the latest version of Angular, things have gotten even more organized with regards to Directives.
There are three types of directives:
Source - https://angular.io/guide/attribute-directives.
This means your building blocks are actually @Component decorated directives with a template.
Now it's time to create your first directive using Angular-CLI:
Let's say you want to create a directive that changes the text of a given DOM element to a famous politician's quote.
ng g d Quote |
Note that directives have a 'selector' attribute. It’s the CSS selector for that directive, which is the way Angular locates all elements with that selector in a given template.
In the example above, Quote will be the Directive's name, which will lead to a similar CSS selector. You don’t need to translate the word 'Directive' to 'Quote', because the CLI will take care of that for you.
This will create the following snippet:
You won't need to use the snake-case selector name for Angular, and use of camelCase is actually encouraged. Let's start with Attribute Directives
If you write the selector name with brackets "[appQuote]", the directive should be used as an attribute, otherwise it will be treated as a new HTML element altogether, which will wrap the directive's template.
In this case, we want to use the existing HTML tag "<pre>" and then add the CSS selector appQuote from the directive as an attribute because we want to augment an existing element by adding extra content to it:
Now we also need to insert the directive Inputs into our recently created directive.
You can create them either by:
Don't forget to import the Input symbol as well, in order to use @Input.
Also, add them to the <pre> tag in the HTML file so we can input information into the recently created QuoteDirective.
If you surround the input variables with brackets (bind values instead of strings), you'll be able to input typescript expressions into the Directive as well.
And inside app-component.ts create the "getPoliticianName" method inside the ts class:
But neither approach will yield anything on the browser, so we need to actually manipulate the DOM using the element reference.
Back to quote-directive.ts, add the element reference to the class's constructor using Angular's ElementRef:
Now, in order for things to work, you need to:
The OnInit lifecycle hook needs to be implemented because it's executed right after the directive's data-bound properties, which have been checked, and thus, are available to the Directive.
Save it, serve it, and you should see something like this on your browser:
But now, you want to actually modify the DOM by adding/removing/manipulating elements and customizing them as data flows through your class, it's time to create your first:
Structural Directive
Start by creating it the same way you created the Quote directive:
ng g d Names |
Structural directives will impact their host element and descendants, so they should be preceded by the " * " (asterisk) symbol such as *ngFor and *ngIf, which are also Structural Directives.
The asterisk is necessary because it wraps the host element with an <ng-template> element. The expression passed to the asterisk preceded attribute will be bound to the outer <ng-template> element as a property binding.
Source - https://angular.io/guide/structural-directives
What that means is, whatever that template renders will be related to the variable passed to the structural directive, which might add /remove/manipulate the host element and its descendants.
In the *ngIf built-in structural directive, the expression passed to it will determine whether or not the element should be rendered on the DOM.
But we've just created our own Structural Directive which is going to behave similarly to the *ngIf directive. However, instead of inputting a boolean expression into the directive, we'll be inputting a very specific string.
First, we need to create an input field to bind different user provided values to our directive.
Don't forget to add the FormsModule to the App Module. Then, we need to create the secretSentence variable in our AppComponent.
Next, the Structural Directive must be applied to a given HTML host element. Don't forget the asterisk preceding the directive selector.
Now, two classes and an interface need to be imported and injected into our Directive in order to call it Structural:
Then, we need to add the appNames @Input
It's necessary to make it a setter by adding the 'set' keyword before the directive property name, since we'll be using the appNames property value to perform actions inside the structural directive's set method.
What this snippet now does is:
-> Check if secretSentence equates to 'show presidents please'
If so, and the template reference is also present, then the template should be created.
The viewContainer instance should then render both the host element and its descendants using the templateRef.
-> If the sentence provided is not 'show presidents please', then the viewContainer should be cleared, and no trace of that host element should be in the DOM.
Therefore, if the user types 'show presidents please' into that field, something like this should show up:
And the corresponding generated HTML should be something like this:
If the secretSentence was different from 'show presidents please', then only the commented section would be displayed and no DOM elements would be rendered.
And last, but not least. Components.
Now that you know about the two other types of directives, it's much easier to understand how components work, because Components are a subset of directives with their own set of methods.
They're also called Directives with a template, since you're going to build your own HTML element with its own template, functions, properties, and lifecycle.
As you've done in the previous article, create your component by using Angular CLI and type the following command:
ng g c States |
This will create the component and add its reference to the App Module.
The next part you need to learn about Directives and Components, is how the Lifecycle works.
The Angular team has created lifecycle hooks for different moments on a Directive's timeline.
You can check them all out in the official documentation right here:
https://angular.io/guide/lifecycle-hooks#component-lifecycle-hooks-overview
I'll be covering three of the most important Lifecycle Hooks.
After creating the States component, a stub for the OnInit interface will be created. This will hook us to the ngOnInit method.
That Lifecycle Hook is triggered right after Angular displays the Directive's data-bound properties and sets its input properties to the local Directive class properties.
This method is useful when passing @Input values to that Directive/Component, since we won't have those, once the class gets instantiated and the constructor is executed.
Those will only be available at the OnInit hook in the lifecycle timeline.
Which means if we try to console.log some @Input property inside the constructor function, the variable will be undefined.
Typescript code:
HTML template on App Component:
HTML template on States Component:
Console log:
states.component.ts:13 Variable is: undefined inside constructor states.component.ts:17 Variable value: This is the President's Speech inside ngOnInit |
The next lifecycle hook you should know about is OnChanges.
It gets triggered whenever data-bound input property changes.
Surprisingly enough, it gets triggered before the ngOnInit hook and provides a SimpleChanges object with both the current and previous value of that data-bound input property.
This is useful when a parent component sends different types of data that need to be parsed prior to being interpolated into the template.
Let's say we want to send some data to StatesComponent using @Input, but this information can either be sent as a string or as an object.
In this case the rollState function is going to be triggered by a button on the AppComponent template.
This function will return either 'West Virginia' (string) or {stateName: 'California'} (object)
Which means the selectedState property will be assigned with either of the above values and sent to StatesComponent as an @Input property.
Template: app.component.html
Template: states.component.html
TypeScript class: states.component.ts
In the ngOnChanges hook described in the snippet above, all changes to data-bound input properties will be verified and applied to the variable change: SimpleChanges.
Now whenever the rollState function gets executed by the parent component, StatesComponent will check its current value andverify whether the selectedState property is an object or a string.
If it is in fact an object and has keys, the variable will then be re-assigned in order to display the actual value of that stateName, otherwise, the interpolated value on the markup would be [object Object]
After clicking that button, you should have something like this:
If the information sent is an object
Or like this
If the information sent was a string, and only because we've explored the lifecycle hook before something like this
Could have happened.
Speaking of getting rid of the unnecessary, it's time to talk about the ngOnDestroy lifecycle hook.
Back in Angular 1.x, there was talk about memory leaks and performance issues, which have been due to the way the garbage collector in the browser worked.
Instead of purging a given resource after using it, developers usually relied on the garbage collector to get rid of unused references in the memory, but since it might not really work exactly how we expect it to work, and those references might still be in the memory, performance might drop.
That's why the ngOnDestroy lifecycle hook has been created. It's called whenever a directive, pipe, or service is destroyed. This way custom cleanups can be executed in order to properly remove any trace of unwanted data.
Assume we want to monitor changes on the selectedState and keep track of those states. We need to subscribe to those changes using rxJs Observables.
Here’s another button that controls if the StatesDirective should be created, using the built-in structural directive *ngIf:
And the app-states directive will then be rendered depending on the showStates property value:
Whenever that variable is set to false, the DOM element gets removed from the node tree and the ngOnDestroy lifecycle hook is called.
So in order to subscribe to the variable changes, we need to create a Subject and an Observable.
This subject will push (next function) data whenever data changes through the ngOnChanges hook:
And the stateObservable is subscribed to at the ngOnInit hook:
Now, if we remove the app-states component from the DOM using the *ngIf triggered by the button in the parent component, the ngOnDestroy hook will be triggered.
This is also the right time for us to unsubscribe from that property since the component will be hidden/removed/thrown away.
Unsubscribing from previously created Observables will allow the garbage collector work more easily by actively freeing up memory.
Conclusion
Creating Directives is the best way to customize your Angular application and deal with the DOM. If the JS libraries you've always used don't have an Angular port, it's time to create your own. Devising your own building blocks is the best way to understand Angular's lifecycle hook.
I hope this article helped you understand a little bit more about directives. But if you feel like you need more information, be sure to follow the links below and download the Github Repo with everything that has been covered in this article.
Github Repo:
https://github.com/mauriciotasca/angular-demo-part2
Useful Links:
The Power of Structural Directives in Angular
https://netbasal.com/the-power-of-structural-directives-in-angular-bfe4d8c44fb1
When to Unsubscribe in Angular
https://netbasal.com/when-to-unsubscribe-in-angular-d61c6b21bad3
Rangle.io Angular 2 Training
https://angular-2-training-book.rangle.io/handout/directives/