A thorough guide to the world of Angular Attribute Directives

Directives are one of the most important and powerful features in Angular. They help structure and add styles to our elements. Either they add a view (UI) to our DOM(Component), change the appearance of our DOM(Attribute) or create/destroy the DOM (Structural).

In Angular, there are three kinds of Directives.

Components: These are directives with the @Component decorator. The Component decorator marks a class as an Angular component and provides configuration metadata that determines how the component should be processed, instantiated, and used at runtime. They are the subset of directives and must be associated with a template. Components are directives with a view.

Structural Directives: These restructure the DOM by adding or removing elements using browser APIs like:

  • document.createElement
  • document.createTextNode
  • document.removeChild
  • document.appendChild

Examples of built-in structural directives are NgFor, NgIf.

Attribute Directives: These directives affect the appearance of elements.

In this article, we dive deep into the inner workings of attributes directives. But, first, let’s explore how Angular internally represents an Angular app. If you are already grounded in it you can skip it and move over to the real deal.

Note: This article is based on the Renderer2 API of Angular. The Ivy version will be coming out soon.

Tip: When using a component-based framework like Angular, use tools like Bit (Github) to easily share, reuse and sync your components across projects- to build faster with your team. It’s free, give it a try.

Share reusable code components as a team · Bit

Angular Internal Representation

Before we see how it works underneath, we have to understand how Angular app is represented under the hood.

Angular transforms an app to some weird-looking code before it is run in the browser. Angular’s Components and NgModules are compiled to JS code called Factories(ComponentFactory/NgModuleFactory).

For example, let’s look at the following component:

@Component({
selector: 'app-root',
template: `<h2><b>I'm b tag inside h2 tag</b></h2>`
})
export class AppComponent {}

The Angular compiler will stamp out this:

import * as i1 from "@angular/core";
export function View_AppComponent_0(_l) {
return i1.ɵvid(0, [
(_l()(), i1.ɵeld(0, 0, null, null, 2, "h2", [], null, null, null, null, null)),
(_l()(), i1.ɵeld(2, 0, null, null, 1, "b", [], null, null, null, null, null)),
(_l()(), i1.ɵted(-1, null, [" I'm b tag inside h2 tag "])),
], null, null)
}
export function View_AppComponent_Host_0(_l) {
return i1.ɵvid(0, [
(_l()(), i1.ɵeld(0, 0, null, null, 2, "app-root", [], null, null, null, View_AppComponent_0, RenderType_AppComponent)),
i1.ɵdid(2, 49152, null, 0, i2.AppComponent, [], null, null)], null, null);
}
var AppComponentNgFactory = i1.ɵccf("app-root", i2.AppComponent, View_AppComponent_Host_0, {}, {}, []);

We see alot of of ɵ-suffixed calls and View_*_0 functions. This code was meant for machine to read an excute, not for humans. So what are all this i1.ɵvid, i1.ɵted, i1.ɵeld calls? Looking at the top the generated code we see:

import * as i1 from "@angular/core";

We see that the functions are from the Angular core library. The ɵ is used by the Angular team to indicate a function is private and cannot be used directly by the user. If we Ctrl-Click on any of these methods in VS Code, we will see their implementation in the Angular core module. We will see ɵvid references viewDef, ɵeld => elementDef, ɵted => textDef, ɵdid => directiveDef, and ɵccf => createComponentFactory.

With this information, we can translate our generated code to this:

import * as core from "@angular/core";
export function View_AppComponent_0(_l) {
return core.viewDef(0, [
(_l()(), core.elementDef(0, 0, null, null, 2, "h2", [], null, null, null, null, null)),
(_l()(), core.elementDef(2, 0, null, null, 1, "b", [], null, null, null, null, null)),
(_l()(), core.textDef(-1, null, [" I'm b tag inside h2 tag "])),
], null, null)
}
export function View_AppComponent_Host_0(_l) {
return core.viewDef(0, [
(_l()(), core.elementDef(0, 0, null, null, 2, "app-root", [], null, null, null, View_AppComponent_0, RenderType_AppComponent)),
core.directiveDef(2, 49152, null, 0, i2.AppComponent, [], null, null)], null, null);
}
var AppComponentNgFactory = core.createComponentFactory("app-root", i2.AppComponent, View_AppComponent_Host_0, {}, {}, []);

