Communication between Angular 1.5 components (and with the API) [“NG 1.5 from the trenches” 3/7]

This post is the 3rd 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:

Components must collaborate to provide complex business behavior

In the previous post in the series I’ve discussed the “static” part of the component-oriented Angular 1.5 architecture: the anatomy of a component and how the app can be structured as a component tree. I also mentioned that we use a lot of small, focused components. What I haven’t discussed yet is how all these components communicate (pass data and propagate user actions and state changes). This will be the topic of this post.

A single-directional (top-down) data flow

One good way to make a complex component tree manageable, popularized by React and gaining traction among other major JS frameworks, is to make communication between components one-directional and explicit, through the components’ attributes.

a single-directional data flow has several benefits:

  • It promotes reusability. A child component that doesn’t know about its parent (that is, is not coupled to the internal state of its parent through the shared scope, and doesn’t cause side-effects in its parent through two-directional bindings) can be easily taken out of its parent context and dropped into another parent.
  • It’s easier to reason about. It applies both to a single component (when there’s no coupling between components, they can be analyzed separately) and to the whole component tree (when there are no loops or side-effects, the flow is much easier to follow).
  • It makes components easier to test. Components de-coupled from their parent and child components (by making all inputs and outputs explicit) become more “functional”, easy to isolate.

a single-directional flow in NG 1.5

Angular 1.5 components lend themselves very well towards single-directional data flow.

The NG 1.5 component scope is by default isolated, which enforces passing data explicitly through component’s attributes. This eliminates the first main source of inter-component coupling. The second main source of coupling – two directional bindings – can be eliminated by using another new addition to NG 1.5: the '<' (single-directional) binding.

let see how the NG 1.5 single-directional data flow looks like in the code:

(all code examples in ES6/ES2015 syntax – more about this in one of the following posts)

angular.module('TodoApp', []).component('todosController', {
  controller: class {
    constructor(todoService) {
      todoService.loadTodos().then((todos) => {
        this.todos = todos;
      });
    }

    get showProgress() {
      return angular.isUndefined(this.todos);
    }

    get showList() {
      return angular.isDefined(this.todos);
    }
  },
  template: `
    <progress-bar ng-if="$ctrl.showProgress"></progress-bar>
    <todo-list ng-if="$ctrl.showList" todos="$ctrl.todos"></todo-list>

  `
});

angular.module('TodoApp', []).component('todoList', {
  bindings: {
    todos: '<'
  },
  controller: class {
    get allTodosCount() {
      return this.todos.length;
    }

    get completedTodosCount() {
      return this.todos.filter(todo => todo.isCompleted).length;
    }
  },
  template: `
    <div>
      <todo-item ng-repeat="item in $ctrl.todos" item="item"></todo-item>
    </div>
    <todo-summary all="$ctrl.allTodosCount" completed="$ctrl.completedTodosCount"></todo-summary>
  `
});

angular.module('TodoApp', []).component('todoItem', {
  bindings: {
    item: '<'
  },
  controller: class {
    get text() {
      return this.item.text;
    }

    get isCompleted() {
      return this.item.isCompleted;
    }
  },
  template: `
    <div ng-class="{completed: $ctrl.isCompleted}">{{ $ctrl.text }}</div>
  `
});

Of course, the example is simplified to show only the core principles of the single-directional data flow. Normally, components would be a little more complex. For example, a todosController could render also a top menu and a sidebar, a count of completed todos could be cached or loaded from the server, and so on – but the general “feel” of the information flow would be the same.

a couple of important observations about the above code:

  • Lower-level components are as dumb as possible. For example, notice how a “ component (rendered in the todoList template) takes all the various counters as parameters. It doesn’t know how to load or calculate them, only how to present them as a pretty-formatted summary.
  • All bindings are single-directional. Apart from binding form fields to controller fields inside a single component, there’s no need to use two-directional bindings in NG 1.5 component-oriented architecture.
  • Components on the all levels of hierarchy use a lot of helper methods. Notice, for example, the explicit showProgress and showList methods in todosController (and the fact that we have two opposite methods instead of just negating one of them in the template). Notice also how we use the text and the isCompleted methods in the todoItem component, even if the gain may seem minimal (shortening the invocation in the template just by one dot: $ctrl.isCompleted vs $ctrl.item.isCompleted). Such heavy usage of helpers is not an inherent part of the single-directional data flow, but it supports it very well by making the flow of data easier to see and follow through a component tree.

Controller Components vs Presentational Components

Another thing you could notice in the above code example is that the data is loaded from the server only in the top-most component (todosController). All other, lower-level components only render data passed as tag attributes, but never directly communicate with the outside world.

