Skip to main content

Cách Share Data giữa các Component trong Angular

Trong phát triển ứng dụng Angular, chúng ta có thể có nhiều component với nhiều chức năng/tính năng khác nhau và trong khi phát triển ứng dụng, chúng ta có thể gặp tình huống cần chia sẻ hoặc chuyển dữ liệu từ component này sang component khác, trong trường hợp đó, chúng ta có thể đạt được điều đó bằng cách sử dụng một số cách chia sẻ dữ liệu giữa các component như trong các ví dụ dưới đây

Cách 1: Từ component cha tới con thông qua @Input

Khai báo @Input() cho phép thành phần cha cập nhật hoặc truyền dữ liệu sang thành phần con.

Bây giờ, chúng ta sẽ có một ví dụ cụ thể là một danh sách Giáo Viên, phụ trách một số môn học, và chúng ta sẽ truyền dữ liệu này từ component cha đến con, cụ thể là component teacher đến component student

Tạo một model Teacher

export class Teacher {
    name: string;
    subject: string;
}
export const Teachers = [
    { name: 'Mr. Deep', subject: 'Angular 6 in DotNet Techy YouTube Channel' },
    { name: 'Mr. Gautam', subject: 'C#, WEB API in DotNet Techy YouTube Channel' },
    { name: 'Mr. DotNet Techy', subject: 'High chart, chart js, prime ng, ag grid in DotNet Techy YouTube Channel ' }
];

Bây giờ ta tạo một project 

ng new DataSharing

Tạo 2 component là student và teacher

ng g component teacher
ng g component student

Trong template của teacher

<h1>
 <span style="color: red;font-size:larger">
 I am Teacher Component behaving as parent :)
 </span>
</h1>
<h2>{{principle}} controls {{teachers.length}} teachers</h2>
<app-student *ngFor="let teacher of teachers"
[teacher]="teacher"
[principle]="principle"></app-student>

 và component của teacher


import { Component, OnInit } from '@angular/core';
import { Teachers } from '../model/teacher.model';

@Component({
    selector: 'app-teacher',
    templateUrl: './teacher.component.html',
    styleUrls: ['./teacher.component.css']
})
export class TeacherComponent implements OnInit {

    teachers = Teachers;
    principle = 'Principle';

    constructor() { }

    ngOnInit() {
    }
}

Template của student component là hiển thị danh sách giáo viên và môn học họ sẽ dạy

<h1>
<span style="color: green;font-size:larger">I am Student Component behaving as child :)</span>
</h1>

<h3 style="color: yellowgreen;">{{teacher.name}} says:</h3>
<p style="color:blueviolet;">I, {{teacher.name}}, I will teach you {{teacher.subject}}, {{principleName}}.</p>

component của student

import { Component,Input, OnInit } from '@angular/core';
import { Teacher } from '../model/teacher.model';

@Component({
 selector: 'app-student',
 templateUrl: './student.component.html',
 styleUrls: ['./student.component.css']
})
export class StudentComponent implements OnInit {

 @Input() teacher: Teacher;
 @Input('principle') principleName: string;

 constructor() { }

 ngOnInit() {
 }

}

Bạn nhìn ở đoạn code trên bạn sẽ thấy student component sẽ có 2 thuộc tính @input được truyền vào 

@Input() teacher: Teacher;
@Input('principle') principleName: string;

Nó được sử dụng bởi template của teacher component ở trên

<app-student *ngFor="let teacher of teachers"
[teacher]="teacher"
[principle]="principle"></app-student>

Cách 2: Từ component con tới cha thông qua @Output và EventEmitter

Khai báo @Output() cho phép thành phần con cập nhật hoặc truyền dữ liệu sang thành phần cha.

EventEmitter được thiết kế để báo cho component cha khi component con có sự thay đổi.

Bây giờ là ví dụ cụ thể, chúng ta có một header component và login component bên dưới header. Khi người dùng đăng nhập tên người dùng và mật khẩu vào component login, thì header sẽ hiển thị tên người dùng lên trên header component.

Demo nào, tạo 2 component

ng g component header
ng g component login

Tạo template header