Now, now is readable. As we have transformed our code into a human form. Let’s look at what they do. Every Component in angular when compiled consist of three parts the component ngfactory AppComponentNgFactory, Host view View_AppComponent_Host_0 and the component view View_AppComponent_0.

AppComponentNgFactory is used to dynamically create components, View_AppComponent_Host_0 creates the host view of the component and View_AppComponent_0 contains the HTML structure of the component template, it creates the view we see on the browser. Our focus will be on the View_AppComponent_0 function cos it generates the view of the component. Like we said earlier, it holds the HTML structure of our template:

export function View_AppComponent_0(_l) {
return core.viewDef(0, [
/** start view creation */
(_l()(), core.elementDef(0, 0, null, null, 2, "h2", [], null, null, null, null, null)),
(_l()(), core.elementDef(2, 0, null, null, 1, "b", [], null, null, null, null, null)),
(_l()(), core.textDef(-1, null, [" I'm b tag inside h2 tag "])),
/** end view creation */
],
/** start change detection */
null, null
/** end change detection */
)
}
|
v
<h2><b>I'm b tag inside h2 tag</b></h2> 

We can easily see that elementDef maps to HTML tags and textDef maps to Text nodes.

Also, we can see View_AppComponent_0 function comprises two parts view creation part and change detection part. The view creation part runs once when the component is bootstrapped by Angular, here the component's HTML view is created and appended to the DOM. The change detection part runs when Angular performs its change detection cycle.

The viewDef function takes all of its parameters and generates a ViewDefinition object. Looking at it, we see that it takes the view creation part in an array which comprises the elementDef and textDef functions. These functions generate a NodeDef object. The NodeDef object in Angular denotes the kind of node to be created.

export interface NodeDef {
flags: NodeFlags;
// Index of the node in view data and view definition (those are the same)
nodeIndex: number;
// Index of the node in the check functions
// Differ from nodeIndex when nodes are added or removed at runtime (ie after compilation)
checkIndex: number;
parent: NodeDef|null;
...
}

The flags property hold info about the type of node to be created:

export const enum NodeFlags {
None = 0,
TypeElement = 1 << 0,
TypeText = 1 << 1,
...
}

Attribute @Directive: Behind The Scenes

We have seen in detail how Angular represents an Angular app. With this knowledge, we look into our attribute directive project.

Let’s start by creating a custom directive that a changes an element’s background-color.

@Directive({
selector: '[highLight]'
})
class HighLight {
constructor(private el: ElementRef){}
ngOnInit() {
this.el.nativeElement.style.backgroundColor = 'red'
}
}

We can apply the directive selector highLight on any element in our component template, like this:

@Component({
selector: 'app-root',
template: `
➥ <h2 highLight>This a H2 header</h2>
`
})
class AppComponent {}

We applied the highLight directive selector to the h2 element as an attribute.

If we run our app:

ng serve

The HighLight directive will run and set the background of the h2 element to red.

On compilation, our app will be transformed to:

import * as i1 from "@angular/core";
import * as i2 from "./app.component";
export function View_AppComponent_0(_l) {
return i1.ɵvid(0, [
(_l()(), i1.ɵeld(0, 0, null, null, 2, "h2", [
["highLight", ""]
], null, null, null, null, null)),
i1.ɵdid(1, 81920, null, 0, i2.HighLight, [i1.ElementRef], null, null),
(_l()(), i1.ɵted(-1, null, ["This a H2 header"]))
],
function(_ck, _v) { _ck(_v, 1, 0); }, null);
}
export function View_AppComponent_Host_0(_l) {
return i1.ɵvid(0, [
(_l()(), i1.ɵeld(0, 0, null, null, 2, "app-root", [], null, null, null, View_AppComponent_0, RenderType_AppComponent)),
i1.ɵdid(1, 49152, null, 0, i2.AppComponent, [], null, null)], null, null);
}
var AppComponentNgFactory = i1.ɵccf("app-root", i2.AppComponent, View_AppComponent_Host_0, {}, {}, []);