In a typical app, there will be only a few components communicating with the outside world, usually high-level ones (we call them Controller Components) and a lot of lower-level components responsible for displaying data passed down from Controllers (we call them Presentational Components).

Such a separation of concerns ensures that the business and presentation logic are never intermingled, which makes an app easier to understand.

The characteristics of the two types of components are as follows:

Controller Components:

  • contain the application business logic
  • communicate with the server
  • have very simple templates (do not contain complex HTML structure, only display or hide child components)
  • are stateful, more class-like

Presentational Components:

  • contain only presentation related logic and state (and many of them are stateless)
  • do not communicate with the server
  • may have complex templates, with complicated HTML structure (but don’t have to – a lot of presentational components are small and simple)
  • are more functional

additional Controller Components can be introduced as the app grows

In the above example, a todoSummary component is just a simple presentational component taking two parameters: the count of all todos and the count of completed todos.

angular.module('TodoApp', []).component('todoList', {
  // ...
  template: `
    ...
    <todo-summary all="$ctrl.allTodosCount" completed="$ctrl.completedTodosCount"></todo-summary>
  `
});

However, if we would like to provide more complex summary in the future (e.g. with historical info, user productivity stats, etc.) the topmost todosController component would have to load and pass down a lot of (mostly unrelated) information and would become bloated.

To prevent this, we might introduce additional Controller Component responsible for loading summary data:

angular.module('TodoApp', []).component('todoList', {
  // ...
  template: `
    ...
    <todo-summary-controller></todo-summary-controller>
  `
});

angular.module('TodoApp', []).component('todoSummaryController', {
  controller: class {
    constructor(todoService) {
      todoService.loadStats().then((stats) => {
        this.stats = stats;
      });
    }

    get allTodosCount() {
      return this.stats.allTodosCount;
    }

    // ...
  },
  template: `
    <todo-summary ng-if="$ctrl.stats" all="$ctrl.allTodosCount" completed="$ctrl.completedTodosCount" last-week="$ctrl.lastWeekTodosCount" ... ></todo-summary>
  `
});

Notice that we didn’t add the stats loading logic directly to the todoSummary component, but introduced additional todoSummaryController component, so the two responsibilities (loading and formatting data) are still separated. We’ve also left the original todoSummary component intact (we’ve only added more attributes to it), but of course it can be split into several, smaller and more focused components when the number of displayed stats grows – our new todoSummaryController makes that easy.

Introducing new Controller Components when the app grows is a common scenario. Some of your original, topmost Controller Components may even degrade into a Presentational Components at some point – defining overall layout for a bunch of new, more focused Controller Components.

Passing user actions up

Data flow is only one half of the equation. The second half is processing and propagating user actions up the component tree, from the low-level UI elements (buttons, links, form fields etc.) to the high-level Controller Components handling business logic.

Controller Components vs Presentational Components in the context of passing actions up

In the first part of the post I’ve discussed two types of components: Controller Components, responsible for the app’s business logic, and Presentational Components, responsible for the app’s interface.

The division of their responsibilities stays exactly the same in the context of handling user actions:

  • Presentational Components handle low-level, technical details of registering user input (clicking buttons, filling form fields, etc.) and map them to more “semantic” actions (“create new todo”, “display next page of results”, and so on). They inform their parent Controller Components about these semantic actions, but don’t execute any related business logic nor communicate with the server themselves – their responsibility ends on registering an action and passing it up for the further processing.
  • Controller Components process semantic actions received from their child Presentational Components. They execute appropriate business logic, send the results of an action to the server, and propagate app state changes caused by an action back down the component tree. They never capture actions themselves, though, and they are not concerned about the low-level, technical details of an action.

two types of Presentational Components

As I just mentioned, there are 2 types of actions: low-level, “atomic” actions and high-level, “semantic” ones (on-click vs on-add-todo). Similarly, we can differentiate 2 types of Presentational Components:

  • Atomic Components. They are usually low-level components: buttons, checkboxes, etc. They pass up low-level, technical actions like onClick or onKeypress. There may also be more complex Atomic Components, like a calendar widget, that would pass up more complex actions, for example onSelectDate, but these are still technical actions, not related to the business domain of an app. Atomic components are universal, portable between different apps.

  • Semantic Components. They are high-level components, tightly bound to the business domain of an app, for example todoItem or contactsList. Semantic Components use Atomic Components internally, but they remap actions received from Atomic Components into more abstract ones, expressed in the vocabulary of the app’s business domain: onCompleteTodo or onAddNewContact. Such components are not portable between different apps.

