Disqus Shortname

Responsive Advertisement

Angular the series Part 8: Component interaction


    In real applications, we should always split our project into small modules and components to make our code readable and easy to maintain and reuse. But that leads to one problem: How can we share the data between those component? In this section, we will find out the way components communicate with each other in Angular.

Parent to Child

    This is probably the most common method of sharing data between components in our Angular applications. We will use the @Input() decorator to allow data to be passed via the template. When you declare a variable with @Input() decorator in the child component, this variable then can be received from the parent component template.

parent.component.ts
import { Component, OnInit } from '@angular/core';

@Component({
    selector: 'app-parent',
    template: '<app-child [username]="user"></app-child>'
})
export class ParentComponent implements OnInit {
    user: string = 'Tian';
    constructor() { }

    ngOnInit(): void {
    }

}
child.component.ts
import { Component, OnInit, Input } from '@angular/core';

@Component({
    selector: 'app-child',
    template: '<p>{{username}}</p>'
}) export class ChildComponent implements OnInit {
    @Input() username: string;
    constructor() { }

    ngOnInit(): void {
    }

}

Child to Parent

    When a child component want to share its property with its parent, it can emit an event which can be listened to by the parent component. You can use this method when you want to share data changes that happen when user clicks on a button or change the input value on your child component. To do this, you can declare an event variable with type of EventEmitter using the @Output() decorator. And in the method handling the user interaction, you can emit a value to the parent component.

child.component.ts
import { Component, OnInit, Output, EventEmitter } from '@angular/core';

@Component({
    selector: 'app-child',
    template: `<button (click)="btnClick(true)">Like</button>
               <button (click)="btnClick(false)">Dislike</button>`
})
export class ChildComponent implements OnInit {
    @Output() interact = new EventEmitter();
    constructor() { }

    ngOnInit(): void {
    }

    btnClick(value: boolean) {
        this.interact.emit(value);
    }

}
parent.component.ts
import { Component, OnInit } from '@angular/core';

@Component({
    selector: 'app-parent',
    template: `<app-child (interact)="onInteract($event)"></app-child>
               <p>Like: {{like}}</p>
               <p>Dislike: {{dislike}}</p>`
})
export class ParentComponent implements OnInit {
    like: number = 0;
    dislike: number = 0;
    constructor() { }

    ngOnInit(): void {
    }

    onInteract(value: boolean) {
        value ? this.like++ : this.dislike++;
    }

}

Another way to share data between child and parent component is using @ViewChild(). All you need is to define a local variable inside parent component with the @ViewChild() decorator and has the type of child component, then you can have access to the child's properties and methods. However, that variable will not be available until the child component is initialized

child.component.ts
import { Component, OnInit } from '@angular/core';

@Component({
    selector: 'app-child',
    template: ''
})
export class ChildComponent implements OnInit {
    username: string = 'Tian';
    constructor() { }

    ngOnInit(): void {
    }

}
parent.component.ts
import { Component, OnInit, ViewChild, AfterViewInit } from '@angular/core';
import { ChildComponent } from '../child/child.component';

@Component({
    selector: 'app-parent',
    template: '<app-child #child></app-child>'
})
export class ParentComponent implements OnInit, AfterViewInit {
    username: string;
    @ViewChild('child') appChild: ChildComponent;
    constructor() { }

    ngOnInit(): void {
    }

    ngAfterViewInit(): void {
        this.username = this.appChild.username;
    }

}

*Note: Since Angular 8, that should be @ViewChild('child',{static: true}) in case you want to use the selected element inside of ngOnInit(). If you don't access that selected element in ngOnInit() but anywhere else in your component, set it to {static: false}. From Angular 9, you just need to add {static: true}. When you don't pass anything, the default will be {static: false}.


Any to Any

    As we already discussed in this article, multiple components can share the same instance of a service. We can take advantage of this feature to create a shared service to share data between components which have lack of direct connection like siblings or grandchildren. In this method, we should use a new definition called BehaviorSubject, it is a kind of Subject in RxJS and it ensure every components consume the service will receive the most up-to-date data (we will find out more about this in another section).

All things we need to do is to create a new service file containing a private BehaviorSubject to hold the current value and a variable to handle data as observable. This variable will be used in the components. We also need to define a method to change the value of BehaviorSubject. In all components we want to share data across, we have to inject our new share service in the constructor and then subscribe to the observable we created inside the service to get the current data, or call the predefined method to set a new value for it. If we change this value, the change is automatically sent to all other components.

share-data.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable()
export class ShareDataService {
    private dataSource = new BehaviorSubject<string>("");
    currentData = this.dataSource.asObservable();

    changeData(data: string) {
        this.dataSource.next(data);
    }
}
first-sibling.component.ts
import { Component, OnInit } from '@angular/core';
import { ShareDataService } from "./share-data.service";

@Component({
    selector: 'app-first-sibling',
    template: '<button (click)="emitNewData()">Press me</button>'
})
export class FirstSiblingComponent implements OnInit {
    count: number = 0; constructor(private shareDataService: ShareDataService) { }

    ngOnInit(): void {
    }

    emitNewData(): void {
        this.shareDataService.changeData(this.count++ + "This is from this first sibling!");
    }

}
second-sibling.component.ts
import { Component, OnInit } from '@angular/core';
import { ShareDataService } from "./share-data.service";

@Component({
    selector: 'app-second-sibling',
    template: '{{message}}'
})
export class SecondSiblingComponent implements OnInit {
    message: string;
    constructor(private shareDataService: ShareDataService) {
        this.shareDataService.currentData.subscribe((data: string) => {
            this.message = data;
        });
    }

    ngOnInit(): void {
    }

}