Like we saw earlier, View_*_0 holds the HTML structure of he component's template. Here, View_AppComponent_0 generates elementDef for the h2 and textDef for the "This a H2 header" text node. If you noticed the compiler added a new ɵdid node which via Ctrl-Click refers to directiveDef. This function creates the instance of the directive class.

Let’s look at the nodes.

(_l()(), i1.ɵeld(0, 0, null, null, 2, "h2", [
["highLight", ""]
], null, null, null, null, null)),

This generates a NodeDef for the h2 element and returns it, like this:

{
// will bet set by the view definition
nodeIndex: -1,
parent: null,
renderParent: null,
bindingIndex: -1,
outputIndex: -1,
// regular values
flags: 0,
checkIndex: -1,
childFlags: 0,
...
childCount: 2,
bindings: [],
bindingFlags: 0,
outputs: [],
element: {
ns: null,
name: "h2",
attrs: ["highLight", ""],
template: null,
...
handleEvent: null
},
...
};

You see this contains vital information on how to create the h2 element. The name of the element element.name, its attributes element.attrs, event listeners bound to the element output element.handleEvent, the number of the child nodes childCount, the element's parent, parent renderParent, its bindings, bindings.

The next node is:

i1.ɵdid(1, 81920, null, 0, i2.HighLight, [i1.ElementRef], null, null),

See that it is passed our HighLight directive class and it dependency ElementRef. It will returns this NodeDef object:

{
// will bet set by the view definition
nodeIndex: -1,
parent: null,
renderParent: null,
bindingIndex: -1,
outputIndex: -1,
// regular values
checkIndex: 1,
flags: 81920,
childFlags: 0,
...
childCount: 0,
bindings: [],
...
outputs: null,
element: null,
provider: {
token: "HighLight",
value: i1.HighLight,
deps: ["ElementRef"]
},
...
};

The important property here is the provider property. It provides info to the framework when instantiating the HighLight class. It has a value key that holds the class and the deps holds the class dependencies. In our case, our class HighLight takes the ElementRef class in its constructor as a parameter. So the deps becomes [“ElementRef”], to be resolved during execution.

The last node is the textDef:

(_l()(), i1.ɵted(-1, null, ["This a H2 header"]))

Remember, we said earlier it creates text nodes. It will return a NodeDef object like this:

{
// will bet set by the view definition
nodeIndex: -1,
parent: null,
renderParent: null,
bindingIndex: -1,
outputIndex: -1,
// regular values
checkIndex: -1,
flags: NodeFlags.TypeText,
childFlags: 0,
...
ngContentIndex: null,
childCount: 0,
bindings: [
{
flags: BindingFlags.TypeProperty,
name: null,
ns: null,
nonMinifiedName: null,
securityContext: null,
suffix: "This a H2 header",
}
],
bindingFlags: BindingFlags.TypeProperty,
outputs: [],
element: null,
provider: null,
text: {
prefix: "This a H2 header"
},
...
};

During the creation of this NodeDef, Angular will create the text node from the text.prefix property. The bindings are used to tell Angular dependencies it should update during change detection. The flags property is set to TypeText to tell Angular this is a text node during the creation of the nodes.

Next thing, we will look at is the value of updateDirectives param in the viewDef inside the View_AppComponent_0 function.

function(_ck, _v) { _ck(_v, 1, 0); }, null);

It takes two params _ck and _v. _ck references the prodCheckAndUpdate and _v is the component’s view data. This param is called when a change detection cycle is triggered. It updates the bindings in the directive’s class and renders the changed value if any.

NB: When an Angular app, is run. Two bootstrap phases are executed: view creation phase and change detection phase. The view creation phase creates and appends the views on the DOM. The change detection phase updates the views' bindings.

DirectiveDef

This is where all the magic happens. Let’s look at our component factory again.

export function View_AppComponent_0(_l) {
return i1.ɵvid(0, [
(_l()(), i1.ɵeld(0, 0, null, null, 2, "h2", [
["highLight", ""]
], null, null, null, null, null)),
i1.ɵdid(1, 81920, null, 0, i2.HighLight, [i1.ElementRef], null, null),
(_l()(), i1.ɵted(-1, null, ["This a H2 header"]))
],
function(_ck, _v) { _ck(_v, 1, 0); }, null);
}

