0

Hiểu hơn về Event Emitters và giao tiếp giữa các component trong Angular 2

Để hiểu được các đoạn code cũng như các khái niệm trong bài viết bạn nên có kiến thức về RxJs, các bạn có thể xem ở đây

Mở đầu

Khi bạn bắt đầu học Angular, một trong những điều đầu tiên bạn học là làm thế nào để giao tiếp giữa các thành phần con và cha của nó. Luồng dữ liệu đi vào component của bạn thông qua các property bindings và ra khỏi các component thông qua các event bindings. Nếu bạn muốn một component thông báo cho cha nó về một thông điệp gì đó, bạn có thể sử dụng Output decorator với EventEmitter để custom một sự kiện. Ví dụ:

@Component({
  selector: 'add-todo',
  template: `<input type="text" placeholder="Add todo.." [formControl]="control">
             <button (click)="add.next(control.value)">Add</button>
`,
})
export class AddTodoComponent {
  control : FormControl = new FormControl("");
  @Output() add = new EventEmitter();
}

Chúng ta có thể sử dụng Output decorator để gắn property add như một sự kiện mà component có thể kích hoạt để gửi dữ liệu đến cha của nó. Component cha có thể nghe một sự kiện như thế này:

<add-todo (add)="addTodo($event)"></add-todo>

Angular sẽ đăng ký vào sự kiện add và gọi phương thức addTodo () với dữ liệu khi component triggers the next() method.

EventEmitter là gì?

Nếu bạn xem mã nguồn, bạn sẽ thấy một cái gì đó thú vị.

export declare class EventEmitter<T> extends Subject<T> {
    __isAsync: boolean;
    constructor(isAsync?: boolean);
    emit(value?: T): void;
    subscribe(generatorOrNext?: any, error?: any, complete?: any): any;
}

Event Emitters cũng chỉ là Subjects.

Điều đầu tiên bạn có thể học được từ mã nguồn là bạn có thể truyền một kiểu dữ liệu boolean cho EventEmitter để xác định nên gửi các sự kiện theo cách đồng bộ hay không đồng bộ. (Mặc định là đồng bộ)

==> Vì vậy bạn có thể tận dụng sức mạnh của Rx: Vì EventEmitters là Subject, chúng ta có thể sử dụng tất cả sức mạnh của Rx. Ví dụ: tôi chỉ muốn phát ra một sự kiện nếu chúng có giá trị.

@Output() add = new EventEmitter().filter(v => !!v);

Đó chưa phải là tất cả. Chúng tôi cũng có thể sử dụng bất kỳ Subject nào mà chúng tôi muốn. Hãy cùng cố gắng sử dụng BehaviorSubject.

@Output() add = new BehaviorSubject("Awesome").filter(v => !!v);

Giao tiếp giữa các component

EventEmitters !== DOM events

Unlike DOM events Angular custom events do not bubble. Điều đó có nghĩa là nếu bạn định nghĩa một cái gì đó như thế này:

export class TodoComponent {
  @Output() toggle = new EventEmitter<any>();
}

export class TodosComponent {}

export class TodosPageComponent {}

Bạn chỉ có thể nghe sự kiện TodoComponent toggle ở cấp độ parent. Vì vậy, dòng code dưới đây sẽ chạy:

// todos.component
<app-todo [todo]="todo"
          *ngFor="let todo of todos.data"
          (toggle)="toggleTodo($event)">
</app-todo>

Nhưng như này thì không:

// todos-page.component
<app-todos (toggle)="toggle($event)"></app-todos>

Hãy cùng xem các cách giải quyết vấn đề trên để hiểu hơn về EventEmitter

1. Truyền các sự kiện theo dạng cây

export class TodoComponent {
  @Output() toggle = new EventEmitter<any>();
}

export class TodosComponent {
  @Output() toggle = new EventEmitter<any>();
}

export class TodosPageComponent {
  toggle($event) {}
}

Trong ví dụ này, nó rất tốt, nhưng nó có thể gây nản lòng nếu bạn có nhiều các thành phần lồng nhau.

2. Sử dụng native DOM events

Bạn có thể tạo native DOM events tương tự như:

@Component({
  selector: 'app-todo',
  template: `
     <p (click)="toggleTodo(todo)">
      {{todo.title}}
     </p>
   `
})
export class TodoComponent {
  @Input() todo;
  constructor(private el: ElementRef) {}

  toggleTodo(todo) {
    this.el.nativeElement
      .dispatchEvent(new CustomEvent('toggle-todo', {
        detail: todo,
        bubbles: true
      }));
  }
}

Sự kiện được gửi đi bằng cách gọi phương thức dispatchEvent(). Chúng ta có thể truyền dữ liệu đến sự kiện với detail property của CustomEvent.

// todos-page.component
<app-todos [todos]="todos$ | async"
           (toggle-todo)="toggle($event)"></app-todos>

Event bubbling sẽ hoạt động ở đây, nhưng vấn đề với cách tiếp cận này là chúng ta bỏ lỡ cơ hội để có thể thực thi trong các môi trường không có DOM như native mobile, native desktop, web worker or server side rendering.

3. Sử dụng Shared Service

@Injectable()
export class TodosService {
  private _toggle = new Subject();
  toggle$ = this._toggle.asObservable();

// method này để change _toggle
  toggle(todo) {
    this._toggle.next(todo);
  }
}

export class TodoComponent {
  constructor(private todosService: TodosService) {}
  
  // ở đây sẽ thay đổi _toggle ở TodosService
  toggle(todo) {
    this.todosService.toggle(todo);
  }
}

export class TodosPageComponent {
  constructor(private todosService: TodosService) {
  // lắng nghe sự thay đổi khi TodoComponent có sự kiện toggle
    todosService.toggle$.subscribe(..);
  }
}

Cách này ta có thể sự dụng tốt khi có nhiều component lồng nhau tuy nhiên sẽ khó kiểm soát nếu ở đâu cũng sử dụng todosService.toggle$.subscribe(..); bởi vì chỉ một thay nhưng component nào sử dụng đoạn trên cũng sẽ thực thi code trong đó.

Kết luận

Bài viết cung cấp thêm thông tin cho bạn về EventEmitter một khái niệm quen thuộc trong Angular 2 và một số cách phổ biến để trao đổi các custom event giữa các component, hi vọng sẽ giúp ích được cho công việc của bạn


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.