Tutorial

Understanding Angular 1.5 Lifecycle Hooks

Draft updated on Invalid Date
Default avatar

By Nyambati Thomas

Understanding Angular 1.5 Lifecycle Hooks

This tutorial is out of date and no longer maintained.

Introduction

The release of Angular 1.5 has introduced some powerful features that have made Angular 1.x fun and easy to use. One of this features is the lifecycle hooks.

Lifecycle Hooks

Lifecycle hooks were introduced to Angular in Angular 2 alpha. They were more or less inspired by the custom elements lifecycle callbacks.

Angular 2 Lifecycle Hooks

This means that they are not actually the same so to speak. Angular 2 components come with lifecycle hooks like ngOnInit(), ngOnDestroy(), ngOnChanges(), and many more. In a nutshell, these lifecycle hooks do the following:

  • ngOnInit(): Initialize the component and the grab the necessary objects/variables we might need
  • ngOnDestroy(): Cleanup the component just before Angular destroys it. Useful for unsubscribing from observables to avoid memory leaks
  • ngOnChanges(): Do something when Angular sets a data-bound property

For detailed information visit the official docs.

Angular 1 Lifecycle Hooks

Angular 1.x is evolving in a way to keep the gap and migration to Angular 2 as small as possible. With the introduction of .component() method, some lifecycle callbacks have been backported to Angular 1.

Some of these hooks Include:

  • $onInit()
  • $onChanges()
  • $onDestroy()
  • $postLink()

Let’s look at them one by one. Please note that Angular lifecycle-hooks were introduced in version 1.5.3.

Note: module as used in this tutorial refers to the angular module as shown below.

var module = angular.module('lifeCycleModule', []);

$onInit()

This lifecycle is executed when all controllers on the element have been constructed and after their bindings are initialized.

This hook is meant to be used for any kind of initialization work for the controller. Imagine you have a view which when visited should be populated with a list of users on load.

Normally you would write a function that would be called when the controller constructed, for example.

...
function MyController() {
  /**
   * Initializing using custom method init
   */

  this.init = function () {
    this.users = ['user1', 'user2', 'user3'];
  };

  /**
   * When the controller is constructed, this method will be called
   */

  this.init();
}
...

Another approach is to directly initialize the list of users to a variable:

...
/**
 * Initialize users directly in the controller
 */
function MyController() {
  this.users = ['user1', 'user2', 'user3'];
}
...

All the above examples work, but now imagine if you have to make HTTP request to the server to get our users, this means we’d have to mock this requests every time we construct this component or controller.

Wouldn’t it be nice if we had some hook that would handle this for us? Now we have the $onInit lifecycle hook. Thanks to $onInit, we can now write the above code as below.

...
function MyController() {

  /**
   * [$onInit: Initializes users with an array of objects]
   *
   * We can also make http request in this hook if needed
   * especially when you are getting data from a server
   */
  this.$onInit = function () {
    this.users = [{
        "id": 1,
        "name": "Sammy Shark",
        "username": "sammy",
        "email": "example@example.com",
        "phone": "1-555-555-5555",
        "website": "example.com",
        "company": {
          "name": "Example Company",
          "catchPhrase": "Multi-layered client-server neural-net"
        }
      },
      ...

    ];
  };

}
....

we can access the users in our template as follows:

....
 <div class="container">
  <table class="bordered striped highlight">
    <thead>
      <tr>
        <th data-field="name">Name</th>
        <th data-field="username">username</th>
        <th data-field="email">E-mail</th>
        <th data-field="city">city</th>
        <th data-field="Phone">Phone Number</th>
        <th data-field="website">website</th>
        <th data-field="company">company</th>
      </tr>
    </thead>
    <tbody>
      <tr ng-repeat="user in users">
        <td>{{ user.name }}</td>
        <td>{{ user.username }}</td>
        <td>{{ user.email }}</td>
        <td>{{ user.address.city }}</td>
        <td>{{ user.phone }}</td>
        <td>{{ user.website }}</td>
        <td>{{ user.company.name }}</td>
      </tr>
    </tbody>
  </table>
</div>
....

Any data that we need before we begin processing any logic should be handled in this hook. This gives us better control of our component data flow.

In the above example, when the component is constructed, the $onInit lifecycle hook will run the function attached to it and populate the users variable with an array of users. Here is a codepen for this example.

http://codepen.io/thomasnyambati/pen/NrRBjm/

Inter-component communication

