Bài viết được dịch từ: toddmotto.com

Các component của Angular 2+ có một cách để thông báo cho các component cha khi có một vài thứ thay đổi, đó là các event. Nó được thiết kế xung quanh hệ thống luồng dữ liệu một chiều (uni-directional), thông qua một hướng tiếp cận hợp lý hơn nhiều để phát triển ứng dụng.

Mục lục

  • Giới thiệu
  • AngularJS
  • Stateful (cha) component
  • @Output decorator
  • EventEmitter
  • Khởi tạo một instance của EventEmitter
  • Nhiệm của hàm callback trong Stateful
  • Bonus: tùy chỉnh tên các thuộc tính
  • Plunker

Hãy học cách giao tiếp cơ bản giữa các component cha - con và con - cha bằng EventEmitter và @Output.

Các bài viết trong loạt bài này

  1. Khởi động ứng dụng Angular đầu tiên của bạn
  2. Tạo Angular 2+ component đầu tiên của bạn
  3. Truyền dữ liệu vào các component Angular với @Input
  4. Các event component với EventEmitter và @Output trong Angular

Giới thiệu

Bài hướng dẫn này sẽ đề cập tới các event sử dụng EventEmitter API và @Output decorator. Chúng cho phép chúng ta phát ra (emit) các event từ một component trong Angular 2+.

AngularJS

Với những người đã làm việc với AngularJS, khái niệm này có thể hơi giống với .component() API và callback binding sử dụng '&':

const counter = {
  bindings: {
    count: '<',
    onChange: '&'
  },
  template: `
    <div class="counter">
      <button ng-click="$ctrl.decrement()">
        Decrement
      </button>
      <input type="text" ng-model="$ctrl.count">
      <button ng-click="$ctrl.increment()">
        Increment
      </button>
    </div>
  `,
  controller() {
    this.$onInit = () => {
      this.count = this.count || 0;
    };
    this.increment = () => {
      this.count++;
      this.onChange(this.count);
    };
    this.decrement = () => {
      this.count--;
      this.onChange(this.count);
    };
  }
};

angular
  .module('app')
  .component('counter', counter);

Thành phần chính ở đây là sử dụng cú pháp callback onChange: '&'. Điều này có nghĩa là chúng ta mong muốn một hàm được truyền xuống từ component cha, và chúng ta có thể gọi nó khi count thay đổi (thông qua this.onChange()) - về cơ bản callback được đăng ký trong component cha, chúng ta sẽ gọi nó trong component con và truyền vào dữ liệu mới, giống như thế này:

<div class="parent">
  <counter
    value="$ctrl.someValue"
    on-change="$ctrl.valueChanged($event)">
  </counter>
</div>

Khi component con gọi $ctrl.valueChanged, dữ liệu được truyền tới nó và chúng ta có thể truy cập phần dữ liệu mới thông qua đối tượng @event.

Stateful component binding

Trong bài hướng dẫn trước chúng ta sử dụng @Input decorator để chấp nhận một input binding, chúng có thể làm tương tự, lắng nghe trong component cha khi giá trị thay đổi trong component con.

Để làm điều này, chúng ta sẽ quay trở lại component cha cái rendering <counter>:

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

@Component({
  selector: 'app-root',
  template: `
    <div class="app">
      Parent: {{ myCount }}
      <counter
        [count]="myCount"
        (change)="countChange($event)">
      </counter>
    </div>
  `
})
export class AppComponent {
  myCount: number = 10;
  countChange(event) {

  }
}

Tôi đã làm vài thứ ở đây:

  • Thay đổi initialCount thành myCount, chúng ta không thiết lập một "initialCount", vì vậy count state sẽ được quản lý trong component cha khi component con thay đổi nó.
  • Tạo một một thuộc tính thay đổi tùy biến cho <counter> template, sử dụng cú pháp () event binding, giống như chúng ta đã học khi tạo component đầu tiên điều này báo hiệu một vài loại event (chẳng hạn như click).
  • Thêm thuộc tính myCount trong component cha.
  • Thêm phương thức countChange() {} tới class, và truyền nó tới event (change).

Đây là thiết lập luồng dữ liệu một chiều. Dữ liệu từ AppComponent class tới <counter>, counter có thể thay đổi giá trị - và khi giá trị thay đổi chúng ta muốn countChange() được gọi. Bây giờ chúng ta cần làm điều đó

@Output decorator

Giống như sử dụng Input, chúng ta có thể import Output và sử dụng nó với một thuộc tính mới là change bên trong CounterComponent:

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

@Component({...})
export class CounterComponent {

