In Part 1 of this blog post we demonstrated how to create a simple Angular 2.0 "style" component using Angular 1.x and TypeScript. We did not focus on different component types that can be created when writing our applications and how to structure your applications with them. We touched upon different component types in a previous blog post, but the goal of this post is to clarify and classify those types a little further and provide a concrete example.
As was mentioned in the previous post, we can view the structure of Angular 2.0 application as a tree of components, and that one of the design goals for Angular 2.0 is to bridge the gap between the framework and the Web Components standards.
Component Types
When designing applications an important distinction between different component types can simplify our mental model of the whole application. The good news is that Angular 2.0 or even Angular 2.0 style components written in Angular 1.x can fall into 2 categories for the most part. Dumb components and Smart components.
Dumb components are re-usable across different applications and are unaware of the application's domain model (hence the name), facilitating an easy re-use and aligning themselves more closely in the direction of Web Component standards. Smart components on the other hand are heavily tied into the domain of the application, share a lot with traditional controllers shuffling data from the domain and build their views based on dumb components.
Dumb Components
The reason we call those components dumb stems from the fact that they are unaware of your application's domain model. They get data passed into them, and can have complex logic encapsulated within, however this logic must be view related and have nothing to do with your application's domain model. This constraint facilitates greater options for re-use of those components in other contexts, and as a result those types of components align even closer with Web Component standards.
As you might already know, Web Component standards aim to allow for the creation of re-usable and interoperable web components that are easy to bring into different applications. The main objective being to allow developers and designers to easily extend the existing DOM with new elements.
To recap, let's start with a simple example of an existing HTML element:
<input type="text" value="Kill Bill" onblur="someFunction()"/>
If we ignore the implicit simplicity of the markup above and look at it from a Dumb component perspective, we can see some properties emerging.
First, this component can accept some inputs via the type and value attributes, which modify how the component looks and behaves. The second part of this is defined by the onblur attribute above, which allows this component to emit events and output data. In essence, we can think of those properties as defining the "API" of this component. This allows us to pass data in to modify the behaviour of this component, and get data out to respond to the events emitted by it.
Now, the purpose of Dumb components is to allow you to extend your DOM options, and encapsulate chunks of your view functionality into modular, re-usable parts that are not dependent on your application's domain that can be used like an example below.
<my-cool-component cool-data="someData" on-cool-event="someCallback(dataOut)"/>
Now, I am not surprised if you are not impressed. Directives in Angular 1.x were doing this all along for Angular applications with Ionic as the prime example of using those ideas to a great effect. Also, Polymer is emerging as an interoperable option, especially popular with "designer types", and aligned very closely with Web Component standards.
Those ideas are not new to a lot of us, but I believe that it is useful for our transition to Angular 2 that we start taking those examples for what they are - different ways to emulate or create "Web Components" style of component that extend the DOM, and provide us with more advanced building blocks for our views.
Smart Components
Dumb components as defined above are great, however in reality your application will contain services/stores and other entities that define the business logic of your application. Those entities can be used to pass data in a hierarchical manner to your web components using their API. However, since everything is a component in Angular 2, those entities need to be structured within components as well. Here come our top level, controller components or as we call them, Smart components. Those components are not as easily re-usable outside of the context of a specific application, but are necessary to implement it.
Application Architecture with Components
For the most part, Smart components are suitable to defining distinct, "controller" layer, which is responsible to shuffle data back and forth between the domain and the view layers of your application. Smart components, usually fit quite nicely into routing, and for the most part your routes should lead to those components.
In contrast, Dumb components are more hierarchical in structure. Smart components pass data into them from above, and Dumb components can pass data further down to other Dumb components and respond to their events. Dumb components should be the primary building blocks of your views. They are domain independent, can be re-used with greater ease, and only rely on their "API" allowing for greater interoperability, overall resulting in a more flexible design.
Example
We will implement 2 components, a <ncg-message-list/> top level component talking to a service and passing in data to <ngc-message> component instances.
Let's start with our <ngc-message> component which will accept a message to be rendered, and will fire an event to return an upper case version of the message. This is the API of this little component defining it's inputs and outputs.
import {Inject} from '../utils';
export class NgcMessageComponent {
public static selector = 'ngc-message';
private static options = {
bindToController: {
message: '=',
onShowMessageEvent: '&'
}
};
public static template = `
<div>{{ ctrl.message }}</div>
<button ng-click="ctrl.onMessageButtonClick()">emit message</button>
`;
private message: String;
private onShowMessageEvent: Function;
constructor(@Inject('$log') private $log: angular.ILogService) {
this.$log.info('NgcMessageComponent');
}
onMessageButtonClick() {
this.onShowMessageEvent({
data: this.message.toUpperCase()
});
}
}
Now lets define our Smart component <ncg-message-list>. This component will iterate over the messages it gets from an injected service and pass them in as data into the <ncg-message> component. This component will also respond to an event emitted by the <ncg-message> with an alert of the message (the data) passed out of it.
import {Inject} from '../utils';
import {NgcMessageService} from '../services/message-service';
export class NgcMessageListComponent {
public static selector = 'ngc-message-list';
public static template = `
<form>
<input type="text" ng-model="ctrl.message"/>
<button type="submit" ng-click="ctrl.addMessage(ctrl.message)">Add a message</button>
</form>
<ngc-message ng-repeat="message in ctrl.messages"
message="message"
on-show-message-event="ctrl.alertMessage(data)">
</ngc-mesage>
`;
private message: String;
constructor(
@Inject('$log') private $log: angular.ILogService,
@Inject('ngcMessageService') private ngcMessageService: NgcMessageService) {
this.$log.info('NgcAppComponent');
}
get messages() {
return this.ngcMessageService.messages;
}
addMessage(message: String) {
this.ngcMessageService.addMessage(message);
this.message = '';
}
alertMessage(data) {
alert('The message is: ' + data);
}
}
Finally, here is our message service:
import {Inject} from '../utils';
export class NgcMessageService {
private _messages: Array<String>;
constructor(@Inject('$log') private $log: angular.ILogService) {
this.$log.info('NgcMessageService');
this._messages = new Array<String>();
}
get messages() {
return this._messages;
}
addMessage(message: String) {
this._messages.push(message);
}
}
I am leaving out the other files in this application for brevity. The full source can be found here, it contains instructions on how to compile and run the app.
Conclusions
When designing your application it is useful to think about what types of component to write where. In short, it makes the most sense to start with top level components that work with services, stores (in case of Flux style architectures) and the other entities of your applications. The main responsibility of your top level components is to gather and control the flow of information out of those entities, hence the reference to controllers above.
Top level components should build their view out of Dumb components that are unaware of the rest of the application and just get their inputs based on their "API". Again, their primary concern is to provide the building blocks, that allow us to create complex views for our applications in a modular and re-usable fashion.
The ideas outlined above are not unique to Angular, the React community has reached similar conclusions, and most definetly served as an inspiration. Dan Abramov's blog illustrates this well.