<h1>
    <span style="color: red;font-size:larger">
        I am Parent Component behaving as a parent for the @output and Event emitter :)
    </span>

</h1>
<div style="width: 100%;background-color: cornflowerblue">
    <h2 style="color: white">
        Welcome {{userName}}
    </h2>
</div>
<app-login (login)="onLogin($event)"></app-login>

Tạo header component

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

@Component({
    selector: 'app-header',
    templateUrl: './header.component.html',
    styleUrls: ['./header.component.css']
})
export class HeaderComponent implements OnInit {

    constructor() { }

    ngOnInit() {
    }
    userName = "";
    onLogin(user: string) {
        this.userName = user;
    }

}

Login template 

<h1>
    <span style="color: red;font-size:larger">
        I am Login Component behaving as a child for the @output and Event emitter :)
    </span>

</h1>
<div class="container">
    <form>
        <div class="form-group">
            <label for="userName">User Name</label>
            <input type="text" name="userName" [(ngModel)]="userName" class="form-control" id="userName" required>
            <!-- <input type="text" name="userName" #ctrl="ngModel" required> -->
        </div>
        <div class="form-group">
            <label for="Password">Password</label>
            <input type="password" class="form-control" id="Password">
        </div>
        <button (click)="submit()">Login</button>
    </form>
</div>

Login component

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

