Angular 2 will often provide you with an Observable
interface to attach to for free, but it is important to know how they are created, configured, and used. More specifically, it is valuable for you to know how to take Observables
and apply them to real scenarios that will be encountered in the client.
The code, links, and a live example of this are available at .
Suppose you started with the following skeleton application:
[app/click-observer.component.ts] import {Component} from '@angular/core'; @Component({ selector: 'click-observer', template: ` <button> Emit event! </button> <p *ngFor="let click of clicks; let i = index"> {{i}}: {{click}} </p> ` }) export class ClickObserverComponent { clicks:Array<Event> = []; }
Your goal is to convert this so that all the button click events are logged in to the repeated field.
Accomplishing this with a component member method and using it in the click
event binding in the template is possible, but this doesn't capture the real value of Observables
. You want to be able to expose an Observable
on ClickObserverComponent
. This will allow any other part of your application to subscribe to these click
events and handle them in its own way.
Instead, you would like to be able to funnel the click
events from the button into the Observable
. With a regular Observable
instance, this isn't possible since it is only acting as the Subscribe part of the Publish-Subscribe model. To accomplish the Publish aspect, you must use a Subject
instance:
[app/click-observer.component.ts] import {Component} from '@angular/core'; import {Subject} from 'rxjs/Subject'; @Component({ selector: 'click-observer', template: ` <button> Emit event! </button> <p *ngFor="let click of clicks; let i = index"> {{i}}: {{click}} </p> ` }) export class ClickObserverComponent { clickEmitter:Subject<Event> = new Subject(); clicks:Array<Event> = []; }
ReactiveX Subjects
act as both the Observable
and the Observer
. Therefore, it exposes both the subscribe()
method, used for the Subscribe behavior, and the next()
method, used for the Publish behavior.
In this example, the next()
method is useful because you want to explicitly specify when an emission should occur and what that emission should contain. There are lots of ways of instantiating Observables
in order to implicitly generate emissions, such as (but certainly not limited to) Observable.range()
. In these cases, Observable
understands how its input behaves, and thus it does not need direction as to when emissions occur and what they should contain.
In this case, you can pass the event directly to next()
in the template click handler definition. With this, all that is left is to populate the array by directing the emissions into it:
[app/click-observer.component.ts] import {Component} from '@angular/core'; import {Subject} from 'rxjs/Subject'; @Component({ selector: 'click-observer', template: ` <button (click)="clickEmitter.next($event)"> Emit event! </button> <p *ngFor="let click of clicks; let i = index"> {{i}}: {{click}} </p> ` }) export class ClickObserverComponent { clickEmitter:Subject<Event> = new Subject(); clicks:Array<Event> = []; constructor() { this.clickEmitter .subscribe(clickEvent => this.clicks.push(clickEvent)); } }
That's all! With this, you should see click events populate in the browser with each successive button click.
ReactiveX Observables
and Observers
are distinct, but their behavior is mutually compatible in such a way that their union, Subject
, can act as either one of them. In this example, the Subject
is used as the interface to feed in Event
objects as the Publish modality as well as to handle the result that would come out as the Subscribe modality.
The way this is constructed might feel a bit strange to you. The component is exposing the Subject
instance as the point where your application will attach observer handlers.
However, you want to prevent other parts of the application from adding additional events, which is still possible should they choose to use the next()
method. What's more, the Subject
instance is referenced directly inside the template and exposing it there may feel a bit odd. Therefore, it is desirable, and certainly good software practice, to only expose the Observable
component of the Subject
.
To do this, you must import the Observable
module and utilize the Subject
instance's member method, namely asObservable()
. This method will create a new Observable
instance that will effectively pipe the observed emissions from the Subject
into the new Observable
, which will be exposed as a public component member:
[app/article.component.ts] import {Component} from '@angular/core'; import {Observable} from 'rxjs/Observable'; import {Subject} from 'rxjs/Subject'; @Component({ selector: 'click-observer', template: ` <button (click)="publish($event)"> Emit event! </button> <p *ngFor="let click of clicks; let i = index"> {{i}}: {{click}} </p> ` }) export class ClickObserverComponent { clickEmitter: Observable<Event>; private clickSubject_: Subject<Event> = new Subject(); clicks:Array<Event> = []; constructor() { this.clickEmitter = this.clickSubject_.asObservable(); this.clickEmitter.subscribe(clickEvent => this.clicks.push(clickEvent)); } publish(e:Event):void { this.clickSubject_.next(e); } }
Now even though only this component is referencing clickEmitter
, every component that uses clickEmitter
will not need or be able to touch the source, Subject
.
This has all been a great example, but this is such a common pattern in that the RxJS library already provides a built-in way of implementing it. The Observable
class exposes a static method fromEvent()
, which takes in an element that is expected to generate events and the event type to listen to.
However, you need a reference to the actual element, which you currently do not have. For the present implementation, the Angular 2 ViewChild
faculties will give you a very nice reference to the button, which will then be passed to the fromEvent()
method once the template has been rendered:
[app/click-observer.component.ts] import {Component, ViewChild, ngAfterViewInit} from '@angular/core'; import {Observable} from 'rxjs/Observable'; import 'rxjs/add/observable/fromEvent'; @Component({ selector: 'click-observer', template: ` <button #btn> Emit event! </button> <p *ngFor="let click of clicks; let i = index"> {{i}}: {{click}} </p> ` }) export class ClickObserverComponent implements AfterViewInit { @ViewChild('btn') btn; clickEmitter:Observable<Event>; clicks:Array<Event> = []; ngAfterViewInit() { this.clickEmitter = Observable.fromEvent( this.btn.nativeElement, 'click'); this.clickEmitter.subscribe(clickEvent => this.clicks.push(clickEvent)); } }
With all of this, the component should still behave identically.