Angular 1.5 app as a tree of components [“NG 1.5 from the trenches” 2/7]

This post is the 2nd part of the “Angular 1.5 from the trenches” series, presenting the architecture of a component-oriented, “NG 2 ready” Angular 1.5 app we’re building.
The module.component() method, introduced in Angular 1.5, may seem only a cosmetic addition, but the approach it promotes (and which we utilise to the full extent) results in a very different flavour of architecture than most of the “classic” Angular tutorials describe, so I hope you’ll learn a thing or two from our experience.

Table of Contents:

A new way to write Angular 1 apps.

A seemingly cosmetic addition introduced in NG 1.5, the module.component() method, can have a big impact on your app. It enables you to architect your whole app as one big tree of components, and to completely get rid of controllers (remember the [in]famous Angular 2 “RIP” talk?). Although NG 1.5 doesn’t force it upon you like NG 2, it makes it possible if you want.

It was already possible to achieve a similar effect in NG 1.4, through the combination of directives, isolated scope, and controllerAs option. It was complicated, though, and caused a lot of friction, so people usually did it on a much smaller scale, only for a couple of selected components. NG 1.5 changes the game. The module.component() method makes this so easy, that it feels natural to make components even for the smallest, dumbest parts of the UI.

We went in this direction as far as possible with our app and are very fond of such component-oriented architecture. In this post, I’ll show how it looks like.

The anatomy of a component

The complete component definition consists of 3 parts: bindings, controller, and template. Such a component may look like the one below:

(all examples in this post are in ES6/ES2015 syntax – more about ES6 syntax in Angular 1.5 in one of the next posts in the series)

angular.module('contacts', []).component('person', {
  bindings: {
    person: '<',
    onRemoveContact: '&'
  },
  controller: class {
    onClick() {
      this.onRemoveContact({ id: this.person.id });
    }

    get fullName() {
      return `${this.person.name} ${this.person.surname}`;
    }
  },
  template: `
    <div>
      <span>{{ $ctrl.fullName }}</span>
      <button ng-click="$ctrl.onClick()">Remove</button>
    </div>
  `
});

Apart from a two minor details (missing constructor method in the controller class, where you can inject component dependencies, and missing transclude option – both not needed in this example), this is all you can put into the NG 1.5 component definition. Simple and straightforward, isn’t it?

Let’s quickly discuss the three main parts of a component:

bindings

Bindings are the parameters passed from the parent component to the child component. They are passed in a template, as attributes of a tag. For example, the component from our example above, could be used in another component’s template like this:

<person person="someObject" on-remove-contact="someFunction(id)"></person>

A component has an isolated scope by default, and should never access its parent scope (or the root scope) directly, so bindings are the only way to pass the data to a component.

There is a new kind of binding introduced in Angular 1.5 – the one-directional binding ('<'). It should be always used to pass data to a component. The old, two-directional binding ('=') should never be used, as the data flow in a component tree should be always one-directional, top-down (more about data flow and communication between components in one of the next posts in the series).

Apart from one-directional binding ('<'), there are two more kinds of bindings you can use in a component: the '&' binding, used to pass callback to a component (used very commonly, to inform component’s parent about some action or a state change – more about this in the upcoming post), and the '@' binding, used to pass static configuration options to a component (used rarely).

Bindings can be also aliased (e.g. instead of person: '<' you could write person: '<contact' and then see the attribute as person="..." in the component’s parent, but see it as this.contact inside the component), but we feel this lowers the readability of the code, and never use this feature in our app.

controller

The responsibility of a controller is twofold:

  • Orchestrating fragments of a component tree (communicating with backend APIs, passing data down to the child components or conditionally hiding and showing them etc.). This happens only in a small number of selected components high in the component tree hierarchy (more about various component responsibilities and communication between components in the next post).
  • Simplifying templates by taking data formatting and presentational logic out of them (the templates should be as “dumb” and simple as possible, the rule of thumb here is to never use anything more than simple variables in a template).

Here is an example of such a presentational component, with all the logic extracted from the template into the controller:

angular.module('streaming', []).component('welcomeScreen', {
  bindings: {
    user: '<',
    supportedCountries: '<'
  },
  controller: class {
    get userFullName() {
      return `${this.user.name} ${this.user.surname}`;
    }

    get userCountry() {
      return this.user.location.country;
    }

    get serviceAvailable() {
      return this.supportedCountries.indexOf(this.userCountry) !== -1;
    }

    get serviceStatus() {
      return this.serviceAvailable ? 'available' : 'unavailable';
    }
  },
  template: `
    <div>
      <span>Hello {{ $ctrl.userFullName }}!</span>
      <span>Our service is {{ $ctrl.serviceStatus }} in {{ $ctrl.userCountry }}.
      <button ng-if="$ctrl.serviceAvailable">Watch the video</button>
    </div>
  `
});

