When you start learning Angular, one of the first things you learn is how to communicate between child and parent components.
Data flow is passed into your component through property bindings, and data flow out of your component is through event bindings.
If you want your component to notify its parent about something, you can use the Output decorator
with the EventEmitter
to create a custom event. We have the following example:
1 2 3 4 5 6 7 8 9 10 11 | @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(); } |
We can use the Output decorator to label our add
as an event that a component can fire to send data to its parent.
And the parent component can listen for an event like this:
1 2 | <add-todo (add)="addTodo($event)"></add-todo> |
Angular will register the add
event and call the addTodo()
with the data when the component activates the next()
.
So what is EventEmitter?
If you look at the code below, you should see something interesting:
1 2 3 4 5 6 7 | 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; } |
We can see that the EventEmitter
actually a Subject
.
The first thing you can see from the code above is that you can pass a boolean value to EventEmitter to determine whether to send events synchronously or asynchronously (Default is synchronous).
You have the power of Rx
Because EventEmitters
are Subject
, we can use all of the Rx features. For example, we want to fire an event only when we have a value.
1 2 | @Output() add = new EventEmitter().filter(v => !!v); |
Here you have seen its power yet.
But that is not enough. We also may use any Subject
that you want. Try using BehaviorSubject
:
1 2 | @Output() add = new BehaviorSubject("Awesome").filter(v => !!v); |
EventEmitters! == DOM events
Unlike DOM events, Angular custom events are not event bubbling . What does it mean if you define something like this:
1 2 3 4 5 6 7 8 | export class TodoComponent { @Output() toggle = new EventEmitter<any>(); } export class TodosComponent {} export class TodosPageComponent {} |
You can only listen to the TodoComponent
toggle event in its parent component.
The following code will work:
1 2 3 4 5 6 | // todos.component <app-todo [todo]="todo" *ngFor="let todo of todos.data" (toggle)="toggleTodo($event)"> </app-todo> |
And the following code does not:
1 2 3 | // todos-page.component <app-todos (toggle)="toggle($event)"></app-todos> |
And the solution is
- Continue to keep moving the event to the tree
1 2 3 4 5 6 7 8 9 10 11 12 | export class TodoComponent { @Output() toggle = new EventEmitter<any>(); } export class TodosComponent { @Output() toggle = new EventEmitter<any>(); } export class TodosPageComponent { toggle($event) {} } |
In this example, that’s fine, but can get annoying if you have nested components
- Using native DOM events
You could create native DOM events as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | @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 })); } } |
The custom event is sent by calling the DispatchEven()
. We can pass the data to our event using the detail
property.
1 2 3 4 | // todos-page.component <app-todos [todos]="todos$ | async" (toggle-todo)="toggle($event)"></app-todos> |
Event bubbling will work here, but the problem with this approach is that we missed the chance to execute it even in a non-DOM environment like native mobile, native desktop, web worker or server side rendering.
- Shared Service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | @Injectable() export class TodosService { private _toggle = new Subject(); toggle$ = this._toggle.asObservable(); toggle(todo) { this._toggle.next(todo); } } export class TodoComponent { constructor(private todosService: TodosService) {} toggle(todo) { this.todosService.toggle(todo); } } export class TodosPageComponent { constructor(private todosService: TodosService) { todosService.toggle$.subscribe(..); } } |
We can use the TodoService
as a message bus. You can learn more about this approach from the documentation .
Because EventEmitters are observables, we can do some crazy things with them. Let’s say you have a button component and you need to know when the user finishes pressing all x buttons and then getting the latest value from it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | @Component({ selector: 'my-button', template: `<button (click)="click()">Click</button>` }) export class MyButtonComponent { @Output() clicked = new EventEmitter(); click() { this.clicked.next(Math.random()); } } @Component({ template: `<my-button></my-button> <my-button></my-button> <my-button></my-button>` }) export class AppComponent { @ViewChildren(MyButtonComponent) buttons; btns$; ngAfterViewInit() { const outputs = this.buttons.map(button => button.clicked); this.btns$ = Observable.combineLatest(...outputs).subscribe(...) } } ```![](https://images.viblo.asia/c3fe8cbf-51ac-408e-acd1-b6148303a438.gif) ### Tài liệu tham khảo https://netbasal.com/event-emitters-in-angular-13e84ee8d28c |