We have looked at i1.ɵdid before. Here, let’s peer more closely. We saw earlier the NodeDef object it generates. This object is used to create the instance of our HighLight class passing along its dependency ElementRef. Our HighLight class takes ElementRef as a parameter in it constructor. It uses the ElementRef class to get the HTMLElement instance of its parent/host element h2, so it can change its background using HTMLElement's style.backgroundColor property assignment:

(new HTMLElement('h2'))style.backgroundColor = 'red'

ElementRef is a class in the Angular core library.

export class ElementRef<T = any> {
public nativeElement: T;
  constructor(nativeElement: T) { this.nativeElement = nativeElement; }
}

It takes nativeElement as an arg in its constructor. This nativeElement holds the HTMLElement reference it’s associated with.

When our HighLight instance is created, the ElementRef class instance is injected.

export function createDirectiveInstance(view: ViewData, def: NodeDef): any {
// components can see other private services, other directives can't.
const allowPrivateServices = (def.flags & NodeFlags.Component) > 0;
// directives are always eager and classes!
➥const instance = createClass(
view, def.parent !, allowPrivateServices, def.provider !.value, def.provider !.deps);
...
return instance
}

This function creates a directive instance. See it calls a function createClass:

function createClass(
view: ViewData, elDef: NodeDef, allowPrivateServices: boolean, ctor: any, deps: DepDef[]): any {
const len = deps.length;
switch (len) {
case 0:
return new ctor();
case 1:
➥ return new ctor(resolveDep(view, elDef, allowPrivateServices, deps[0]));
...
}
}

You see it creates the instance of the passed in class using the new keyword. The resolveDep resolves and returns the class dependency.

export function resolveDep(
view: ViewData, elDef: NodeDef, allowPrivateServices: boolean, depDef: DepDef,
notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any {
...
while (searchView) {
if (elDef) {
switch (tokenKey) {
...
case ElementRefTokenKey:
➥ return new ElementRef(asElementData(searchView, elDef.nodeIndex).renderElement);
...
}

The asElementData accesses the index of the NodeData in the view’s node array. Here the index of our h2 element NodeDef is passed in so it would return its NodeData (This was generated when the h2 element was created). Here, its HTMLElement instance is referenced by the renderElement key in it NodeData.

In summary of what happened above. Our class HighLight uses ElementRef to change the style color of its host element. Also, the ElemnetRef class accepts the HTMLElement of an element so it could access HTMLElements methods and properties in order to change its DOM appearance and behavior. In that case, the HTMLElement of our directive’s host element h2 had to be supplied. Remember when we saw the NodeDef objects gen by the functions. If you noticed there is always this parent property. This points to the parent of an element. So here it will point to the h2 element NodeDef object. During view creation via createViewNodes functions, the HTMLElement created of a NodeFlags.TypeElement node is stored in the renderElement property of the NodeData generated. So when the ElementRef dependency is resolved, the h2 element via def.parent.view.nodes[index].renderElement is passed to ElementRef class.

So, in the end, we have something like this:

const instance = new HighLight(new ElementRef(new HTMLElement('h2')))

updateDirectives

Directives functionality are executed in this phase change detection. As the name implies, it runs updates on the properties of directives if any is bound on the template.

Looking at the value of the updateDirectives param in our factory:

function(_ck, _v) { _ck(_v, 1, 0); }, null);

The _ck refers to the prodCheckAndUpdateNode function and _v is the handle to our view’s data. The prodCheckAndUpdateNode _ck function is called. We passed in _v the view host of the directive, 1 the directive index in the view _v

i1.ɵdid(➥ 1, 81920, null, 0, i2.HighLight, [i1.ElementRef], null, null),

and 0, the type of update to perform.

function prodCheckAndUpdateNode(
view: ViewData, checkIndex: number, argStyle: ArgumentType, v0?: any, v1?: any, v2?: any,
v3?: any, v4?: any, v5?: any, v6?: any, v7?: any, v8?: any, v9?: any): any {
const nodeDef = view.def.nodes[checkIndex];
checkAndUpdateNode(view, nodeDef, argStyle, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9);
return (nodeDef.flags & NodeFlags.CatPureExpression) ?
asPureExpressionData(view, checkIndex).value :
undefined;
}

prodCheckAndUpdate gets the instance of our HighLight from the view.def.nodes array using the checkIndex param. Then, calls the checkAndUpdateNode with the params. After a series of calls from checkAndUpdateNode, it lands at checkAndUpdateNodeInline.

function checkAndUpdateNodeInline(
view: ViewData, nodeDef: NodeDef, v0?: any, v1?: any, v2?: any, v3?: any, v4?: any, v5?: any,
v6?: any, v7?: any, v8?: any, v9?: any): boolean {
switch (nodeDef.flags & NodeFlags.Types) {
case NodeFlags.TypeElement:
return checkAndUpdateElementInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9);
case NodeFlags.TypeText:
return checkAndUpdateTextInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9);
➥ case NodeFlags.TypeDirective:
return checkAndUpdateDirectiveInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9);
case NodeFlags.TypePureArray:
case NodeFlags.TypePureObject:
case NodeFlags.TypePurePipe:
return checkAndUpdatePureExpressionInline(
view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9);
default:
throw 'unreachable';
}
}

