Angular components communication

Mar 17, 2016 in AngularJS, components, angular2

Modern UI is all about flexible and reusable components, I see this as an essential part of the frontend workflow. They are also so easy to use, test and develop. However, there might be a confusion about how to componentize your views/widgets in the most transparent and clear way. What make a good component? What is the optimal granularity? I will try to answer these questions.

What is the component?

Here is the simple I use for myself definition:

Component is an independent, single-concern, black-boxed UI entity.

Each of the characteristics this definition contains are what actually makes an arbitrary piece of HTML a component.

Independence

A good component should not depend on its surroundings. This is very important. It means, that in order to function properly component only needs very few things from its environment: this is a contracts and interfaces component provides. It also means that plugging in a new component or swapping it with another one should be relatively easy task, as soon as component adheres to contract.

Because of this, component is very loosely coupled to it's surrounding, which makes it much simpler to develop and write unit tests for.

Single concern

The more component does, the more complex it becomes. The purpose of component is to achieve optimal granularity: not only structural but also functional, and thus this is a responsibility of developer to find this ideal balance between complexity of component and it's functionality. This is common sense: the more advanced component is, the more things it can do, the more difficult it is both to develop and maintain. Component complexity answers to the question: how much functionality is packed in it. It is often possible to tell if component will be easy to reuse by just looking at how much things it can do, and while component can serve several purposes, this is normally what we should avoid.

That being said, one should not confuse overcomplicated component that does too much stuff, and component that uses composition of several simpler components. In fact, this is our goal in component-based design. Something worth emphasizing:

Component composition solves complexity.

Black-boxed

Ideal component abstracts its implementation and complexity behind simple API/interface. In this context interface has special meaning, i.e. the means by which outer world can interact with a component:

Now that I highlighted main conditions, let's see how component-based approach can actually be implemented and used in code. I will use Angular 1.5 and it's component method. In part 2 will do the same in Angular 2. In fact, if you design your code with components in mind, it will be pretty easy to convert your code to Angular 2. But this is the topic for another article. Now, it's time for code.

Component-based design

For the purpose of the article, let's imagine that we have two components that want to interact with each other. The first component will have buttons to select a view setting, and the second one needs to react on those choices, rendering something according to currently selected view.

We can illustrate it with this simple diagram:

Angular component scheme

The question: how to make header component talk to the main one?

What I'm about to write here is based on my Stackoverflow answer I recently posted, so check it out, maybe upvote if you like it ;)

So.. We basically want header and main components to share some piece of state to be able to use it. There are several approaches we can use to make it work, but the simplest is to make use of intermediate parent controller property. So let's assume parent controller (or component) defines this view property you want to be used by both header (can read and modify) and main (can read) components.

Header component: input and output

Here is how simple header component could look like:

.component('headerComponent', {
  template: `
    <h3>Header component</h3>
    <a ng-class="{'btn-primary': $ctrl.view === 'list'}" ng-click="$ctrl.setView('list')">List</a>
    <a ng-class="{'btn-primary': $ctrl.view === 'table'}" ng-click="$ctrl.setView('table')">Table</a>
  `,
  controller: function( ) {
    this.setView = function(view) {
      this.view = view
      this.onViewChange({$event: {view: view}})
    }
  },
  bindings: {
    view: '<',
    onViewChange: '&'
  }
})

The most important part here is bindings. With view: < we specify that header component reads outer something and binds it as a view property of its own controller. With onViewChange: '&' binding component defines outputs: the channel for notifying/updating outer world with whatever it needs. Header component will push some data through this channel, but it doesn't know what parent component will do with it, and it should not care.

Pay attention on how you invoke this.onViewChange() function, basically calling a outer callback.

So it means that header component can be used something like this

<header-component view="root.view" on-view-change="root.view = $event.view"></header-component>

Main component: input

Main component is simpler. It doesn't need to change the state, so it only needs to define input it accepts. Here is relevant implementation:

.component('mainComponent', {
  template: `
    <h4>Main component</h4>
    Main view: {{ $ctrl.view }}
  `,
  bindings: {
    view: '<'
  }
})

Now when view input changes outside due to whatever reason (remember, mainComponent doesn't know about headerComponent existence), mainComponent will pick up this change, thanks to framework change detection.

It's time to wire everything up together!

Parent component

Parent component is responsible for mediating communication between its child components, however it doesn't do it directly. Strictly speaking, parent-child relationship is very loose: parent component doesn't need to interact with its children directly, instead parent "provides" children its property they can use: read (input) or invoke if it is a function (output).

<header-component view="root.view" on-view-change="root.view = $event.view"></header-component>
<main-component view="root.view"></main-component>

So one more time: header takes in root.view (input), and pushes its changes back via on-view-change="root.view = $event.view" thus modifying original parent component root.view. Main component just uses view as an input.

Now we can see it in action:

Conclusion

So that was it. Using a parent component/controller, or rather it's properties, as a mean of communication between child components is a powerful and simple way to set up loosely-coupled component architecture. However, there are other approaches that can be used for the same purpose, mainly event-based communication and shared services. Both have its own advantages and disadvantages. I will cover these mechanisms in the next series of posts. Stay tuned and leave feedback!

comments powered by Disqus