Imagine that you tried to inline all this (seemingly simple) presentational logic directly in the template:

<div>
  <span>Hello {{ $ctrl.user.name }} {{ $ctrl.user.surname }}!</span>
  <span>Our service is {{ $ctrl.supportedCountries.indexOf($ctrl.user.location.country) !== -1 ? 'available' : 'unavailable' }} in {{ $ctrl.user.location.country }}.
  <button ng-if="$ctrl.supportedCountries.indexOf($ctrl.user.location.country) !== -1">Watch the video</button>
</div>

What a mess! Completely unreadable and unmaintainable!

A very useful technique to keep in your toolbox is to use ES5 getter methods in controllers. This gives us two benefits:

  • It makes templates even neater (<span>{{ $ctrl.serviceStatus }}</span> vs <span>{{ $ctrl.serviceStatus() }}</span>).
  • It makes such methods indistinguishable from variables, so it becomes possible to use a plain variable first, and then “upgrade” it to a method when the need to add more complex logic to this variable arises, without having to refactor the template to add parentheses to all invocations.

template

The template is the “meat” of the component and its only required element. There’s not really much to write about the template. The only thing that’s new about the templates in the case of Angular 1.5 components is that all the bound attributes of a component, as well as all the controller’s fields and methods, are visible in a template as the fields of the $ctrl object by default. You can alias this to a different name through the controllerAs option, but we don’t find it useful and never alias it in our app.

Also, like in the previous versions of Angular, a template can be stored in a separate file (you should use templateUrl option instead of template in such case). Opinions about the readability of inline vs external templates were divided in our team. On the one hand it’s nice to have everything on the screen at once, on the other hand, in case of some more complex, bigger components inlining everything makes the file too long, lowering instead of improving readability. For now, we settled for using inline templates everywhere for consistency – but choose what works better for you.

components without controller, bindings, or both

As mentioned above, the template is the only required part of the component definition. Does it make sense, though, to create components without controller or bindings? Wouldn’t they be too simple to justify their existence?

We’ve found from our experience that NG 1.5 makes defining components so straightforward, that creating them even for the simplest UI elements is justified. All 3 cases (no bindings, no controller, and even the template-only components) have their use cases.

For example, it is common for top-level components to have no bindings (as they load the data themselves from the API):

angular.module('contacts', []).component('contactList', {
  controller: class {
    constructor(contactService) {
      this.contacts = contactService.loadContacts();
    }
  },
  template: `
    <div>
      <person ng-repeat="person in $ctrl.contacts" person="person"></person>
    </div>
  `
});

It is also common to have a component with bindings but without a controller when you need to extract a reusable, parameterized piece of the UI but there is no logic associated with it, only simple formatting.

It may seem a little overkill to create components with the template only but even such cases have their place. An example from our application:

angular.module('shared', []).component('progressBar', {
  template: `
    <div layout="row" layout-align="center">
      <md-progress-linear flex="90" md-mode="indeterminate" flex></md-progress-linear>
    </div>
  `
});

Although the above component isn’t parameterized and doesn’t contain any logic, it is still useful to encapsulate Angular Material’s progress bar configuration in a custom component for the sake of reusability and maintainability. And with the new module.component() method there is practically no conceptual overhead to create such simple components.

This leads us to another topic: how such a low barrier to creating components, even for the smallest, simplest pieces of UI, impacts the architecture of an app?

An Angular 1.5 app as a tree of components

The idea behind a fully component-oriented app is to make EVERY distinct part of the UI a component, recurrently rendering other, lower-level components. In effect, there is no main template like in a traditional Angular 1 app. Instead, we have a single, huge tree of components, starting from a single root component (or, in the case of an app using routing, several such trees – one for each view).

the app without routes

The main template of such an app looks like this:

<html>
  <head> ... </head>
  <body>
    <our-app-root-component></our-app-root-component>
  </body>
</html>

As you can see, it is extremely simple compared to the old “the whole app in a single template” approach.

the app with routes

In the case of an app using routes, we replace the root component with a “slot” where the current route is rendered and then provide a separate component tree (with a separate root component) for each view in the router configuration. The main template of such an app looks like this:

<html>
  <head> ... </head>
  <body>
    <div ng-view></div>
  </body>
</html>

and the routing configuration like this:

(the example uses ngRoute but the general idea is the same for any kind of a router)

