Angular component method, the natural evolution
One of the cool things that we "were/are/will be" using while coding our front applications with AngularJs is the option to use directives to create our own reusable components. Basically, the main idea of the component was (...and is ...and will be) to have reusable elements that brings their own encapsulated functionality and also the ways to manipulate the data they need. If we were following the common known best practices, we were creating custom components by:
- isolating its scope
- adding the controller directly in the directive and exposing it under
controllerAs
name - avoiding direct changes in the parent data
- using bindToController to bind properties to it instead to the scope
- using directives restricted to elements
To help us achieving this coding way, Angular 1.5 has introduced the component()
method in order to simplify the task of creating components (concept that will be also completely integrated in Angular 2). Now, instead of implementing similar views and controllers over and over again, this approach enables to easily create components. So, what is a component in Angular?... Quoting Angular documentation: "a component is a special kind of directive which represents a self-contained UI component and that uses a simpler configuration which is suitable for a component-based application structure". For us, as developers, it means that we can stop implementing similar directives/controllers/... over and over again, and start to use components that can be created once, reusable and also composed into bigger components (components tree).
Let's go deeper...
Component works internally as a directive with some default properties. To see it, let's review the Angular code . There, we can find the component method definition:
this.component = function registerComponent(name, options) {
First of all, the function that is behind the component method. As we can see, it is created following the pattern named function expression ("NFE") and uses two parameters:
- Name: string that Works in the same way than in other directives (camelCase normalized which in the DOM corresponds with lower-case dash-delimited tag name).
- Options: object with the configuration of the component. Here we find the first difference with directives in which the second parameter is a factory function. In fact, the object parameter is basically a simplified version of the one returned by that function.
...and what does the function return?
return this.directive(name, factory);
As I mentioned, component works internally like one directive, so what it returns is a definition of a directive, with its name (that comes from the first parameter used in the definition) and its factory function. Let's review it...
function factory($injector) {
factory
is a function and we should know what it returns. Basically one object with configuration for the directive, but in this case, with some remarkable default values and without other usual ones.
return {
controller: controller,
Controller uses the value of the homonym variable, which is set with a default value if options doesn't have a controller function.
controllerAs: identifierForController(options.controller) || options.controllerAs || '$ctrl',
It uses automatically controllerAs syntax and if it is not defined in the configuration object, the default value is $ctrl
template: makeInjectable(template),
templateUrl: makeInjectable(options.templateUrl),
template
and templateUrl
are used in the same way than in all directives, and if it is not defined in options, it will set as empty string (done by makeInjectable
function).
transclude: options.transclude,
Transclusion is optional and it is disabled by default.
scope: {},
The returned directive has an isolate scope and it doesn't have any property bound.
bindToController: options.bindings || {},
It uses the property bindings
to extend the controller and, as we can see, it is on by default (if bindings property is not defined, it sets an empty object) . bindings
is an object which properties that are attributes of the component. It works as an API that handles the Input and Outputs of the component. The only data that is passed is the one that is needed for the component to behave as expected and the component should never modify any data that is out of their own scope. In order to accomplish this goal, Angular allow us to use one-way bindings by the use of <
in our bindings. So <
and @
bindings are useful for the inputs, and the outputs can be realized with &
bindings:
// inputs for the component
'<' for one way binding
'@' for strings
// output that works as function callbacks
'&' for callbacks to component events.
As I have mentioned, the component should never modify the input data. Instead of it, the component can call the Output Event with the changed data, sending it back to the owner component (ancestor component or directive). The parent is the one that modify the data it owns.
restrict: 'E',
The components are directives restricted to elements
require: options.require
It allows the communication between components, if the value is an object in which the property values are the names of the required controllers (components name) and the keys are the values that will hold in the current controller. And this is all. There are no more properties, so this is the returned object and, as we can see, it is a subset of properties that could be used in the definition of any directive (https://docs.angularjs.org/api/ng/service/$compile#directive-definition-object). But as remarkable as is the default properties that the object has, there are properties that are often in directives but are not defined in component... Do you miss any any of them?... if the answer is yes, I guess you have seen that it doesn't include link and compile functions. Neither others like priority or terminal. This lead us to think that components shouldn't be used if we need/want to manipulate the DOM. At the end of the article there is a table with all the properties that directive and component definition object allow to use.
Code simpler and clean...
So we could compare our component code with and old style directive:
myapp.directive('panel', function() {
return {
restrict: 'E',
templateUrl: 'panel.tpl.html',
controller: function panelCtrl() {},
controllerAs: '$ctrl',
bindToController: true,
scope: {
config: '='
}
}
});
And as component, saving from boilerplate and with the default properties:
myapp.component('panel', {
controller: function panelCtrl() {},
templateUrl: 'panel.tpl.html',
bindings: {
config: '< ' // one way binding
}
});
The mandatory function that always return an object has disappeared, also other often repeated properties. Therefore we have a simpler and more straightforward code that does the same. Also in the component the property attached to the bindings uses '<', which has more sense because there is no reason to keep the two way binding for the configuration that the component uses.
Concluding...
So what is component()
method good for? What are its advantages?:
- it saves us from boilerplate code and keep the code simple and clean. It simplifies the configuration of the directives that we were building to generate components... what it is the most of the cases, right?
- it integrates default values that have become best practices (also promotes to continue with their use)
- it is optimized for component-based architecture
- it leads us to have an easier way to upgrade to Angular 2
...but, as we saw, we should not use component method if:
- we want to manipulate the DOM (how could it be without link or compile functions?)
- we need to define priority
- the directive should be triggered by other way than element (for instance, attribute or class value)
Directive | Component | |
---|---|---|
bindings | No | Yes (binds to controller) |
bindToController | Yes (default: false) | No (use bindings instead) |
compile function | Yes | No |
controller | Yes | Yes (default function() {}) |
controllerAs | Yes (default: false) | Yes (default: $ctrl) |
link functions | Yes | No |
multiElement | Yes | No |
priority | Yes | No |
require | Yes | Yes |
restrict | Yes | No (restricted to elements only) |
scope | Yes (default: false) | No (scope is always isolate) |
template | Yes | Yes, injectable |
templateNamespace | Yes | No |
templateUrl | Yes | Yes, injectable |
terminal | Yes | No |
transclude | Yes (default: false) | Yes (default: false) |
...extracted from https://docs.angularjs.org/guide/component