  @Input()
  count: number = 0;

  @Output()
  change;

  // ...

}

Điều này sẽ cấu hình metadata cần thiết để nói với Angular thuộc tính này được coi như output binding. Tuy nhiên, nó cần đi kèm với một thứ được gọi là EventEmitter.

EventEmitter

Đây là phần thú vị. Để có thể sử dụng Output, chúng ta cần import và liên kết (bind) với một instance mới của EventEmitter tới nó:

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

@Component({...})
export class CounterComponent {

  // ...

  @Output()
  change = new EventEmitter();

  // ...

}

Sử dụng TypeScript chúng ta có thể làm một vài thứ như thế này để báo hiệu kiểu giá trị của event mà chúng ta phát ra (emit). Trong trường hợp của chúng ta là kiểu number:

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

@Component({...})
export class CounterComponent {

  // ...

  @Output()
  change: EventEmitter<number> = new EventEmitter<number>();

  // ...

}

Khởi tạo một instance của EventEmitter

Vậy cái gì đang xảy ra ở đây? Chúng ta đã tạo một thuộc tính change, và gán cho nó  một instance mới của EventEmitter, làm gì tiếp theo?

Giống như ví dụ của AngularJS tôi đã đưa ra ở trên, chúng ta chỉ cần gọi phương thức this.change - tuy nhiên bởi vì nó tham chiếu tới một instance của EventEmitter, chúng phải gọi .emit() để phát (emit) một event tới component cha:

@Component({...})
export class CounterComponent {

  @Input()
  count: number = 0;

  @Output()
  change: EventEmitter<number> = new EventEmitter<number>();

  increment() {
    this.count++;
    this.change.emit(this.count);
  }

  decrement() {
    this.count--;
    this.change.emit(this.count);
  }

}

Điều này sẽ phát ra một sự thay đổi tới (change) listener chúng ta đã thiết lập trong component cha, và hàm callback countChange($event) sẽ được gọi, và dữ liệu liên quan với event sẽ được cung cấp thông qua tham số $event.

Nhiệm vụ hàm callback trong Stateful

Đến đây chúng ta cần gán lại this.myCount với event cái được truyền ngược trở lại. Tôi sẽ giải thích tại sao ở dưới:

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

@Component({
  selector: 'app-root',
  template: `
    <div class="app">
      Parent: {{ myCount }}
      <counter
        [count]="myCount"
        (change)="countChange($event)">
      </counter>
    </div>
  `
})
export class AppComponent {
  myCount: number = 10;
  countChange(event) {
    this.myCount = event;
  }
}

Tại sao chúng ta làm điều này? Nó tạo ra một luồng dữ liệu một chiều. Dữ liệu từ AppComponent, truyền tới <counter>, counter tạo ra một sự thay đổi, và phát ra thay đổi đó ngược trở lại component cha thông qua EventEmitter chúng ta thiết lập. Khi chúng ta nhận được dữ liệu mới, chúng trộn các thay đổi đó vào trong component cha.

Lý do chúng ta làm điều này để minh họa Parent: {{ myCount }} cập nhật cùng thời điểm Output thông báo cho component cha.

Bonus: tùy chỉnh tên thuộc tính

Giống với @Input() có thể tạo các thuộc tính với tên tùy ý, chúng ta cũng có thể làm điều tương tự với @Output.

Hãy thay đổi (change) thành (update):

@Component({
  selector: 'app-root',
  template: `
    <div class="app">
      Parent: {{ myCount }}
      <counter
        [count]="myCount"
        (update)="countChange($event)">
      </counter>
    </div>
  `
})
export class AppComponent {
  myCount: number = 10;
  countChange(event) {
    this.myCount = event;
  }
}

Chúng ta có thể sử dụng thuộc tính update với thuộc tính bên class CounterComponent như sau:

@Component({...})
export class CounterComponent {

  // ...

  @Output('update')
  change: EventEmitter<number> = new EventEmitter<number>();

  increment() {
    this.count++;
    this.change.emit(this.count);
  }

  decrement() {
    this.count--;
    this.change.emit(this.count);
  }

}

Ở đây, chúng ta chỉ nói với Angular coi update như là thuộc tính được liên kết tới, và chúng ta có thể sử dụng this.change trong nội bộ component con.

Plunker

Mọi thứ chúng ta làm đã có sẵn trong một Plunker cho bạn tham khảo.

Bạn có thể thử phiên bản không có hàm callback, để thấy state không được cập nhật trong phần tử cha:

Và phiên bản update state với hàm callback trong phần tử cha: