Log In

Angular components communication and lifecycle hooks


by Yariv Katz

Angular architecture is now component based. We wrap our entire app with a component that usually called AppComponent and that component is hosting other components which creates an angular app made out of tree of components. Having a tree of components means we will have to transfer information between them. In this article we will talk about different ways to transfer information between components in our tree.

Bootstrapping app and components

To start our communication experiment we will first need to start a new angualr application. We are using angular 7 and we will use the angular cli to start our application. The best way to do that is not to install @angular/cli as a global package but rather use npx to install the latest and start our project. In your terminal type the following:

> npx @angular/cli new component-comm-tutorial

Our app already has a root component app.component.ts. We will create a child component, and communicate between the child component and the root component. We will use angular cli to create our child component. In the terminal type:

> cd component-comm-tutorial
> ng g c Child
> ng serve

Now lets place out child component inside our root component. Modify the template file app.component.html:

<app-child></app-child>

Input

First way to communicate is by passing properties from the parent component to the child component. Lets transfer a message string from the parent and print it in the child, and also transfer an object with another message. In the app.component.html add this at the bottom:

<app-child message="hello from parent" [messageObj]="{msg: 'message in obj'}"></app-child>

Notice that if we are transfering a constant string we don't have to wrap the property in square brackets. The expressions we do pass in square brackets are expressions transfered one way from the class to the template, or defined in the template, those expression needs to run fast and they should not alter properties in the components so we can not preform assignments or increment properties. Now in the child component we need to specify that we are getting properties through the message property and through the messageObj property.

import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core';

@Component({
    selector: 'app-child',
    templateUrl: './child.component.html',
    styleUrls: ['./child.component.scss']
})
export class ChildComponent implements OnInit, OnChanges{
    @Input() message: string;
    @Input('messageObj') obj: {msg : string};

    ngOnInit() {
        console.log('In this lifecycle hook the input properties will be populated');
    }

    ngOnChanges(changes: SimpleChanges): void {
        console.log('when the properties change this lifecycle hook will be called');
    }

}

We decorated the properties we are getting from the parent with the @Input decorator. If the name of the property match the attribute property, we can leave the decorator with no arguments. If the name of the property is different then the name of the attribute we need to pass a string as an argument to the decorator and specify the attribute name. In the ngOnInit lifecycle hook we are gurenteed that the input properties will contain their value. The ngOnChanges hook will be called every time the input change. Now modify the child component template to print the string messages we got. Modify the child.component.html to this:

<h4>
    {{message}}
</h4>

<h4>
    {{obj?.msg}}
</h4>

Another way to pass data from class to template is by using double curly braces. The same rules of expressions without sideeffects apply here as well. Notice that when the components is initiated first the ngOnChanges hook is called and only after that the ngOnInit is called. ngOnInit will be called once while ngOnChanges will be called every time the properties change.

ngOnChanges

I want to examine what exactly consititue as change in the properties. We have the log message printed in the child when the OnChanges event is called. I want to change the parent and transfer the object from the parent to the child and after a second i want to change that property. Modify the app.component.ts:

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit{
    messageObj = {msg: 'message in obj'}

    ngOnInit() {
        setTimeout(() => {
        // will this cause the OnChanges hook in the child to be called?
        this.messageObj.msg = 'changed message';
        });
    }
}

In the parent component, we are changing the msg property in our object. This means our pointer is still on the same object, we are simply changing a property inside that object. Now modify the parent template app.component.html to pass the object as the object property in the child.

<app-child message="hello from parent" [messageObj]="messageObj"></app-child>

We are tranfering the object that change after 1 second to the messageObj of the child and we will examine if the lifecycle hook OnChanges is called. The result is kind of weird cause we do see the changed message printed in the child, which means the child gets the change, but if we examine how much time the OnChanges hook was called we will see it was called once. the setTimeout do cause a change detection which means that angular will rerender the changes on the app component and all its children, but the OnChanges hook will only be called if the reference is changed. That hook will also contain the properties that changed.

DoCheck

This lifecycle hook we will implement usually on components with OnPush strategy. This hook is called on every change detection of my parent (my parent will rerender). In this hook we can manually set our component to be rerendered by calling markForCheck In the previous example lets see what happens when we set the child component change detection strategy to OnPush. Modify the child.component.ts component decorator:

@Component({
    selector: 'app-child',
    templateUrl: './child.component.html',
    styleUrls: ['./child.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush
})

So before even if the OnChanges was not called, because we kept the same pointer on the parent, still the child was updated with the changes cause the timer cause a change detection from parent to child. Now we set the child to change detection strategy onpush which means that the component will not re render if the input properties are not changed by reference. Which means that even though the parent changed the object the change is not reflected in the child. This is the common usage of the DoCheck hook where we can state a change and rerender ourselves. The hook will be called even though the child did not re render, in it we can compare the msg value of the property and cause a manuall re render if it's changed. Modify the child.component.ts to add the DoCheck hook:

import { Component, Input, KeyValueDiffers ,OnInit, OnChanges, SimpleChanges, DoCheck, ChangeDetectionStrategy, KeyValueDiffer, ChangeDetectorRef } from '@angular/core';

@Component({
    selector: 'app-child',
    templateUrl: './child.component.html',
    styleUrls: ['./child.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent implements DoCheck, OnInit{
    @Input() message: string;
    @Input('messageObj') obj: {msg : string};
    private _objDiffer: KeyValueDiffer<any, any>

    constructor(
        private _differs: KeyValueDiffers,
        private _cd: ChangeDetectorRef
    ) {}

    ngOnInit() {
        this._objDiffer = this._differs.find(this.obj).create();
    }

    ngDoCheck(): void {
        const isChange = this._objDiffer.diff(this.obj);
        if (isChange) {
            this._cd.markForCheck();
        }
    }  
}

In order to track changes on an object, we will need to create a differ on the object. Differ will hold changes on an object. We create a differ on the input object. We then can verify in the ngDoCheck if there is a change on the object and if so mark the component and children as need to be re rendered.

Output

Let's go over another form of communication which is data passed from the child to the parent through event. To define an event in the child and connect to it in the parent you have to do the following steps:
1. define a property in the child, the property type is EventEmitter, you need to also create instance of that property. This property will define the stream of events, and it has a generic type of the data we are sending in the event. Whenever the even happens we need to call the emit on the property.
2. Decorate the property we create in 1 with the @Output decorator. that decorator gets an optional string argument which determined the name of the attribute by which we can connect to this event. If left empty, the name of the attribute is identical to the name of the property.
3. When placing the component in the parent template, we need to connect to the event with brackets. The $event will be equal to what the child is sending.
To demonstrate lets place a button in the child component, and whenever we press the button we send an event to the parent with an hello from child message. Modify the file child.component.ts like so:

import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
    selector: 'app-child',
    templateUrl: './child.component.html',
    styleUrls: ['./child.component.scss'],
})
export class ChildComponent {
    @Input() message: string;
    @Input('messageObj') obj: {msg : string};
    @Output('buttonClick') messageEvent: EventEmitter<string> = new EventEmitter();

    /**
    * when the user click the button to send message 
    * to parent
    */
    buttonClicked = () => {
        this.messageEvent.emit('hello from child');
    }
}

In this code we created the first and second step of creating an output, we created an EvenEmitter property and decorated with the @Output decorator. Now in the child component template let's add a button that will call the method buttonClicked. Modify the child.component.html and add this to the buttom of the template:

<button (click)="buttonClicked()">Send message to parent</button>

Now to achieve number 3 where we connect to the event we created to the parent we will have to modify the parent template. Modify the file app.component.html add this to the buttom:

<h2>
    Output
</h2>

<app-child (buttonClick)="title = $event"></app-child>

{{title}}

we connect to the event based on the name we placed in the Output decorator. In the event we are getting the message we send in the emit on the $event and assigning it to a title property where we print that property.

Service

Another way to communicate between components is by using a service. With a service we can communicate between any component, not necesarly parent and child. The way we do it is define a Subject (or other type whose parent is Subject) and call next, complete, error to specify something happened to the observable and pass the next value or an error value. Lets use the angualr cli to create a new service.

> ng g s Comm

We will define a subject in our service. Modify the service comm.service.ts

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable({
    providedIn: 'root'
})
export class CommService {
    sendMessage: Subject<string> = new Subject();
}

In our service we defined a subject that will be used to transfer string data between components. Now lets use that service to transfer data from the child to the parent. In the child component, we will request the service from the DI and in the buttonClicked event we will use the subject to transfer a message from the child. child.component.ts

constructor(private _commService: CommService) {}

/**
* when the user click the button to send message 
* to parent
*/
buttonClicked = () => {
    this.messageEvent.emit('hello from child');
    this._commService.sendMessage.next('message from child through service');
}

We are calling the next on the subject in our service. Now in the parent class we will also inject the service and subscribe to listen to that event. Modify the app.component.ts

export class AppComponent implements OnInit{
    messageObj = {msg: 'message in obj'}
    messageFromService$: Observable<string>

    constructor(private _commService: CommService) {}

    ngOnInit() {
        this.messageFromService$ = this._commService.sendMessage;
    }
}

We are grabbing the service and saving the subject to a property in our class. Notice that we don't even need to subscribe, just grab the observable and we will subscribe through the template which will be safer since angular will make sure to also unsubscribe from it when destroying the component. Modify the app.component.html and add the following to the bottom:

<h2>
  Service
</h2>

{{ messageFromService$ | async}}

We are using the async pipe to subscribe to the observable and grab the string data from the observable.

Communication with Templete Reference Variable

Another form of communication between parent to child is by using Template reference variable (TRV). with TRV we can grab an element from the template and place it in a variable whose scope is the template. If attaching this variable to a regular dom element like div or span, the TRV will contain the instance of the DOM element. If attaching TRV to an angular component / directive, it will get the instance of the class of the component or directive. Lets create a method in the child which returns a string message from the child, then in the parent we will create a TRV on the child and call that method. Modify the child.component.ts and add the following method to the class.

transferMessage = () => {
    return 'this is passed with TRV';
}

Now in the template of the app component we will grab with TRV the instance of the child and call this method. Modify the file app.component.html and add this to the bottom of the template.

<h2>
    TRV
</h2>

<app-child #child></app-child>

{{child.transferMessage()}}

We attach the TRV with #variableName and then we can call the public methods of that instance.

@ViewChild

Another form of communication is with the @ViewChild decorator. With this decorator we can grab element from the template by specifying type, or by specifying the string of TRV we want to grab. If specifying type it will grab the first child in the template that match the type we placed. We can also use the decorator @ViewChildren to grab all the instances of type. In the parent component app.component.ts let's grab the TRV we placed before. Modify app.component.ts

export class AppComponent implements OnInit, AfterViewChecked, AfterViewInit{
    messageObj = {msg: 'message in obj'}
    messageFromService$: Observable<string>
    @ViewChild('child') childInstance: ChildComponent;

    constructor(private _commService: CommService) {}

    ngOnInit() {
        this.messageFromService$ = this._commService.sendMessage;
    }

    ngAfterViewChecked() {
        console.log('changes in the child component will be reflected');
    }

    ngAfterViewInit() {
        console.log('Our ViewChild properties will be populated');
    }
}

Now we have one of the ChildComponents on the template available for us to use in the parent component class. We can call public methods from that instance or pass data to the parent template. 2 lifecycle hooks relates to the ViewChildren decorator. AfterViewInit will be called once after all the ViewChild properties are populated with the instances. AfterViewChecked is called on every change detection cycle and this represents that the child components view has been updated with the changes.

Transclusion

Another way we can pass data from parent to child is via transclusion. Transclusion allows us to project an entire template html to the child. The child can define slots where to place the template data, we can also define multiple data and multiple slots. the scope of the template projected from the parent is the parent meaning the public properties and methods in the parent and not the child, even though that template is placed in the child. Lets modify the app.component.html add the following at the bottom:

<h2>
    Transclusion
</h2>

<app-child>
    <div header>
        <h1>title from parent</h1>
    </div>
    <div body>
        <app-child message="child from transclusion"></app-child>
    </div>
</app-child>

We are passing a header div which will be placed in a certain slot in the child, and a body div which will be placed in another. We can even transfer components to the child. Now let's place those elements in our child. In the template of the child child.component.html add this at the bottom:

<h2>
    Transclusion
</h2>

<h3>
    header slot
</h3>
<ng-content select="[header]"></ng-content>

<h3>
    body slot
</h3>
<ng-content select="[body]"></ng-content>

Our component is using the select attribute to place the content in different slots. The select can get a css like syntax so we are selecting by attribute. We are passing from the parent component template snippet which may contain other components (In our example we sent the ChildComponent) what if our child component needs to grab instance of element passed through ng-content, for example we want to grab the child component that is passed to us in the transclusion from the parent. This bring us to the next form of communication.

@ContentChild

works very similar to ViewChild decorator we seen before, we can use this to grab instance passed by transclusion, we can simply pass a string of TRV or a type of the component we want to grab. In the child component class child.component.ts let's grab the instance of the child component passed by transclusion. Modify that class:

export class ChildComponent implements AfterContentInit, AfterContentChecked{
    @Input() message: string;
    @Input('messageObj') obj: {msg : string};
    @Output('buttonClick') messageEvent: EventEmitter<string> = new EventEmitter();
    @ContentChild(ChildComponent) childFromParent: ChildComponent;

    constructor(private _commService: CommService) {}

    /**
    * when the user click the button to send message 
    * to parent
    */
    buttonClicked = () => {
        this.messageEvent.emit('hello from child');
        this._commService.sendMessage.next('message from child through service');
    }

    transferMessage = () => {
        return 'this is passed with TRV';
    }

    ngAfterContentInit() {
        console.log('ContentChild properties will be populated');
    }

    ngAfterContentChecked() {
        console.log('children passed in transclusion are updated with view changes');
    }
}

Two lifecycle hooks relates to the data passed through transclusion. The AfterContentInit will be run once, after which we know that the ContentChild properties are populated. The AfterContentChecked is run on every change detection after which we are gurenteed that the children passed in transclusion have updated the changes.

Summary

This article went over advanced components communication with angular, we also went over the lifecycle hooks and understanding the communication and connecting it to the lifecycle of a compoonent helps us better understand those hooks as well. Hope you learned new stuff from this tutorial, now it's time to practice it yourself. Happy coding...