@Component({
    selector: 'app-login',
    templateUrl: './login.component.html',
    styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {

    userName: string = '';
    @Output() login = new EventEmitter<string>();
    constructor() { }

    ngOnInit() {
    }

    submit() {
        console.log(this.userName)
        this.login.emit(this.userName);
    }

}

Bạn sẽ thấy chúng ta import EventEmitter, Output từ core Angular để sử dụng

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

Sau đó, ta dùng @Output() gán cho thuộc tính login là một EventEmiter. Khi kích hoạt sự kiện đó sẽ trả về kèm giá trị nhận được từ sự kiện

@Output() login = new EventEmitter<string>();

Như vậy thuộc tính login sẽ là nhận được từ sự kiện login từ form đăng nhập, bạn sẽ lấy giá trị từ form thông qua sự kiện login: 

this.login.emit(this.userName);

Giờ chuyển qua header component, ta sẽ thấy nó đang render login component

<app-login (login)="onLogin($event)"></app-login>

Ở đây @Output của thuộc tính login có nhận sự kiện $event từ login component và gọi tới một method onLogin của header component để setting username trong header component template.

onLogin(user: string) {
 this.userName =user;
}

Vậy nên khi bạn đặt code này vào component của ứng dụng

<app-header></app-header>/Users/nguyenphudung/Sites/loc.wp-test/wp-config.php

Bạn sẽ nhận được giá trị từ component login truyền đến header component theo mối quan hệ cha con.

Cách 3: Từ component con tới cha thông qua @ViewChild

Tại sao phải dùng @ViewChild để truyền data từ component con qua cha khi mà đã có @output và Event Emitter. Bởi vì khi sử dụng @ViewChild sẽ cho phép component cha có thể kiểm soát các Event của component con.

@ViewChild được dùng khi chúng ta muốn truy cập vào các child component, directive hay DOM element từ parent component.

Trong ví dụ này sẽ có ecparent component là một component cha và component con là ecchild component. 

Trong ecparent template 

<h1>
    Election Commission Parent component.
</h1>

<h3>Vote Counting (via ViewChild)</h3>
<button (click)="start()">Start Vote Counting</button>
<button (click)="stop()">Stop Vote Counting(Lunch Break)</button>
<div class="seconds">{{ seconds() }}</div>
<app-ecchild></app-ecchild>

Ecparent component

import { Component, OnInit, ViewChild } from '@angular/core';
import { EcchildComponent } from '../ecchild/ecchild.component';

@Component({
    selector: 'app-ecparent',
    templateUrl: './ecparent.component.html',
    styleUrls: ['./ecparent.component.css']
})
export class EcparentComponent implements OnInit {

    @ViewChild(EcchildComponent)
    private counterComponent: EcchildComponent;
    seconds() { return 0; }
    ngOnInit() {

    }
    ngAfterViewInit() {
        // Redefine `seconds()` to get from the `CountdownTimerComponent.seconds` ...
        // but wait a tick first to avoid one-time devMode
        // unidirectional-data-flow-violation error
        setTimeout(() => this.seconds = () => this.counterComponent.seconds, 0);
    }
    start() { this.counterComponent.start(); }
    stop() { this.counterComponent.stop(); }
}

ecchild template 

 <h1>
 Election commission Child compoent which will do Vote Counting
 </h1>
 
 <p>{{message}}</p>

ecchild component

import { Component, OnInit, OnDestroy } from '@angular/core';

@Component({
    selector: 'app-ecchild',
    templateUrl: './ecchild.component.html',
    styleUrls: ['./ecchild.component.css']
})
export class EcchildComponent implements OnInit, OnDestroy {
    intervalId = 0;
    message = '';
    seconds = 0;
    clearTimer() { clearInterval(this.intervalId); }
    ngOnInit() { this.start(); }
    ngOnDestroy() { this.clearTimer(); }
    start() { this.countDown(); }
    stop() {
        this.clearTimer();
        this.message = `Holding at T-${this.seconds} seconds`;
    }
    private countDown() {
        this.clearTimer();
        this.intervalId = window.setInterval(() => {
            this.seconds += 1;
            if (this.seconds === 0) {
                this.message = 'Completed counting!';
            } else {
                if (this.seconds < 0) { this.seconds = 50; } // reset
                this.message = `Vote-${this.seconds} and counting going on`;
            }
        }, 1000);
    }
}

Bây giờ, sẽ giải thích đoạn code trên.

Đầu tiên để sử dụng @ViewChild thì import vào

import { Component, OnInit,ViewChild } from '@angular/core';

Sau đó nói với ứng dụng Angular là import viewchild là EcchildComponent, và gắn cho thuộc tính counterComponent là một component con Ecchild Component như một view child 

@ViewChild(EcchildComponent)
private counterComponent: EcchildComponent;

Và trong template html của component cha, chúng ta có 2 dòng code

 <button (click)="start()">Start Vote Counting</button>
 <button (click)="stop()">Stop Vote Counting(Lunch Break)</button>

Nếu bạn chú ý, start và stop là một sự kiện/ event của component con

start() { this.counterComponent.start(); }
stop() { this.counterComponent.stop(); }

Và nó được tham chiếu bởi

private counterComponent: EcchildComponent;

Bây giờ nói về component con, bạn sẽ thấy 2 method bên dưới sẽ được gọi từ component cha

    start() { this.countDown(); }
    stop() {
        this.clearTimer();
        this.message = `Holding at T-${this.seconds} seconds`;
    }

Để xem rõ đoạn code trên, bạn có thể render vào trong html của ứng dụng

<app-ecparent></app-ecparent>

Cách 4: Sử dụng Service để trao đổi dữ liệu hai component không liên quan với nhau

Trong ví dụ này, chúng ta có 3 component: basiccheck, advancecheck và finalcheck. Lần lượt là comment của từng quá trình phê duyệt (approval)

ng g component basiccheck
ng g component advancecheck 
ng g component finalcheck

Và chúng ta có thêm một service approval

ng g s approval

Bây giờ chúng ta sẽ tạo một service cung cấp stage hiện tại của của quá trình approval ( phê duyệt) của các component ở trên.

Đây là code của service

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

@Injectable({
    providedIn: 'root'
})
export class approvalService {

    private approvalStageMessage = new BehaviorSubject('Basic Approval is required!');
    currentApprovalStageMessage = this.approvalStageMessage.asObservable();

    constructor() {

    }
    updateApprovalMessage(message: string) {
        this.approvalStageMessage.next(message)
    }
}

Nếu bạn chú ý sẽ thấy dùng ta có import một BehaviorSubject từ rxjs

import { BehaviorSubject } from 'rxjs';

Để chúng ta có thể sử dụng

private approvalStageMessage = new BehaviorSubject('Basic Approval is required!');

và 

currentApprovalStageMessage = this.approvalStageMessage.asObservable();

Bây giờ chúng ta sẽ có đoạn code Basiccheck template

<b>Current Message</b> -{{message}}<br>
<b>Updated approval message-</b> {{approvalText}}
<div class="container">
    <form>
        <div class="form-group">
            <label for="approvalText">approval message</label>
            <input type="text" name="approvalText" [(ngModel)]="approvalText" class="form-control" id="approvalText" >
        </div>
        <button (click)="submit()">Approve Basic</button>
    </form>
</div>

Và Basiccheck component

import { Component, OnInit } from '@angular/core';
import { approvalService } from "../approval.service";

@Component({
    selector: 'app-basiccheck',
    templateUrl: './basiccheck.component.html',
    styleUrls: ['./basiccheck.component.css']
})
export class BasiccheckComponent implements OnInit {
    message: string = "";
    approvalText: string = "";
    constructor(private appService: approvalService) { }

    ngOnInit() {
        this.appService.currentApprovalStageMessage.subscribe(msg => this.message = msg);

    }

    submit() {
        console.log(this.approvalText)
        this.appService.updateApprovalMessage(this.approvalText)
    }

}

Nếu bạn chú ý, sẽ thấy chúng ta import một service approval ở trên, và tròn ngOninit chúng ta có subscribing appoval service và nhận lấy giá trị stage từ service

ngOnInit() {
  this.appService.currentApprovalStageMessage.subscribe(msg => this.message = msg);
}

Và khi click button "Approval Basic" sẽ gọi đến submit() và cập nhập lại stage hiện tại của quá trình Approval này.

    submit() {
        console.log(this.approvalText)
        this.appService.updateApprovalMessage(this.approvalText)
    }

Tương tự như Basiccheck component,ta cũng có advancecheck component và finalcheck component

Advancecheck component template

<b>Current Message</b> -{{message}}<br>
<b>Updated approval message-<cc
<div class="container">
    <form>
        <div class="form-group">
            <label for="approvalText">approval message</label>
            <input type="text" name="approvalText" [(ngModel)]="approvalText" class="form-control" id="approvalText" >
        </div>
        <button (click)="submit()">Approve Advance</button>
    </form>
</div>

Advancecheck component 

import { Component, OnInit } from '@angular/core';
import { approvalService } from '../approval.service';

@Component({
    selector: 'app-advancecheck',
    templateUrl: './advancecheck.component.html',
    styleUrls: ['./advancecheck.component.css']
})
export class AdvancecheckComponent implements OnInit {
    message: string = "";
    approvalText: string = "";
    constructor(private appService: approvalService) { }

    ngOnInit() {
        this.appService.currentApprovalStageMessage.subscribe(msg => this.message = msg)
    }
    submit() {
        console.log(this.approvalText)
        this.appService.updateApprovalMessage(this.approvalText);
    }
}

Finalcheck component html

<b>Current Message</b> -{{message}}<br>
<b>Updated approval message-</b> {{approvalText}}
<div class="container">
    <form>
        <div class="form-group">
            <label for="approvalText">User Name</label>
            <input type="text" name="approvalText" [(ngModel)]="approvalText" class="form-control" id="approvalText" >
        </div>
        <button (click)="submit()">Approve Final</button>
    </form>
</div>

Và finalcheck component

import { Component, OnInit } from '@angular/core';
import { approvalService } from '../approval.service';

@Component({
    selector: 'app-finalcheck',
    templateUrl: './finalcheck.component.html',
    styleUrls: ['./finalcheck.component.css']
})
export class FinalcheckComponent implements OnInit {
    message: string = "";
    approvalText: string = "";
    constructor(private appService: approvalService) { }

    ngOnInit() {
        this.appService.currentApprovalStageMessage.subscribe(msg => this.message = msg);
    }
    submit() {

        this.appService.updateApprovalMessage(this.approvalText);
    }
}

Để chạy các component, bạn có thể thêm code vào html của ứng dụng

<!-- Sharing data via services -->
 <app-basiccheck></app-basiccheck>
 <app-advancecheck></app-advancecheck>
 <app-finalcheck></app-finalcheck>

Như bạn thấy, chúng ta có khá nhiều cách để chia sẻ data thông qua các component khác nhau. Nếu có cách nào hay hơn hãy share cho mình với nhé