$routeProvider
  .when('/some-route', {
    template: `<some-route-root-component></some-route-root-component>`
  })
  .when('/other-route', {
    template: `<other-route-root-component></other-route-root-component>`
  });

As you can see, routing also becomes very simple, as the route doesn’t need controller etc. – everything is encapsulated in a single, attribute-less root component.

the structure of a component tree

A part of a component tree of a very simple app could look like this:

(controllers, bindings and tag attributes omitted for brevity)

<!-- <root> component template -->
<side-nav></side-nav>
<contact-list></contact-list>

<!-- <contact-list> component template -->
<search-bar></search-bar>
<add-contact-form></add-contact-form>
<div>
  <person ng-repeat="person in $ctrl.contacts"></person>
</div>

<!-- <person> component template -->
<div>
  <div>
    <span>{{ $ctrl.fullName }}</span>
    <span>{{ $ctrl.mail }}</span>
    <address></address>
  </div>
  <div>
    <button>Edit</button>
    <button>Remove</button>
  </div>  
</div>

Looking at the above templates, you can observe a couple of interesting characteristics of a component-oriented app:

  • All the components are very simple, containing only a few lines of HTML. (In a real, more complex app you can usually find a couple of bigger components, with more complex templates, but still, even in a huge app, the vast majority of components are as simple as on the above example).
  • Most of the components contain almost exclusively our custom tags (i.e. other, lower-level components). Only the lowest-level components (like a person component in our example) contain mostly plain HTML, but even such “leaf” components can encapsulate parts of their templates in custom components for the sake of reusability (notice how in the example the person component uses the address component to encapsulate a simple but reusable part of its template).

It is also worth noting that although the above example is simple, it is nonetheless realistic. Any non-trivial app will surely be much more complex, but the best way to build such an app is to start from a simple structure like the above and gradually introduce new components when needed. For example, if the number of actions in our person component will grow in the future, we may extract them into a separate actions component, or if we’ll want to display not only mail but also phone and Twitter ID, we may introduce a contact-data component, similarly to the address).

Which parts of Angular we use in a component-oriented app?

As you could see in this post, a component-oriented architecture influences which parts of the Angular do we use, and to what degree (for example, you could see that the app template is now just a single line that renders the app’s root component). So, exactly, which parts of Angular do we use in our app?

the parts we don’t use (at all)

  • controllers
  • shared scope (not using a shared scope is a good practice anyway, even without components)
  • partials
  • directives (there are legitimate use cases for them, as NG 1.5 component API is limited – e.g. you can’t match components based on attribute, there is no link function for components etc.; however, after over 3 months of development, we haven’t yet encountered a case that forced us to use a directive)

the parts we use

  • components (yay, surprise! ;))
  • services (no revolution here, they serve exactly the same purpose in a component-oriented app as in a “traditional” one)
  • filters (but only to a relatively small degree; in many cases a filter can be replaced by component’s controller method or by a small, standalone component)
  • constants and values (for simple helper classes, enums, configs etc.)

Conclusion

Such architecture works amazingly well for us. It makes code very maintainable (splitting UI into many small components makes most of the changes local and well isolated), encourages reuse and is easily testable (more about testing Angular 1.5 components in one of the next posts in the series). Although NG 1.5 allows you to use components only to a very small degree if you want, we strongly encourage you to do it on a full scale, like us.

Over to you

Have you also tried NG 1.5 module.component()? How much do you use it? What do you think about the architecture it enables? I’d love to hear your opinion!

Subscribe for the rest of the “NG 1.5 from the trenches” series

If you want to get notified when the next posts in the series get published, subscribe to my blog via RSS, follow me on Twitter or subscribe below via mail:

Enter your email address to follow this blog and receive notifications of new posts by email.

PREVIOUS POST: Starting a new app in Angular 1.5 – does it make sense?
NEXT POST: Communication between Angular 1.5 components (and with the API)

Advertisements

3 thoughts on “Angular 1.5 app as a tree of components [“NG 1.5 from the trenches” 2/7]

  1. Thanks for this post.
    How about DOM Manipulation? I need to implement ng-keydown, but it doesn’t work on a table row () element. However it works fine if I use keydown event handler on a directive link function.

    Would you have a clue?

    Like

  2. Thank you for sharing this series of posts.
    I’m relatively new to Angular, but have worked with Backbone and Ember. Working on two Angular 1.4.9 projects – one professional, the other a hobby project – I’m migrating the latter to ng 1.5, and was really glad to stumble upon this particular post, as I had some trouble implementing a component-based solution!

    Like

What do you think?

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s