Now imagine you need to access the parent component controllers, how would you go about this. With $onInit we can access controllers of the parent components through the require property.

This means we don’t need link() functions to access other directives controllers - If you are using .directive() method helper to build components.

Let us use a scenario where we are building a tabs and tab component. The tab component being the child component needs to access the TabsController to register itself, we can simply use the require property and call it directly via the component instance. Let’s see an example.

...
/**
 * [TabController]
 */
function TabController() {
  /**
   * [addTab description]
   * @param {component} tab [The component to be registered]
   */
  this.addTab = function (tab) {
    // some code to register component
  };
}

/**
 * [template: Tab component template]
 * @type {String}
 * [controller: Component controller]
 * @type { function}
 */
module.component('myTabs', {
  template: '<div><!-- some code for tabs --></div> ',
  controller: TabController
});
...

In the code snippet above we have built a Tab component, but a tab without panel is not what we want. We need to add a TabPanel component which will be a child of myTab component.

...
/**
 * TabPanelController
 */
function TabPanelController() {
  /**
   * [$onInit registers panel to parent component]
   */
  this.$onInit = function () {
    this.parent.addTab(this);
  };

}

/**
 * [template panel template]
 * @type {String}
 *
 * [require gives access to the parent tab component]
 * @type {Object}
 */
module.component('TabPanel', {
  require: {
    'parent': '^myTabs'
  },
  template: '<div><!-- template for tab --></div>',
  controller: TabPanelController
});
...

Simple and clean.

$onChanges()