Here, the NodeFlags.TypeDirective case is passed because we are running update on a directive. Remember, our directive NodeDef object has 81920 in its flags property which sets the NodeFlags.Directive bitmask. The checkAndUpdateDirectiveInline function is called. As the name indicates it checks and update a directive. checks in the sense that the bound properties on the directive are checked for changes if any, an update is run on the bound properties.

export function checkAndUpdateDirectiveInline(
view: ViewData, def: NodeDef, v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, v6: any,
v7: any, v8: any, v9: any): boolean {
const providerData = asProviderData(view, def.nodeIndex);
1.➥const directive = providerData.instance;
let changed = false;
let changes: SimpleChanges = undefined !;
const bindLen = def.bindings.length;
X.➥if (bindLen > 0 && checkBinding(view, def, 0, v0)) {
changed = true;
changes = updateProp(view, providerData, def, 0, v0, changes);
}
X.➥...
if (changes) {
2.➥ directive.ngOnChanges(changes);
}
if ((def.flags & NodeFlags.OnInit) &&
shouldCallLifecycleInitHook(view, ViewState.InitState_CallingOnInit, def.nodeIndex)) {
3.➥ directive.ngOnInit();
}
if (def.flags & NodeFlags.DoCheck) {
4.➥ directive.ngDoCheck();
}
return changed;
}

The directive instance is retrieved 1.. Next, the bindings on the directives are checked, if any changes detected. It updates the directive property with the changes X.. The ngOnChanges hook 2. is run, if defined on the directive class passing in the changes. It calls ngOnInit hook 3. if its the first time the directive is checked and if the hook is defined on the directive class.

Remember, we implemented the OnInit interface and defined the ngOnInit method in our HighLight class:

export class HighLight implements OnInit{
constructor(private el: ElementRef){}
➥ ngOnInit() {
this.el.nativeElement.style.backgroundColor = 'red'
}
}

Here, our ngOnInit method is run. This sets the h2 element's background color to red using ElementRef instance el. Now, in our browser, the h2 This a H2 header turns to red.

// pic here

Next, ngDoCheck hook 4. is called and the changes are returned.

Attribute @Directives: Other Use Cases

Currently, the HighLight directive simply sets an element color. There are many more ways by which attribute directives could be used. It could be made more dynamic by adding events and by data-binding a property to the class.

Adding events to directives is achieved through the @HostListener decorator and data-binding a directive’s property is made through the @Input decorator.

Using @HostListener decorator

Let’s begin with @HostListener, we like said before. It adds events to directives, to add events to Components you use () and EventEmitters. With the @HostListener, we can add different events like mouseenter, mouseleave, click, keyup, keydown etc to the directive. Since directives are children of its host element, the events are bubbled and applied to the host element.

Let’s say we redefine our HighLight directive to add a mouseenter event. That is, it will change the h2 element to red when the mouse cursor is dragged over the h2 element in the browser.

