Component composition in Angular2 — Part 1
When developers work on applications, they think about good designs by avoiding code duplication (DRY pattern) and reusing or extending entities. Such approach also applies to Angular 2 applications. We see some discussions regarding such aspects and the ways to address them in such applications aren’t obvious.
In a previous post, we described how decorators work in Angular 2 and which metadata they add on associated classes. We also showed how to make them work along with class inheritance.
Whereas this seems to be promising, this approach suffers from drawbacks and isn’t the recommended one by the Angular team. The main drawback is that it prevents us from using offline compile to precompile component templates. Using a custom decorator for components also prevents external tools from detecting that they are actually components.
In this article, we will describe another approach based on the component composition based on components and attribute directives. We will deal with the way to implement it, its advantages and limitations.
What is component / directive composition?
Angular 2 defines three kinds of directives:
- Components. They are directives with a content defined in a template. They are the most common ones.
- Attribute directives. They aim to change the appearance or behavior of a DOM element or a component.
- Structural directives. They aim to change the DOM layout by adding and removing DOM elements. They are linked to the template element.
Defining attribute directives
An attribute directive aims to change the appearance or behavior of a DOM element or a component. For this, they are applied to existing elements or component based on selectors.
Defining such directives is really simple using the Directive decorator. Here is a sample:
The instance of ElementRef provided in the constructor corresponds to the element the directive is applied on.
Like components, we can add inputs and outputs. Attribute directives leverage the same lifecycle as components.
Applying attribute directives
We can leverage two approaches applying directives:
- The transparent one. Based on an HTML element or attribute, there is no need to add additional things to apply directives. Such an approach is used by Angular 2 for forms: directives are added under the hood on form and form elements (input, select, text area).
- The explicit one. To apply directives, we need to specify this to add an additional attribute for example. For forms, this corresponds for example to the adding of the required attribute. In this case, a specific directive is applied.
Let’s take the sample of a component with the comp selector. With the first approach, the directive will use the same selector and will be automatically applied when the comp HTML element is used.
If the directive an explicit attribute to be applied, the selector would be now comp[applied] for example.
In all cases, we need to have the directive class present in the directives attribute of the component we want to use it.
Usage in Angular 2
Such directives are used under the hood by Angular 2 itself. Let’s take the sample of forms. By default, with HTML5, when we click on a submit button, the data are posted and the page is reloaded. This behavior obviously isn’t acceptable for SPAs since we need to stay on the same HTML page.
To do that, Angular 2 automatically and transparently applies the NgForm directive to form elements. This directive intercepts the event and doesn’t propagate it but rather trigger an ngSubmit event.
This sample is simple and such behavior can also be used for custom components. This allows us to compose and adapt components according to use cases. We will describe now which support Angular 2 provides to implement component composition.
Linking and updating the host component
An attribute directive can be applied to HTML elements or components. In the second case, it’s possible to interact with components, called host components, it applies on.
The following figure provides an overview of links that can be implemented between component and directives applied on.
Referencing the host component
The corresponding instance is available in the dependency injection. This means that the directive can specify it within its constructor parameters and receive the corresponding instance.
The following snippet describes how to inject the component a directive is applied on:
Angular 2 also provides a set of decorators to reference elements like properties, attributes, styles or classes from applied directives.
The @HostBinding decorator allows us to link a directive property to an element of the host component.
The following expressions are supported at this level:
- propertyName: references a property of the host with the propertyName name.
- attr.attributeName: references an attribute of the host with the attributeName name. The initial value is set to the associated directive property. Setting a value in the property updates the attribute on the corresponding HTML element. Using the null value at this level removes the attribute on the HTML element.
- style.styleName: links a directive property to a style of the HTML element.
- classNames: links a directive property to the classes of the HTML element. All the specified classes will be defined on the HTML element.
- class.className: links a directive property to a class name of the HTML element. If the value is true, the class is added otherwise removed.
The following snippet describes how to use the @HostBinding decorator:
The @HostListener decorator is similar to the @HostBinding one but tackles events that the host component can trigger. This decorator can only decorate methods. These methods will be called when the corresponding event will be triggered.
The supported expressions follow the following pattern:
- eventName: the name of the event to register a method callback on.
The following snippet describes how to use the @HostListener decorator:
We can also notice that some prefixes are supported to register from directives events triggered by the window or document objects:
- window.eventName: for events linked to the window object.
- document.eventName: for events linked with the document object.
Component composition and providers
The component and its applied attribute directives share the same injector. This means that it’s possible to use the providers attribute of directives to “extend” the providers one of the component(s) they apply on.
Configuring providers for several components
Since the injector is shared, directives can define providers that will be used on host components. For this, simply use the providers attribute of the Directive decorator.
The following snippet describes a sample of use:
Leveraging multiple providers
An interesting approach at this level consists of leveraging multiple providers. Each directive can register a provider. The component directives apply on can then use all the registered providers.
The following snippet describes how to register a multiple provider:
The component will be then able to get all these providers using dependency injection from its constructor:
Changing styles of components
It’s not possible to change styles of components from directives out of the box. I mean adding or removing entries in the styles and styleUrls attributes.
That being said, we can leverage classes on the root element of the component to apply another set of styles. The :host selector will help us to use the classes on the root element to define specific styles.
The following snippet describes how to leverage the :host selector and a directive to change applying styles from a directive.
In this first part, we describe foundations of component composition available in Angular 2 based on components and attribute directives. We described how to implement such directives and make them interact with their host components.
Such techniques provide an interesting approach to organize Angular 2 applications and make them more structured, maintainable and easy to test.
In the next part, we will see that class inheritance can be helpful to share common elements like inputs and outputs and for polymorphism. We also describe how content projection and template elements can be used to make components extensible.