There are two important patterns related to such a division of Presentational Components:

  • Controller Components should always receive semantic actions. Atomic, technical actions should never reach Controller Components. They should always be remapped into your business domain terms before they reach a Controller.
  • Atomic Components should know as little as possible about the context in which they are used. A ‘Complete Todo’ button should know only that it was clicked and should pass this information up to a more semantic component, that can interpret this click (a todoItem or a todoList). A button should never know the ID of the todo to mark as completed or even that the click means anything todo-related. This information should be added by the parent during remapping of an atomic action into a semantic one.

The 3 ways of passing actions up

In our app, we use three mechanisms to pass user actions up:

through callbacks (‘&’ bindings)

Communication through callbacks is the “official” and the most straightforward way of passing information to the parent component. A child component can invoke a callback provided by its parent like this:

angular.module('TodoApp', []).component('todosController', {
  controller: class {
    // ...
    addTodo(title, description) {
      // ...
    }
  },
  template: `
    ...
    <todo-list on-add-todo="$ctrl.addTodo(title, description)" ... ></todo-list>

  `
});

angular.module('TodoApp', []).component('todoList', {
  bindings: {
    // ...
    onAddTodo: '&'
  },
  template: `
    ...
    <button on-click="$ctrl.onAddTodo({ title: $ctrl.title, description: $ctrl.description })">Add Todo</button>
  `
});

Notice a bit unusual way of passing parameters from a child component (onAddTodo({ title: $ctrl.title, description: $ctrl.description }) instead of simply onAddTodo($ctrl.title, $ctrl.description)). This is because the '&' binding is not really a callback function but an expressions, and the title and description are not function parameters but variables visible in the scope of the expression context. In the parent component we could as well do something like this: on-add-todo="$ctrl.todoText = title + ' ' + description", although we don’t recommend it and never do it in our app.

Benefits: Explicit API. It’s clearly visible in the bindings option which callbacks a child component exposes. It’s also easy to see in the parent’s component template which of the child component’s callbacks does it use and how. This makes the communication between components explicit and easy to understand.

Drawbacks: Communication is possible only with a direct parent. It’s impossible to skip through more than one layer of components, so if there are several layers between a component that is the source of the action and the component that should handle the action, you need to push the action through all the intermediary layers, which quickly gets very tedious and makes the component tree harder to refactor.

When to use: The '&' callback should be used by default for all Atomic Components. They are meant to be reused, so making their API explicit makes it easier. Also, the actions from Atomic Components should be remapped to more semantic actions by their direct parents anyway, so in the case of Atomic Components the problem of passing actions through many intermediary component layers doesn’t exist.

through component’s local $scope events

Another built-in Angular mechanism that can be used to pass actions up are local $scope events. A child component can pass actions up via events like this:

angular.module('TodoApp', []).component('todosController', {
  controller: class {
    constructor($scope) {
      $scope.$on('ADD_TODO', (event, title, description) => {
        // ...
      });
    }

    // ...
  },
  template: `
    ...
    <todo-list ... ></todo-list>

  `
});

angular.module('TodoApp', []).component('todoList', {
  controller: class {
    constructor($scope) {
      this.$scope = $scope;
    }

    // ...
  },
  template: `
    ...
    <button on-click="$ctrl.$scope.$emit('ADD_TODO', $ctrl.title, $ctrl.description)">Add Todo</button>
  `
});

Benefits: More flexible component tree structure, easier to refactor. Events can transparently pass through intermediary layers of components, which makes moving a component up or down the hierarchy, or inserting additional components in-between the action source and consumer effortless – you don’t have to explicitly handle and re-emit action in every intermediary component.

Drawbacks: Hidden API. You can’t see as easily as in the case of '&' bindings what events a component emits. Also, it is harder to find which component handles emitted events, because it doesn’t have to be a direct parent of the component emitting an event and it’s not explicitly visible in a template. Another drawback is that $scope events can travel only up, so they can be handled only by the components above in the component tree, but not by the siblings or components in different parts of the UI.

When to use: Passing actions through $scope events is ideal for Semantic Components. They are often separated from their Controller Components by a few layers of simple layout components. Events make it easier to tunnel an action through all these intermediary components. For Atomic Components it makes less sense, as their actions are usually handled and remapped to more semantic ones by their direct parent anyway.

through global $rootScope events

The last Angular mechanism we use to pass information about app state changes are global $rootScope events.

The syntax is identical to the local $scope events, so I won’t duplicate the code example – take a look at the section above and replace all the $scope occurrences with the $rootScope. However, there is an important difference in how the $rootScope work: they don’t bubble up the component tree, but have to be subscribed to globally, at the topmost, $rootScope level.

Benefits: Can connect any two components, no matter where they are placed in the component tree hierarchy. This makes it possible to pass information between sibling components, or even between two completely separate parts of the UI. It also makes it possible to pass events between components and services.