@Directive({
selector: '[highLight]'
})
export class HighLight implements OnInit{
constructor(private el: ElementRef){}
      @HostListener('mouseenter') onMouseEnter() {
this.el.nativeElement.style.backgroundColor = 'blue'
}
    ngOnInit() {
this.el.nativeElement.style.backgroundColor = 'red'
}
}

It turns to red on initial load but when the mouse is moved over the h2 element, it turns the h2 element background to blue.

The factory will look like this:

export function View_AppComponent_0(_l) {
return i1.ɵvid(0, [
(_l()(), i1.ɵeld(0, 0, null, null, 2, "h2", [
["highLight", ""]
], null,
➥ [
[null, "mouseenter"]
],
➥ function(_v, en, $event) {
var ad = true;
if (("mouseenter" === en)) {
var pd_0 = (i1.ɵnov(_v, 1).onMouseEnter() !== false);
ad = (pd_0 && ad);
}
return ad;
}, null, null)),
i1.ɵdid(1, 81920, null, 0, i2.HighLight, [i1.ElementRef], null, null),
(_l()(), i1.ɵted(-1, null, ["This a H2 header"]))
], function(_ck, _v) { _ck(_v, 1, 0); }, null);
}

New things were added in the element definition i1.ɵeld(…) function, an array and a function param — by looking at the docs(elementDef) , we see that its outpus and handleEvent params. They are used for event propagation.

The output array holds events to be registered by the element, and the handleEvent function will be called when any of the events in the outputs array is fired. So the events name in the outputs array will be registered against the handleEvent function.

It will be like this:

function handleEvent(_v, en, $event) {
var ad = true;
if (("mouseenter" === en)) {
var pd_0 = (i1.ɵnov(_v, 1).onMouseEnter() !== false);
ad = (pd_0 && ad);
}
return ad;
}
var events = ["mouseenter", "click"]
var element = new HTMLElement('h2')
for(evt in events) {
element.addEventListener(evt, handleEvent)
}

Whenever an event is fired from the h2 element, the handleEvent function is called.

During the creation of nodes, in the createViewNodes function. events of the NodeDefs of type TypeElement is registered via listenToElementOutputs function.

