From the moment I started working with Angular, I've been curious about the distinction between components and directives. This is a persistent question, especially for developers coming from AngularJS, where we only had directives that we frequently used as components. When you look for answers online, you'll find statements like these:
Components are directives that have a template-defined content...Angular components are a subset of directives. Unlike directives, components always have...
Components are higher-order directives with templates that serve as...
These statements appear valid, since when I inspected the factories generated for components, I found no component definitions there! And you won't find them either. Only directives...
I also haven't discovered an explanation for why this happens, because providing one requires a deep understanding of Angular's internals. If this question has been on your mind, this article is for you. It aims to uncover the mystery, but prepare for some heavy material.
In summary, this article explains how Angular represents components and directives under the hood and introduces a new type of view node definition — the directive definition.
The classic view
If you've read some of my earlier articles, especially about how Angular updates the DOM, you're likely aware that underneath Angular, an application is structured as a tree of views. Each view is created from a factory and consists of various types of view nodes, each serving a specific purpose. In that earlier piece (reading it will greatly aid your understanding of this one), I described the two simplest node types — element definitions and text definitions. The former is generated for all DOM element nodes, and the latter is produced for all text nodes.
So, if you have a template like this:
<div><h1>Hello {{name}}</h1></div>
the compiler will generate a view definition containing two element nodes for the div and h1 DOM elements, plus one text node for the Hello {{name}} portion. These nodes are crucial; without them, nothing would appear on the screen. But because the component composition pattern requires that we can nest components, there must be another type of view node for embedded components. To identify those special nodes, let's first examine what a component consists of. A component is basically a DOM element with attached behavior implemented in the component class. Let's begin with the DOM element.
Custom DOM elements
You likely know that you can create a new HTML tag and employ it in your HTML. For example, if you avoid any framework and insert <a-comp></a-comp> into your HTML, then query the DOM node and inspect its type, you'll find it's a legitimately valid DOM element:
const element = document.querySelector('a-comp');
element.nodeType === Node.ELEMENT_NODE; // true
This a-comp element will be created by the browser using the HTMLUnknownElement interface, which inherits from the HTMLElement interface, but without adding any extra properties or methods. You can style it with CSS and also attach event listeners for standard events like click. As mentioned, it's a completely valid HTML element.
You can create a significantly enhanced version of this element by turning it into a custom element. This requires defining a class for it and registering it through the provided API:
class AComponent extends HTMLElement {...}
window.customElements.define('a-comp', AComponent);
Does that ring a bell with something you've been doing?
Exactly, this closely mirrors what we do in Angular when declaring a component. In fact, Angular adheres quite closely to the web components specification but streamlines many aspects so we don't need to manually create a shadow root and attach it to the host element. However, the components we create in Angular are not registered as custom elements and are handled by the framework in a distinct manner. If you're curious about creating a component without a framework, check out Custom Elements v1: Reusable Web Components.
So, we've seen that we can create any HTML tag and use it in a template. Unsurprisingly, if we use such a tag in an Angular component's template, the framework will create an element definition for it:
function View_AppComponent_0(_l) {
return jit_viewDef2(0, [
jit_elementDef3(0, null, null, 1, 'a-comp', [], ...)
])
}
However, you must inform Angular that you're using a custom element by adding schemas: [CUSTOM_ELEMENTS_SCHEMA] to the module or component decorator properties; otherwise, the Angular compiler will raise an error:
'a-comp' is not a known element:
1. If 'a-comp' is an Angular component, then ...
2. If 'a-comp' is a Web Component then add...
We now have the element but are missing the class. Is there anything in Angular that includes a class besides a component? Certainly — a directive! Let's add a directive and see what we get.
Directive definition
You might already know that each directive has a selector that can target a specific DOM element. Most directives use attribute selectors, but element selectors are also entirely valid. For instance, Angular's form directive uses the element selector form to implicitly attach specific behavior to HTML forms.
Therefore, we can create a do-nothing directive and apply it to our custom element. Let's do that and inspect the resulting view definition:
@Directive({selector: 'a-comp'})
export class ADirective {}
Now, let's check the factory:
function View_AppComponent_0(_l) {
return jit_viewDef2(0, [
jit_elementDef3(0, null, null, 1, 'a-comp', [], ...),
jit_directiveDef4(16384, null, 0, jit_ADirective5, [],...)
], null, null);
}
Great, now the compiler has added a new jit_directiveDef4 node to the view definition alongside the element definition. It also set the childCount parameter for the element definition to 1 because all directives applied to an element are regarded as children of that element.
The newly added directive definition is a straightforward node definition generated by the directiveDef function. It accepts these parameters:
+----------------+-------------------------------------------+
| Name | Description |
+----------------+-------------------------------------------+
| matchedQueries | used when querying child nodes |
| childCount | specifies how many children |
| | the current element have |
| ctor | reference to the component or |
| | directive constructor |
| deps | an array of constructor dependencies |
| props | an array of input property bindings |
| outputs | an array of output property bindings |
+----------------+-------------------------------------------+
For this article, we're only concerned with the ctor parameter. It's simply a reference to the ADirective class we defined. When Angular instantiate directives (I'll cover this soon, so follow me for updates), it will create a directive class instance here. That instance is then stored as provider data on the view node.
So, our tests indicate that a component is simply an element plus a directive definition. Is that all? As you might guess, it's never that straightforward with Angular.
Representing a component
I've shown above how we can simulate a component by creating a custom HTML element and a directive targeting that element. Let's now define a real component and compare the generated factory with our experimental one:
@Component({
selector: 'a-comp',
template: '<span>I am A component</span>'
})
export class AComponent {}
Ready to compare? Here's the generated factory:
function View_AppComponent_0() {
return jit_viewDef2(0, [
jit_elementDef3(0, null, null, 1, 'a-comp', [], ...
jit_View_AComponent_04, jit__object_Object_5),
jit_directiveDef6(49152, null, 0, jit_AComponent7, [], ...)
So, we've just confirmed what we suspected from previous sections. Indeed, Angular represents a component as two view nodes — an element and a directive definition. However, with a genuine component, there are differences in the parameter lists for the element and directive definition nodes. Let's examine those.
Node flags
Node flags serve as the first parameter for all node definitions. It's a bitmask of node flags that contain specific node information used mainly during the change detection cycle. This number differs between our cases: 16384 for the simple directive and 49152 for the component directive. To grasp which flags the compiler set, let's convert these numbers to binary:
16384 = 100000000000000 // 15th bit set
49152 = 1100000000000000 // 15th and 16th bit set
If you're interested in how that conversion works, read about the simple math behind decimal-binary conversion algorithms. For the simple directive, the compiler only sets the 15-th bit, which is done in Angular's source code like this:
TypeDirective = 1 << 14
For the component node, both the 15-th and 16-th bits are set, which is:
TypeDirective = 1 << 14
Component = 1 << 15
Now it's evident why the numbers differ. The node generated for a directive is marked as a TypeDirective node, while the node generated for a component directive is additionally marked as Component.
View definition resolver
Since a-comp is now a component with the following simple template:
<span>I am A component</span>
the compiler produces a factory for it with its own view definition and view nodes:
function View_AComponent_0(_l) {
return jit_viewDef1(0, [
jit_elementDef2(0, null, null, 1, 'span', [], ...),
jit_textDef3(null, ['I am A component'])
Angular exists as a tree of views, so the parent view definition must reference child view definitions. These child view definitions are stored on the element nodes generated for components. In our case, the element definition node for a-comp will hold the view for a-comp. The jit_View_AComponent_04 parameter that the a-comp element node receives is a reference to the proxy class, which will resolve the factory that creates the view definition. Each view definition is created only once and then stored on the DEFINITION_CACHE. This view definition is subsequently used when Angular creates a view instance.
How Angular selects a renderer for a component
Depending on the ViewEncapsulation mode set in the component decorator, Angular picks between several DOM renderers:
The DomRendererFactory2 class is responsible for constructing the renderer. The definition passed into the factory — in this case the componentRendererType parameter with the value jit__object_Object_5 — describes what kind of renderer the component needs. Its key ingredients are the encapsulation mode and the styles that must be attached to the component’s view:
{
styles:[["h1[_ngcontent-%COMP%] {color: green}"]],
encapsulation:0
}
When you declare styles on a component, the compiler automatically assigns the ViewEncapsulation.Emulated mode. Alternatively, you can explicitly set the mode using the encapsulation property in the component decorator. If neither styles nor an explicit encapsulation mode are present, the descriptor still defaults to ViewEncapsulation.Emulated, but it is effectively ignored. Such a component falls back to the renderer of its parent component.
Directives applied to a component
One more question remains: what does the generated factory look like when a directive is attached to a component in a template, as in the following example?
<a-comp adir></a-comp>
As established earlier, when the factory for AComponent is generated, the compiler produces an element definition for the a-comp tag along with a directive definition for the AComponent class. Since a separate directive definition node is emitted for every directive in the template, the factory for the template above appears like this:
function View_AppComponent_0() {
return jit_viewDef2(0, [
jit_elementDef3(0, null, null, 2, 'a-comp', [], ...
jit_View_AComponent_04, jit__object_Object_5),
jit_directiveDef6(49152, null, 0, jit_AComponent7, [], ...)
jit_directiveDef6(16384, null, 0, jit_ADirective8, [], ...)
There is nothing new here — just an additional directive definition has been added, and the child count for the element has been bumped up to 2.