Drawbacks: Global events are, well, global, and as anything global should be used sparingly. Otherwise, your app can quickly become hard to reason about and maintain. Global events amplify the main drawback of local events – hidden API. In the case of global events it’s even harder to find who handles an event, as you can’t just search directly up the component tree; an event can be potentially handled by any component in the whole app, or even not by a component but a service.

When to use: For synchronization between high-level Controller Components, orchestrating different parts of the UI. An app usually consists of a couple of loosely related parts (for example a todo app can display a list of todos in the main pane and a list of categories or tags in the sidebar). The responsibilities of these parts are separate enough to justify individual Controller Components and individual component tree sub-hierarchies. However, although mostly separate, they are not completely independent – for example deleting a todo in the main pane should refresh todo counts for categories in the sidebar. Global $rootScope events are a great tool for such a synchronization.

the flow of actions through the component tree – a complete example

Here you can see how all three ways to pass actions can coexist in an app:

a complete flow of actions through the component tree

A button (an Atomic Component) generates a low-level, technical action: onClick. Because this action is immediately handled by the button’s direct parent, the todoItem component, it is passed via '&' binding. The todoItem (a Semantic Component) re-maps the technical action into a semantic one, using the app’s business domain terms: onCompleteTodo. This action is then tunneled through a couple of intermediary layout components using $scope events mechanism. The todosController (a Controller Component) handles the action (sends a request to the server, re-renders the sub-part of the component tree it is responsible for, and so on). In addition, it informs another Controller Component, orchestrating the tags list in the sidebar, that the count of completed todos has changed (so that the sidebar could be refreshed if needed). This communication between two Controller Components happens through a global $rootScope event.

An important side-note: It may seem from the diagram that the components lower in the component tree send actions to the components higher in the hierarchy. However, it is critical to notice, that the action senders are actually completely unaware about the action receivers. The completeTodoButton just invokes a callback passed to it via binding, but doesn’t know who passed this callback and how it will be interpreted. Similarly, the todoItem component emits a $scope event but it doesn’t know which component, and how many layers above, will handle it. Even the todosController doesn’t send the onCompletedCountChanged event directly to the tagsController; it just emits this event through the $rootScope but doesn’t know if and how many other Controller Components or services will subscribe to receive this event.

what about Flux?

Our architecture is conceptually similar to the Flux architecture, popularized by React. Why didn’t we just adapt some of the available Flux libraries, then?

The answer is our philosophy of gradual progression. We’ve intentionally chosen to start with the simplest tools possible, ideally only those built-into Angular. That’s why we’ve started with jqLite, ngRoute, and plain $http instead of full jQuery, UI Router, or Restangular. It was a good decision: it flattened the learning curve, and after 4 months of development these tools are still sufficient for our needs.

We approached the architecture of our component-tree in a similarly minimalistic manner. In this case, however, the initial, simple solution turned out to be not sufficient. It wasn’t a big leap, though, but a gradual progression:

  • At first, our component tree was relatively flat, so we used only '&' bindings. We have also stored all of the application state in components.
  • As the app grew and the component tree became deeper, we’ve introduced local $scope events. We still had only 2 Controller Components though.
  • As the app grew even further, we’ve introduced more Controller Components and global $rootScope events so they could synchronize. The app state was still kept in the components, though.
  • Just recently, we’ve started moving parts of the app state from components to a centralized Flux-like “Data Store” (implemented using Angular Service).

We still don’t feel the need to introduce external Flux implementation. Angular provides enough tools to enable robust architecture without resourcing to external libraries. However, our app is still growing, so we don’t dismiss the idea that we may introduce such a library at some point.

Conclusion

The data and user actions flow architecture, presented in this post, works really well for us. We have progressively developed and battle tested it during the 4 months of the development of our app, and are fond of the result. We strongly recommend using a similar approach in your app. A conceptually similar architecture can be achieved in several ways, using various tools, but we encourage you to try out Angular built-in functionalities first, before resorting to external libraries.

Over to you

How do you pass actions and data in your app? Do you have any preferred mechanism, or do you mix different ones, like us? Have you tried a full Flux architecture in Angular? I’d love to hear about your experience!

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.

Advertisements

8 thoughts on “Communication between Angular 1.5 components (and with the API) [“NG 1.5 from the trenches” 3/7]

  1. Woow, finally someone presented how to properly do a more complicated communication in angular app. Anyway it’s funny that this is reinventing a flux, but from my experience it is the best (the most maintainable) way to build an app.

    I would like to see how it is connected with factories to keep data on page transitions and how it propagates changes to server or any other async(!) storage. Do you do any autosave with this or only by form submitting etc.

    Like

  2. I must say, this series is awesome. I decided to go with ng-1.5 on my latest project and this has to be one of the best resources I have found so far. Thanks for sharing, I look forward to reading the rest of the series.

    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