When building components at one point you will have data coming into your component from an external source e.g parent component. With ``$onChanges` we can react to this changes and update the child component data effectively.

Before we go ahead into examples it is important for us to understand, how, why and when this hook is called. $onChanges hook is called in two scenarios, one being during component initialization, it passes down the initial changes that can be used right away through isFirstChange method.

function MyController() {
  /**
   * Get the initial changes when the component is initialized
   * Assuming we passed name from the parent
   * <our-component name="ValueOfName"></our-component>
   */

  this.$onChanges = function(changes) {

    if(changes.user.isFirstChange()) {

      this.name = changes.user.currentValue;
    }

  };
}

The other scenario is when changes occur in one way bindings < and evaluated DOM attributes @. It propagates this changes down to the child component which we would re-assign to our local component.

Let’s look at an example. We have this two dudes who like to be greeted differently, Thomas likes Howdy and John the old Hello.

...
/**
 * [MyController: Greets user with an name]
 */
function MyController() {
  this.greeting = 'Hello ' + this.name;
}

module.component('onChanges', {
  template: '<h1>{{$ctrl.greeting}}</h1>',
  controller: MyController
});
...

If we run this component with <on-changes name="'John'"> we will get Hello John output. What do you this will happen if we change the name attribute value to Thomas?

    <on-changes name="'Thomas'"></on-changes>

We still get Hello Thomas as output. To remedy this situation, we need to detect changes on the name attribute and respond with the appropriate greeting. Let’s do that.

...
function MyController() {
  this.$onChanges = function (obj) {
    var prefix;
    /**
     * [if: checks when the name has changed and greet the persons appropriately]
     */

    if (obj.name && this.name) {
      (obj.name.currentValue.toLowerCase() === 'thomas') ? prefix = 'Howdy ': prefix = 'Hello ';
      this.greeting = prefix + this.name;
    }

    /**
     * If: checks if the name is not defined set greeting to empty string
     */

    if (!this.name) {
      this.greeting = '';
    }
  };
}

module.component('onChanges', {
  template: '<h1>{{$ctrl.greeting}}</h1>',
  controller: MyController
});
...

The $onChanges() passes down a hash with property of the bound attribute in this case name and a function isFirstChange(). The property contains two other properties currentValue which has the current value of the bound attribute and previousValue the value we have changed from. The function isFirstChange can be used to get the initial value when the component is initialized. Using these properties we can track the changes and reflect them on the view.

In the example above we have used obj.name.currentValue to access the current name from the attribute. We then get the current name from the attribute, check if its Thomas and if it is then we use Howdy to greet him. Otherwise, we will use Hello.

Here is the CodePen:

http://codepen.io/thomasnyambati/pen/JKRZbP

The big picture of one-way data flow is to replace default two-way data-binding syntax =. Ideally, we can delegate functions from the parent component through & bindings, to which the child component will use $event to pass data back to the parent component. The combination of $onChanges + < + & brings to live the concept of uni-directional data flow.

Let’s look at an example of how we can achieve this.


module.component('parentComponent', {
  controller: function () {
    /**
     * Initial name
     */

    this.name = 'Initial Name';

    /**
     * This method will be called when changes happen in the child component.
     */

    this.updateName = function (event) {
      this.name = event.name;
    };
  },

  template: `

    <p>The parent object: {{ $ctrl.name }}</p>
    <input type="text" ng-model="$ctrl.name">
    <child-component name="$ctrl.name" on-update="$ctrl.updateName($event)"><child-component>`
});


module.component('childComponent', {
  bindings: {
    name: '<',
    onUpdate: '&'
  },
  controller: function () {

    var self = this;

    self.$onChanges = function (changes) {
      self.name = angular.copy(changes.name.currentValue);
    };

    /**
     * When the button is clicked we can update the parent component
     */

    self.updateData = function () {
      self.onUpdate({
        $event: {
          name: self.name
        }
      });
    };
  },
  template: `
  <p> child component: {{ $ctrl.name }}</p>
  <input type="text" ng-model="$ctrl.name">
  <button class="btn" ng-click="$ctrl.updateData()"> Update </button>`
});

In the above example we have two components the parent and child component. Through one way data binding the parent component passed down the value of name to the child component.

The changes from the parent component will trigger the $onChanges hook wich will clone the value of the name that we get from the changes and re-assign it to self.name in our child component.

The reason why we are cloning the value is to treat the value from the parent component as immutable to avoid any nasty surprises. This enables us to manipulate the data passed down to us without affecting the parent.

So what happens when data changes in our child components?

In our bindings we specified one-way data binding meaning we only get data coming in from the parent but nothing is getting out.

To notify the parent component of changes from the child component, we have delegated a function onUpdate. This function enables us to access this.updateName function in the parent component. When the update button is clicked, self.updateData will in turn call the parent this.updateName with the new value of name. This is achieved through the delegated function self.onUpdate.

Here the CodePen.

http://codepen.io/thomasnyambati/pen/NABarq

$onDestroy()

This hook is called when its containing scope is destroyed. We can use this hook to release external resources, watches and event handlers. This is basically the same as $scope.on($destroy, fn) when used in controllers.

...
  $scope.on('$destroy', function() {
    // do some cleaning in here.
  });
...

In a scenario where you have attached non-native angular event listeners or logic, we can use this hook to clean it up when the component is destroyed.

...
function MyController($element) {
  /**
   * eventHandler: custom event handler to be attached to our element]
   */
  var eventHandler = function () {
    /**
     * Do something cool in here
     */
  };

  /**
   * [$onInit: Attach our eventHandler when the element is clicked]
   */

  this.$onPostLink = function () {

    // When the component DOM has been compiled attach you eventHandler.

  };

  /**
   * [$onDestroy: Destroy the eventHandler once the component is destroyed]
   */
  this.$onDestroy = function () {

    // Destroy all custom events or bindings when the component scope is destroyed.

  };

}
...

When our component is constructed, we attach a click event to the component element that does something to it when clicked, it can be wherever we want let’s say show alert box saying I am clicked.

In the duration when this component is active we would be happy to have this event, but when this component is inactive (meaning we have destroyed it), we don’ t need this event anymore it has to go.

Since this our custom event and it’s not native to Angular, it will not be detached from our app along with the component, but it will be left snooping around for an element which does not exist.

We need to tell Angular through the $onDestroy hook when you destroy this component detach this event too.

This hook is called after the controller’s element and its children have been linked. When the component elements have been compiled and ready to go, this hook will be fired.

...
function MyController($element) {
  /**
   * When the element and its child nodes have been compiled
   */

  this.$postLink = function () {
    /**
     * Do something awesome in here
     */

  };

}
...

This hook can help us to implement some functionalities that depend on the component elements to be fully compiled.

It is important to note that this is not a complete replacement for DOM manipulation, this functionality should be handled by decorator directives.

Conclusion

Lifecycle hooks provide us with an easy way of invoking operation based on the lifecycle of our components. Using this hooks lets us provide our users with relevant information or action at the appropriate time.

For more details refer to understanding components docs. It’s important that you practice what we have discussed above in order to have a deeper understanding of how and where to use these hooks.

Go out there and be creative.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about us


About the authors
Default avatar
Nyambati Thomas

author

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more
DigitalOcean Cloud Control Panel