Angularjs Tutorial :  Building Quality  Angular 1.5 Components

Angularjs Tutorial : Building Quality Angular 1.5 Components

  • 2016-10-26
  • 1785

In Angular 1, components are the mechanism which allows you to create your own custom HTML elements. This has been possible with Angular directives in the past, but components build on the various improvements that have been made to Angular and enforce best practices in how they are built and designed.

In this article, we’re going to dig into the design of components and how to put them to use inside of your applications. If you haven’t already started to use components in Angular 1, you can read about their syntax and design in one of our recent tutorials. My goal is to outline some best practices that will improve the quality of your application.

It should also be noted that many of the best practices of Angular 2 are brought into Angular 1 through the new components API, allowing you to build applications that are more easily refactored later. Angular 2 has influenced the way that we think about and design Angular 1 components, but there are still a number of distinct differences. Angular 1 is still a very powerful tool for building applications, so I believe it is worthwhile to invest in improving your applications with components even if you aren’t planning or ready to migrate to Angular 2.

What Makes a Good Component?

Components should be designed with a number of key characteristics in mind to make them a powerful building block for your application. We’ll dig into each of these in more detail, but here are the primary concepts components should adhere to.

  • Isolated – The logic of the component should be encapsulated to remain internal and private. This helps create less coupling between components.
  • Focused – Components should act as a single unit for one primary task, which makes them easy to reason about and often more reusable.
  • One-Way Binding – When possible, components should leverage one-way binding to reduce the load on the digest cycle.
  • Use Lifecycle Events – The lifecycle of a component starts with instanciation and ends with removal from the page. It is to best to hook into these events to maintain the component over time.
  • Well Defined API – Components should accept configuration as attributes in a consistent manner, so it is easy to know how to use them.
  • Emit Events – In order to communicate with other components, they should emit events with appropriate names and data.

Now let’s start by looking at why and how components should be isolated and encapsulated from the rest of the application.

Components Should Be Isolated

The evolution of Angular 1 capabilities has been to enable isolated and encapsulated components, and for good reason. Some of the early applications were highly coupled with the use of $scope and nested controllers. Originally Angular didn’t provide a solution, but now it does.

Good components do not expose their internal logic. Thanks to the way they are designed, this is pretty easy to accomplish. However, resist any temptation to abuse components by using $scope unless absolutely necessary, such as emitting/broadcasting events.

Components Should Be Focused

Components should take on a single role. This is important for testability, reusability, and simplicity. It is better to make additional components rather than overload a single one. This doesn’t mean you won’t have larger or more complex components, it simply means each component should remain focused on its primary job.

I’ve classified components into four primary groups based on their role in the application to help you think about how you design your components. There is no different syntax to build these different types of components — it is just important to consider the specific role a component takes.

These types are based on my 5+ years of Angular experience. You may choose to organize slightly differently, but the underlying concept is to ensure your components have a clear role.

App Components

There can be only one app component that acts like the root of your application. You can think of it like having only one component in the body of your web application, and all other logic is loaded through it.

<body>
  <app></app>
</body>

This is recommended primarily for Angular 2 design parity, so it will be easier to migrate some day should you wish. It also helps with testing by moving all of the the root content of your application into a single component, instead of having some of it in the index.html file. The app component also gives you a place to do app instantiation so you don’t have to do it in the app run method, enhancing testability and decreasing reliance upon $rootScope.

This component should be as simple as possible. It probably will contain just a template and not contain any bindings or a controller if possible. It does not replace ng-app or the need to bootstrap your application, however.

Routing Components

In the past, we’ve linked controllers and templates in a ui-router state (or ngRoute route). Now it is possible to link a route directly to a component, so the component is still the place in which a controller and template are paired, but with the benefit of being also routable.

For example, with ui-router this is how we would link a template and controller.

$stateProvider.state('mystate', {
  url: '/',
  templateUrl: 'views/mystate.html',
  controller: MyStateController
});

Now, you can link a url directly to a component instead.

$stateProvider.state('mystate', {
  url: '/',
  component: 'mystate'
});

These components can bind data from the route params (such as an item id), and their role is to focus on setting up the route to load the other components needed. This seemingly minor change to defining routes is actually very important for Angular 2 migration capability, but also important in Angular 1.5 to better encapsulate a template and controller at the component level.

Angular 1 actually has two router modules, ngRoute and ngComponentRouter. Only ngComponentRouter supports components, but it is also deprecated. I think the best bet is to go with ui-router.

Stateful Components

Most of the unique components you’ll build for your application are stateful. This is where you’ll actually put your application business logic, make HTTP requests, handle forms, and other stateful tasks. These components are likely unique to your application, and they focus on maintaining data over visual presentation.

Imagine you have a controller that loads a user’s profile data to display, and has a corresponding template (not shown here) linked together in a directive. This snippet might be the most basic controller to accomplish the job.

.controller('ProfileCtrl', function ($scope, $http) {
  $http.get('/api/profile').then(function (data) {
    $scope.profile = data;
  });
})
.directive('profile', function() {
  return {
    templateUrl: 'views/profile.html',
    controller: 'ProfileCtrl'
  }
})

With components, you can design this better than before. Ideally, you would also use a service instead of $http directly in the controller.

.component('profile', {
  templateUrl: 'views/profile.html',
  controller: function($http) {
    var vm = this;
    // Called when component is ready, see below
    vm.$onInit = function() {
      $http.get('/api/profile').then(function (data) {
        vm.profile = data;
      });
    };
  }
})

Now you have a component that loads its own data, thus making it stateful. These types of components are similar to routing components, except they might be used without being linked to a single route.

Stateful components will use other (stateless) components to actually render out the UI. Also, you’ll still want to use services instead of putting data access logic directly in the controller.

Stateless Components

Stateless components are focused on rendering without managing business logic, and need not be unique to any particular application. For exampe most components that are used for UI elements (such as form controls, cards, etc) don’t also handle logic like loading data or saving a form. They are intended to be highly modular, reusable, and isolated.

A stateless component may not need a controller, if it just displays data or controls everything in the template. They will accept input from a stateful component. This example takes a value from the stateful component (the profile example above) and displays an avatar.

.component('avatar', {
  template: '<img ng-src="http://example.com/images/{{vm.username}}.png" />',
  bindings: {
    username: '>'
  },
  controllerAs: 'vm'
})

To use it, the stateful component would pass the username via the attribute like so <avatar username="vm.profile.username">.

Most libraries you use are a collection of stateless components (and perhaps services). They certainly can accept configuration to modify their behavior, but they are not meant to be in charge of logic outside of their own.

Components Should Use One-way Bindings

This is not a new feature with components, but it is often smart to leverage it with components. The intent of one-way bindings is to avoid loading more work into the digest cycle, which is a major factor in application performance. Data now flows into the component without having to look outside of it (which causes some of the coupling problems that exist today), and the component can simply render itself given that input. This design also lends itself to Angular 2, which helps with future migration.

In this example, the title property is only bound into the component once based on the initial value provided. If the title changes by some outside actor, it does not get reflected in the component. The syntax to denote a binding as one-way is to use the > symbol.

bindings: {
  title: '>'
}

We’ll cover how to still listen for changes to the title property in case you need to react to changes, but it is recommended to use one-way anytime you can.

Components Should Use Lifecycle Events

You probably noticed the $onInit function as a new capability. Components have a lifecycle with corresponding events that you should be using to help manage certain aspects of the component.

$onInit()

The first step in the component lifecycle is initialization. This event runs after the controller and bindings are initialized. You should almost always use this method to do component setup or initialization. It will ensure that all values are available to the component before running. If you were to access binding values in the controller directly there is no guarantee those values will be available to you.

controller: function() {
  var vm = this;
  console.log(vm.title); // May not yet be available!
  vm.$onInit = function() {
    console.log(vm.title); // Guaranteed to be available!
  }
}

$postLink()

The next step is linking any child elements from the template. When the component initializes, there is no guarantee it will have also rendered any children used inside of your template. This is important if you need to manipulate the DOM in any way. One important caveat is that templates that are loaded asynchronously might not have loaded by the time this event fires. You can always use a template caching solution to ensure that templates are always available.

controller: function() {
  var vm = this;
  vm.$postLink = function() {
    // Usually safe to do DOM manipulation
  }
}

$onChanges()

While the component is active, it may need to react to changes in input values. With the introduction of one-way data binding, component bindings no longer hook into and add load to the standard digest cycle. However, since you still need to listen for changes to data bindings, you can use the $onChanges event binding. Events are a lower cost way of triggering changes, so it is recommended to leverage it when possible. Sometimes, two way binding still makes sense, such as form inputs.

For this sample, imagine there is a product title and description provided to a component. You can detect changes as demonstrated below. You are able to look at the object passed to the function, which has an object mapped to the available bindings with both the current and previous values.

bindings: {
  title: '>',
  description: '>'
},
controller: function() {
  var vm = this;
  vm.$onChanges = function($event) {
    console.log($event.title.currentValue); // Get updated value
    console.log($event.title.previousValue); // Get previous value
  }
}

