In the wake of the disappearance of $scope
, Angular was left with a void for propagating information up the component tree. This void is filled in part by custom events, and they represent the Yin to the downward data binding Yang.
The code, links, and a live example of this are available at .
Suppose you had an Article application as follows:
[app/text-editor.component.ts] import {Component} from '@angular/core'; @Component({ selector: 'text-editor', template: ` <textarea></textarea> ` }) export class TextEditorComponent {} [app/article.component.ts] import {Component} from '@angular/core'; @Component({ selector: 'article', template: ` <h1>{{title}}</h1> <p>Word count: {{wordCount}}</p> <text-editor></text-editor> ` }) export class ArticleComponent { title:string = ` Maternity Ward Resorts to Rock Paper Scissors Following Baby Mixup`; wordCount:number = 0; updateWordCount(e:number):void { this.wordCount = e; } }
This application will ideally be able to read the content of textarea
when there is a change, and also count the number of words and report it to the parent component to be interpolated. As is the case, none of this is implemented.
A developer thinking in terms of Angular 1 would attach ng-model
to textarea
, use $scope.$watch
on the model data, and pass the data to the parent via $scope
or some other means. Unfortunately for such a developer, these constructs are radically different or non-existent in Angular 2. Fear not! The new implementation is more expressive, more modular, and much cleaner.
ngModel
still exists in Angular 2, and it would certainly be suitable here. However, you don't actually need to use ngModel
at all, and in this case, it allows you to be more explicit about when your application takes action. First, you must retrieve the text from the textarea
element and make it usable in TextEditorComponent
:
[app/text-editor.component.ts] import {Component} from '@angular/core'; @Component({ selector: 'text-editor', template: ` <textarea (keyup)="emitWordCount($event)"></textarea> ` }) export class TextEditorComponent { emitWordCount(e:Event):void { console.log( (e.target.value.match(/\S+/g) || []).length); } }
Excellent! As claimed, you don't need to use ngModel
to acquire the element's contents. What's more, you are now able to utilize native browser events to explicitly define when you want TextEditorComponent
to take action.
With this, you are setting a listener on the native browser's keyup
event, fired from the textarea
element. This event has a target
property that exposes the value of the text in the element, which is exactly what you want to use. The component then uses a simple regular expression to count the number of non-whitespace sequences. This is your word count.
console.log
does not help to inform the parent component of the word count you are calculating. To do this, you need to create a custom event and emit it upwards:
[app/text-editor.component.ts] import {Component, EventEmitter, Output} from '@angular/core'; @Component({ selector: 'text-editor', template: ` <textarea (keyup)="emitWordCount($event)"></textarea> ` }) export class TextEditorComponent { @Output() countUpdate = new EventEmitter<number>(); emitWordCount(e:Event):void { this.countUpdate.emit( (e.target.value.match(/\S+/g) || []).length); } }
Using the @Output
decorator allows you to instantiate an EventEmitter
member on the child component that the parent component will be able to listen to. This EventEmitter
member, like any other class member, is available as this.countUpdate
. The child component is able to send events upward by invoking the emit()
method on this member, and the argument to this method is the value which you wish to send to the event. Here, since you want to send an integer count of words, you instantiate the EventEmitter
member by typing it as a <number>
emitter.
So far, you are through with only half the implementation, as these custom events are being fired off into the ether of the browser with no listeners. Since the method you need to use is already defined on the parent component, all you need to do is hook into the event listener to that method.
The ( )
template syntax is used to add listeners to events, and Angular does not discriminate between native browser events and events that originate from EventEmitters
. Thus, since you declared the child component's EventEmitter
as @Output
, you will be able to add a listener for events that come from it on the parent component, as follows:
[app/article.component.ts] import {Component } from 'angular2/core'; @Component({ selector: 'article', template: ` <h1>{{title}}</h1> <p>Word count: {{wordCount}}</p> <text-editor (countUpdate)="updateWordCount($event)"> </text-editor> ` }) export class ArticleComponent { title:string = ` Maternity Ward Resorts to Rock Paper Scissors Following Baby Mixup`; wordCount:number = 0; updateWordCount(e:number):void { this.wordCount = e; } }
With this, your application should correctly count the words in the TextEditor
component and update the value in the Article
component.
Using @Output
in conjunction with EventEmitter
allows you to create child components that expose an API for the parent component to hook into. The EventEmitter
sends the events upward with its emit
method, and the parent component can subscribe to them by binding to the emitter output.
The flow of this example is as follows:
textarea
causes the native browser's keyup
event.TextEditor
component has a listener set on this event, so the attached expression is evaluated, which will invoke emitWordCount
.emitWordCount
inspects the Event
object and extracts the text from the associated DOM element. It parses the text for the number of contained words and invokes the EventEmitter.emit
method.EventEmitter
method emits an event associated with the declared countUpdate @Output
member.ArticleComponent
sees this event and invokes the attached expression. The expression invokes updateWordCount
, passing in the event value.ArticleComponent
property is updated, and since this value is interpolated in the view, Angular honors the data binding process by updating the view.The name EventEmitter
is a bit deceiving. If you're paying attention, you will notice that the parent component member method invoked in the handler does not have a typed parameter. You will also notice that you are directly assigning that parameter to the member typed as number
. This should seem odd as the template expression invoking the method is passing $event
, which you used earlier as a browser Event
object. This seems like a mismatch because it is a mismatch. If you bind to native browser events, the event you will observe can only be the native browser event object. If you bind to custom events, the event you will observe is whatever was passed when emit
was invoked. Here, the parameter to updateWordCount()
is simply the integer you provided with this.countUpdate.emit()
.
Also note that you are not required to provide a value for the emitted event. You can still use EventEmitter
to signal to a parent component that an event has occurred and that it should evaluate the bound expression. To do this, you simply create an untyped emitter with new EventEmitter()
and invoke emit()
with no arguments. $event
should be undefined
.
It is not possible to pass multiple values as custom events. To send multiple pieces of data, you need to combine them into an object or array.