function createViewNodes(view: ViewData) {
...
switch (nodeDef.flags & NodeFlags.Types) {
case NodeFlags.TypeElement:
const el = createElement(view, renderHost, nodeDef) as any;
let componentView: ViewData = undefined !;
if (nodeDef.flags & NodeFlags.ComponentView) {
const compViewDef = resolveDefinition(nodeDef.element !.componentView !);
componentView = Services.createComponentView(view, nodeDef, compViewDef, el);
}
➥ listenToElementOutputs(view, componentView, nodeDef, el);
...
}

The listenToElementOutputs function does what we did above:

export function listenToElementOutputs(view: ViewData, compView: ViewData, def: NodeDef, el: any) {
1.➥for (let i = 0; i < def.outputs.length; i++) {
const output = def.outputs[i];
const handleEventClosure = renderEventHandlerClosure(
view, def.nodeIndex, elementEventFullName(output.target, output.eventName));
let listenTarget: 'window'|'document'|'body'|'component'|null = output.target;
let listenerView = view;
if (output.target === 'component') {
listenTarget = null;
listenerView = compView;
}
const disposable =
2.➥ <any>listenerView.renderer.listen(listenTarget || el, output.eventName, handleEventClosure);
view.disposables ![def.outputIndex + i] = disposable;
}
}
function renderEventHandlerClosure(view: ViewData, index: number, eventName: string) {
return (event: any) => dispatchEvent(view, index, eventName, event);
}

It loops through the outputs array 1. and registers each event with handleEvent 2.. This handleEvent function is run whenever an event is triggered.

Note something, Angular uses Zone.js to run change detection, to put it correctly, to know when to run change detection. Async operations like setTimeout, events DOM(click, mouseneter, etc) etc are basically used to change data in our apps. Zone.js monkey-patched these functions so Angular could catch when an async op is executed and trigger a change detection cycle.

Looking into this function handleEvent we see that it has an if- check on mouseenter. Whenever the $event equals mouseenter, the onMouseenter method defined in our HighLight class is called. The i1.ɵnov(_v, 1) refers to nodeValue(...) function. This returns the value of a node, if its an element it returns its HTMLElement instance, if a text node returns its Text node, if a pipe or directive, returns its instance. so in our case, it returns the instance of our HighLight class. So we could call the onMouseEnter method defined in it.

export class HighLight implements OnInit{
constructor(private el: ElementRef){}
➥    @HostListener('mouseenter') onMouseEnter() {
this.el.nativeElement.style.backgroundColor = 'blue'
}
    ngOnInit() {
this.el.nativeElement.style.backgroundColor = 'red'
}
}

This changes the background color of our h2 element from red to blue.

Using @Input decorator

We hard-coded the colors in the HighLight class, we could make it flexible by passing colors into the HighLight class. Let’s redefine our app, so we could pass data to our HighLight directive from our AppComponent.

import { OnChanges,SimpleChanges,Input, HostListener,Component, Directive, ElementRef, OnInit } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<h2 highLight [color]="boundColor">This a H2 header</h2>
    <button (click)="changeToGreen()">ChangeToGreen</button>
<button (click)="changeToYellow()">ChangeToYellow</button>
`,
})
export class AppComponent {
constructor(){}
boundColor = '';
changeToGreen() {
this.boundColor = 'green'
}
changeToYellow(){
this.boundColor = 'yellow'
}
}
@Directive({
selector: '[highLight]'
})
export class HighLight implements /*OnInit,*/ OnChanges{
@Input() color;
constructor(private el: ElementRef){}
      @HostListener('mouseenter') onMouseEnter() {
this.el.nativeElement.style.backgroundColor = 'blue'
}
    /*ngOnInit() {
this.el.nativeElement.style.backgroundColor = 'red'
}*/
ngOnChanges(changes: SimpleChanges) {
//Called before any other lifecycle hook. Use it to inject dependencies, but avoid any serious work here.
//Add 'implements OnChanges' to the class.
this.el.nativeElement.style.backgroundColor = this.color
}
}

What did we do here? We bound the color property of HighLight directive to boundColor property of AppComponent. It means that whenever the boundColor property of AppComponent the value is passed to the HighLight class through its color property, so it holds the value of boundColor. This is called property binding. To bind a property in a Directive/Component, the square braces [] is used.

<h2 highLight [color]="boundColor">This a H2 header</h2> 

The Input decorator marks a property as the means of communication of data from the parent. That’s the reason we marked the color property with @Input().

@Input() color; 

When implemented the OnChanges interface and defined the ngOnChanges method in our directive class. This method is run when the value of its property color changes. It will change the h2 element background to the value of the color property.

So in order to change the color property, we added two buttons ChangeToGreen and ChangeToYellow in the AppComponent template, when clicked they call the changeToGreen() and changeToYellow() methods respectively. These methods change the value of the boundColor property of AppComponent to green and yellow respectively.

You see this makes our directive dynamic, the color is no longer hard-coded into it.

On compilation, we will have this:

export function View_AppComponent_0(_l) {
return i1.ɵvid(0, [
(_l()(), i1.ɵeld(0, 0, null, null, 2, "h2", [
["highLight", ""]
], null, [
[null, "mouseenter"]
], function(_v, en, $event) {
var ad = true;
if (("mouseenter" === en)) {
var pd_0 = (i1.ɵnov(_v, 1).onMouseEnter() !== false);
ad = (pd_0 && ad);
}
return ad;
}, null, null)),
i1.ɵdid(1, 540672, null, 0, i2.HighLight, [i1.ElementRef],
I.➥ {
color: [0, "color"]
}, null),
(_l()(), i1.ɵted(-1, null, ["This a H2 header"])),
II.➥ (_l()(), i1.ɵeld(3, 0, null, null, 1, "button", [], null, [
[null, "click"]
], function(_v, en, $event) {
var ad = true;
var _co = _v.component;
if (("click" === en)) {
var pd_0 = (_co.changeToGreen() !== false);
ad = (pd_0 && ad);
}
return ad;
}, null, null)),
(_l()(), i1.ɵted(-1, null, ["ChangeToGreen"])),
III.➥ (_l()(), i1.ɵeld(5, 0, null, null, 1, "button", [], null, [
[null, "click"]
], function(_v, en, $event) {
var ad = true;
var _co = _v.component;
if (("click" === en)) {
var pd_0 = (_co.changeToYellow() !== false);
ad = (pd_0 && ad);
}
return ad;
}, null, null)),
(_l()(), i1.ɵted(-1, null, ["ChangeToYellow"]))
IV.➥ ], function(_ck, _v) {
var _co = _v.component;
V.➥ var currVal_0 = _co.boundColor;
_ck(_v, 1, 0, currVal_0);
}, null);
}

Things changed here. Let’s point them out. In our directive definition i1.ɵdid we have the bindings object I. declared. This object holds property(-ies) of directives that are bound to its host parent. In our case, it contains color because we had @Input on the HighLight class color property. Bindings are used for DOM and bound properties update. So the color binding will be updated whenever updates on our directive are run.

Remember, we have seen and looked into the updateDirectives param. It’s used to perform/run updates on directives when a change detection cycle is triggered. Looking at the value of the updateDirectives IV. in our ViewDefinition. We see that it gets the current value of boundColor V. from the view and runs a check and update node check on our HighLight directive at index 1.

Also, remember we said that change detection is triggered by Zone.js when an async event is caught. Good, we have click events II., III. registered in our buttons. The handleEvents functions when the buttons are pressed. See they get the instance of the AppComponent _co and call their attached functions _co.changeToYellow(), _co.changeToGreen(). These functions change the boundColor property to a new value, either to green or to yellow. To reflect the changes, change detection runs the updateDirectives function param IV..

We saw what the _ck is and its function earlier. This time it takes a value currVal_0 it needs to update on the directive at index 1.

export function checkAndUpdateDirectiveInline(
view: ViewData, def: NodeDef, v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, v6: any,
v7: any, v8: any, v9: any): boolean {
const providerData = asProviderData(view, def.nodeIndex);
const directive = providerData.instance;
let changed = false;
let changes: SimpleChanges = undefined !;
const bindLen = def.bindings.length;
X.➥if (bindLen > 0 && checkBinding(view, def, 0, v0)) {
changed = true;
changes = updateProp(view, providerData, def, 0, v0, changes);
}
X.➥...
if (changes) {
directive.ngOnChanges(changes);
}
if ((def.flags & NodeFlags.OnInit) &&
shouldCallLifecycleInitHook(view, ViewState.InitState_CallingOnInit, def.nodeIndex)) {
directive.ngOnInit();
}
if (def.flags & NodeFlags.DoCheck) {
directive.ngDoCheck();
}
return changed;
}

This time the updateProp is run to update the color property with the current value of its bound property on AppComponent boundColor. Also, the ngOnChanges hook is run. Remember, we defined the ngOnChanges method on our HighLight directive class. Here, it is run which sets our h2 element background color to the value of the color property.

Wheee!! That was heavy.

There are so many other use cases you can come up with. We did a few above so as to get you started and serve as your guide.

Conclusion

You see there is no magic in Angular. Everything is JS and brilliance.

We have seen that whenever an attribute directive is attached to an element.

core.elementRef("ELEMENT_NAME")
core.directiveDef("DIRECTIVE_CLASS")

The above code must be generated by the compiler. It will create an element definition for the host element and a directive definition for the directive’s class.

I think with this, we know what happens under the hood whenever we create and apply an attribute directive.

If you have any question regarding any concept or code in this article, feel free to comment, email or DM me

Next thing, to follow and understand the call of functions in Angular, the browser debugger is a very good tool. It helped me understand what happens in runtime after going through the Angular sources. The step-over, step-in, step-in buttons are very useful for navigation and the Call Stack tool comes handy when you want to track the flow of functions already executed.

Let the debugger be your guide.

Thanks !!!

Learn More


In-Depth Look at the Inner Workings of Angular Attribute Directives was originally published in Bits and Pieces on Medium, where people are continuing the conversation by highlighting and responding to this story.