Delegation: EventEmitter or Observable in Angular Delegation: EventEmitter or Observable in Angular angular angular

Delegation: EventEmitter or Observable in Angular


Update 2016-06-27: instead of using Observables, use either

  • a BehaviorSubject, as recommended by @Abdulrahman in a comment, or
  • a ReplaySubject, as recommended by @Jason Goemaat in a comment

A Subject is both an Observable (so we can subscribe() to it) and an Observer (so we can call next() on it to emit a new value). We exploit this feature. A Subject allows values to be multicast to many Observers. We don't exploit this feature (we only have one Observer).

BehaviorSubject is a variant of Subject. It has the notion of "the current value". We exploit this: whenever we create an ObservingComponent, it gets the current navigation item value from the BehaviorSubject automatically.

The code below and the plunker use BehaviorSubject.

ReplaySubject is another variant of Subject. If you want to wait until a value is actually produced, use ReplaySubject(1). Whereas a BehaviorSubject requires an initial value (which will be provided immediately), ReplaySubject does not. ReplaySubject will always provide the most recent value, but since it does not have a required initial value, the service can do some async operation before returning it's first value. It will still fire immediately on subsequent calls with the most recent value. If you just want one value, use first() on the subscription. You do not have to unsubscribe if you use first().

import {Injectable}      from '@angular/core'import {BehaviorSubject} from 'rxjs/BehaviorSubject';@Injectable()export class NavService {  // Observable navItem source  private _navItemSource = new BehaviorSubject<number>(0);  // Observable navItem stream  navItem$ = this._navItemSource.asObservable();  // service command  changeNav(number) {    this._navItemSource.next(number);  }}
import {Component}    from '@angular/core';import {NavService}   from './nav.service';import {Subscription} from 'rxjs/Subscription';@Component({  selector: 'obs-comp',  template: `obs component, item: {{item}}`})export class ObservingComponent {  item: number;  subscription:Subscription;  constructor(private _navService:NavService) {}  ngOnInit() {    this.subscription = this._navService.navItem$       .subscribe(item => this.item = item)  }  ngOnDestroy() {    // prevent memory leak when component is destroyed    this.subscription.unsubscribe();  }}
@Component({  selector: 'my-nav',  template:`    <div class="nav-item" (click)="selectedNavItem(1)">nav 1 (click me)</div>    <div class="nav-item" (click)="selectedNavItem(2)">nav 2 (click me)</div>`})export class Navigation {  item = 1;  constructor(private _navService:NavService) {}  selectedNavItem(item: number) {    console.log('selected nav item ' + item);    this._navService.changeNav(item);  }}

Plunker


Original answer that uses an Observable: (it requires more code and logic than using a BehaviorSubject, so I don't recommend it, but it may be instructive)

So, here's an implementation that uses an Observable instead of an EventEmitter. Unlike my EventEmitter implementation, this implementation also stores the currently selected navItem in the service, so that when an observing component is created, it can retrieve the current value via API call navItem(), and then be notified of changes via the navChange$ Observable.

import {Observable} from 'rxjs/Observable';import 'rxjs/add/operator/share';import {Observer} from 'rxjs/Observer';export class NavService {  private _navItem = 0;  navChange$: Observable<number>;  private _observer: Observer;  constructor() {    this.navChange$ = new Observable(observer =>      this._observer = observer).share();    // share() allows multiple subscribers  }  changeNav(number) {    this._navItem = number;    this._observer.next(number);  }  navItem() {    return this._navItem;  }}@Component({  selector: 'obs-comp',  template: `obs component, item: {{item}}`})export class ObservingComponent {  item: number;  subscription: any;  constructor(private _navService:NavService) {}  ngOnInit() {    this.item = this._navService.navItem();    this.subscription = this._navService.navChange$.subscribe(      item => this.selectedNavItem(item));  }  selectedNavItem(item: number) {    this.item = item;  }  ngOnDestroy() {    this.subscription.unsubscribe();  }}@Component({  selector: 'my-nav',  template:`    <div class="nav-item" (click)="selectedNavItem(1)">nav 1 (click me)</div>    <div class="nav-item" (click)="selectedNavItem(2)">nav 2 (click me)</div>  `,})export class Navigation {  item:number;  constructor(private _navService:NavService) {}  selectedNavItem(item: number) {    console.log('selected nav item ' + item);    this._navService.changeNav(item);  }}

Plunker


See also the Component Interaction Cookbook example, which uses a Subject in addition to observables. Although the example is "parent and children communication," the same technique is applicable for unrelated components.


Breaking news: I've added another answer that uses an Observable rather than an EventEmitter. I recommend that answer over this one. And actually, using an EventEmitter in a service is bad practice.


Original answer: (don't do this)

Put the EventEmitter into a service, which allows the ObservingComponent to directly subscribe (and unsubscribe) to the event:

import {EventEmitter} from 'angular2/core';export class NavService {  navchange: EventEmitter<number> = new EventEmitter();  constructor() {}  emit(number) {    this.navchange.emit(number);  }  subscribe(component, callback) {    // set 'this' to component when callback is called    return this.navchange.subscribe(data => call.callback(component, data));  }}@Component({  selector: 'obs-comp',  template: 'obs component, index: {{index}}'})export class ObservingComponent {  item: number;  subscription: any;  constructor(private navService:NavService) {   this.subscription = this.navService.subscribe(this, this.selectedNavItem);  }  selectedNavItem(item: number) {    console.log('item index changed!', item);    this.item = item;  }  ngOnDestroy() {    this.subscription.unsubscribe();  }}@Component({  selector: 'my-nav',  template:`    <div class="nav-item" (click)="selectedNavItem(1)">item 1 (click me)</div>  `,})export class Navigation {  constructor(private navService:NavService) {}  selectedNavItem(item: number) {    console.log('selected nav item ' + item);    this.navService.emit(item);  }}

If you try the Plunker, there are a few things I don't like about this approach:

  • ObservingComponent needs to unsubscribe when it is destroyed
  • we have to pass the component to subscribe() so that the proper this is set when the callback is called

Update: An alternative that solves the 2nd bullet is to have the ObservingComponent directly subscribe to the navchange EventEmitter property:

constructor(private navService:NavService) {   this.subscription = this.navService.navchange.subscribe(data =>     this.selectedNavItem(data));}

If we subscribe directly, then we wouldn't need the subscribe() method on the NavService.

To make the NavService slightly more encapsulated, you could add a getNavChangeEmitter() method and use that:

getNavChangeEmitter() { return this.navchange; }  // in NavServiceconstructor(private navService:NavService) {  // in ObservingComponent   this.subscription = this.navService.getNavChangeEmitter().subscribe(data =>     this.selectedNavItem(data));}


You can use either:

  1. Behaviour Subject:

BehaviorSubject is a type of subject, a subject is a special type of observable which can act as observable and observer you can subscribe to messages like any other observable and upon subscription, it returns the last value of the subjectemitted by the source observable:

Advantage: No Relationship such as parent-child relationship required to pass data between components.

NAV SERVICE

import {Injectable}      from '@angular/core'import {BehaviorSubject} from 'rxjs/BehaviorSubject';@Injectable()export class NavService {  private navSubject$ = new BehaviorSubject<number>(0);  constructor() {  }  // Event New Item Clicked  navItemClicked(navItem: number) {    this.navSubject$.next(number);  } // Allowing Observer component to subscribe emitted data only  getNavItemClicked$() {   return this.navSubject$.asObservable();  }}

NAVIGATION COMPONENT

@Component({  selector: 'navbar-list',  template:`    <ul>      <li><a (click)="navItemClicked(1)">Item-1 Clicked</a></li>      <li><a (click)="navItemClicked(2)">Item-2 Clicked</a></li>      <li><a (click)="navItemClicked(3)">Item-3 Clicked</a></li>      <li><a (click)="navItemClicked(4)">Item-4 Clicked</a></li>    </ul>})export class Navigation {  constructor(private navService:NavService) {}  navItemClicked(item: number) {    this.navService.navItemClicked(item);  }}

OBSERVING COMPONENT

@Component({  selector: 'obs-comp',  template: `obs component, item: {{item}}`})export class ObservingComponent {  item: number;  itemClickedSubcription:any  constructor(private navService:NavService) {}  ngOnInit() {    this.itemClickedSubcription = this.navService                                      .getNavItemClicked$                                      .subscribe(                                        item => this.selectedNavItem(item)                                       );  }  selectedNavItem(item: number) {    this.item = item;  }  ngOnDestroy() {    this.itemClickedSubcription.unsubscribe();  }}

Second Approach is Event Delegation in upward direction child -> parent

  1. Using @Input and @Output decorators parent passing data to child component and child notifying parent component

e.g Answered given by @Ashish Sharma.