Wednesday, February 24, 2016
If you have a large Angular 1.x application or you are starting a new Angular application but you are not quite ready to use Angular 2.0, there are things you can do that will make your current application easier to upgrade should you want to do that in the future.
One of those things you can do to ease your future upgrade is to start using the Angular "component" function helper which is new in version 1.5 instead of using "directives". An angular component is basically a directive that is simpler to write and corresponds better to many of the concepts that are being used today in Angular 2.0.
Let's look at an example of how I can refactor an Angular directive into a component. I have a directive that shows an image, and the user can rotate the image forwards or backwards based on the button they click.
This is really basic but here is what it looks like:
To use this directive I place the element on the HTML page.
Here is the complete code for the directive:
In this particular example, I am using the controllerAs setting to alias a controller to the directive and the "bindToController" setting to bind the scope of the directive to the controller. Doing this creates a "vm" binding on the template.
In the link function, I am handling all of the event listener logic for the previous and next button.
Convert to Component:
Okay so let's take the above code and convert it into a component and discuss some of the differences.
First thing to notice at the bottom of the code sample, is that I am now using the "component()" function helper instead of the directive. The signature for this function requires an object instead of a function that returns an object, so right off the bat, this makes for much simpler code (especially if you are writing it in TypeScript).
Secondly, by default the component defaults the "restrict" setting to "E" (for element), the "controllerAs" setting is defaulted to "$ctrl", and the "bindToController" setting to true so you no longer need any of those settings making the code even more concise. In the template. since $ctrl is now the default template a binding I need to change "vm" to "$ctrl".
Also, where in directives you defined scopes, in components you define a bindings and by default the scope is isolated. So there no longer the ability to use inherited scopes which is a good thing. Notice the "<" indicator for the images binding? That specifies that images the property is a one way binding, so if the images values changed outside the component it will flow into the component; however, images values changed inside the component will not propagate back outside the component. The benefit to this approach is that it will make your application easier to debug as you don't have to worry about your component changing the outside model inadvertently. It may also be an improvement performance although I haven't seen any evidence on that.
Now the question is the component no longer has a link function, so how do I get access to the DOM? Depending on what you are doing, using components might be a deal breaker, such as listening to events on a DOM element, but if you are not doing that, you can inject the $element into the controller and change the DOM via the $element. In this case, I am only listing to the "ng-click" for the previous and next buttons, so I just need to add those callback functions in the controller and make the code necessary code changes to get that working. Let's look at those changes.
Refactoring the link code
First, I want to make sure my prevClicked and next nextClicked functions are integrated in the view, so I need to update my template, so I prefix those with the "$ctrl" object.
I then update my controller with the ng-click events:
I now need to decide what to do with the element object that used to be passed in through the link function, since I was using it to change the URL on the src attribute. Actually, it turns out in this case I don't need the element, since I can create a new property on the controller that has the current URL and use the new life-cycle $onInit() event to update the image.
The $onInit() life-cycle event
Angular components have a new life-cycle event called $onInit. This event is similar to the "componentDidMount" event in React applications in that it gets fired as soon as the component is mounted on to do DOM and it only gets fired once.
So in the above code I can create a new property called "currentImageUrl" and set the "ng-src" to that in the $onInit() event.
Now if I take my code for the ng-click handlers and place them in my controller, the only thing I have to do is update the currentImageUrl property instead of updating the DOM attribute.
Here is the code now for the component:
This is nice and the code is much cleaner than the earlier directive code, but there is still some more refactoring that could be done. For example, the prevClick and nextClick code are pretty similar. Perhaps, I can create a child component for the buttons and reuse it for the previous and next events. For that, we will need to look at the new "required" property for retrieving the values from the parent controller which will be covered in a future blog post.
As usual, you can see the code at Githhub.