$onDestroy()

The final phase is the removal of the component from the page. This event runs right before the controller and its scope are destroyed. It is important to clean up anything that your component might have created or that holds memory, such as event listeners, watchers, or additional DOM elements.

controller: function() {
  var vm = this;
  vm.$onDestroy = function() {
    // Reset or remove any event listeners or watchers
  }
}

Components Should Have a Well Defined API

To configure and initialize a component with a set of data, a component should use bindings to accept these values. This is sometimes thought of as the component API, which is just a different way of describing the way a component accepts inputs.

The challenge here is to give bindings concise but clear names. Sometimes developers try to shorten names to be really succinct, but this is dangerous for the usage of the component. Imagine we have a component that accepts a stock symbol as input, which of these two are better?

bindings: {
  smb: '>',
  symbol: '>'
}

Hopefully you thought symbol was better. Sometimes developers also like to prefix components and bindings as a way to avoid name collisions. Prefixing the components is sensible, like md-toolbar is a Material toolbar, but prefixing all of the bindings gets verbose and should be avoided.

Components Should Emit Events

In order to communicate with other components, components should emit custom events. There are many examples of using a service and two-way data binding to sync data between components, but events are a better design choice. Events are far more efficient as a means to communicate with the page (and a foundational part of the JavaScript language and the way it works in Angular 2, which is not a coincidence).

Events in Angular can use either $emit (up the scope tree) or $broadcast (down the scope tree). Here is a quick example of events in action.

controller: function($scope, $rootScope) {
  var vm = this;
  vm.$onInit = function() {
    // Emits an event up to parents
    $scope.$emit('componentOnInit');
  };
  vm.$onDestroy = function() {
    // Emits an down child tree, from root
    $rootScope.$broadcast('componentOnDestroy');
  };
}

There are two primary situations where you will need to communicate between components: between components you know about, and components you don’t. To illustrate the difference, let’s imagine we have a set of components that help manage tabs on the page, and a toolbar that has a link to the corresponding help page.

<my-toolbar></my-toolbar>
<my-tabs>
  <my-tab title="Description"></my-tab>
  <my-tab title="Reviews"></my-tab>
  <my-tab title="Support"></my-tab>
</my-tabs>

In this situation, the my-tabs and my-tab components are likely aware of one another, because they work together to create a set of three different tabs. However, the my-toolbar component is outside of their awareness.

Whenever a different tab is selected (which would be an even on the my-tab component instance), the my-tabs component needs to be aware so it can adjust the display of the tabs to show that instance. The my-tab component can emit an event up to the parent my-tabs component. This type of communication is like an internal communication between two components that work together to make a single capability (a tabbed interface).

However, what if my-toolbar wants to know what tab is currently selected so it can change the help button based on what is visible? The my-tab event will never reach my-toolbar because it is not a parent. So another option is to use the $rootScope to emit the event down the entire component tree, which allows any component to listen and react. The potential downfall here is that your event now reaches every controller, and if another component uses the same event name you could trigger unintended effects.

Decide which of these approaches make sense for your use case, but anytime another component might need to know about an event you’ll likely want to use the second option to emit to the entire component tree.

Summary

Angular 1 applications can now be written with components, which changes the best practices and nature of how we write applications. This is for the better, but just simply using a component doesn’t necessarily make it better than what you had before. Here are the key things to keep in mind as you build your Angular 1 components.

  • Isolate your logic. Keep as much of the component logic internal and away from other aspects of the application to ensure consistency and quality.
  • Keep components simple and focused on a single role. They might be complex components, but the various tasks of a single component should be logically connected as a unit.
  • Use the lifecycle events. By hooking into the component lifecycle, you can ensure that data is ready at the right time and that you can clean up.
  • Use one-way bindings. When possible, one-way bindings are more efficient and promote good design. You can always use $onChanges lifecycle event to keep in sync.
  • Use events for communication. Components can communicate using custom events, which is in line with how Angular 2 functions and a better design.
  • Have a well defined API. Ensure that your components are clearly named and easy to understand.

Are you using components in your Angular 1.x applications? Or, are you going to wait until you make the jump to Angular 2 instead? I’d love to hear about your experiences in the comments below.

Suggest

AngularJS 2.0: What to Expect and Interesting?

Building a HTML5 Mobile App Using AngularJS

Comparing ReactJS vs AngularJS

How to Leverage MongoDB, MVC and AngularJS

Angularjs 2 with TypeScript Tutorial for Beginners