264

I am trying to implement something like a delegation pattern in Angular. When the user clicks on a nav-item, I would like to call a function which then emits an event which should in turn be handled by some other component listening for the event.

Here is the scenario: I have a Navigation component:

import {Component, Output, EventEmitter} from 'angular2/core';

@Component({
    // other properties left out for brevity
    events : ['navchange'], 
    template:`
      <div class="nav-item" (click)="selectedNavItem(1)"></div>
    `
})

export class Navigation {

    @Output() navchange: EventEmitter<number> = new EventEmitter();

    selectedNavItem(item: number) {
        console.log('selected nav item ' + item);
        this.navchange.emit(item)
    }

}

Here is the observing component:

export class ObservingComponent {

  // How do I observe the event ? 
  // <----------Observe/Register Event ?-------->

  public selectedNavItem(item: number) {
    console.log('item index changed!');
  }

}

The key question is, how do I make the observing component observe the event in question ?

yurzui
  • 171,085
  • 24
  • 365
  • 354
the_critic
  • 12,014
  • 19
  • 60
  • 109
  • According to the docs: > [The EventEmitter class extends Observable.](https://angular.io/guide/observables-in-angular) – halllo Jun 18 '18 at 12:30

7 Answers7

470

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.

Community
  • 1
  • 1
Mark Rajcok
  • 348,511
  • 112
  • 482
  • 482
  • 3
    It was mentioned repeatedly in comments to Angular2 issues that it's discouraged to use `EventEmitter` anywhere except for outputs. They are currently rewriting the tutorials (server communication AFAIR) to not encourage this practice. – Günter Zöchbauer Feb 23 '16 at 05:00
  • 1
    @GünterZöchbauer, thanks for the info. I can't keep up with SO, much less github issues. – Mark Rajcok Feb 23 '16 at 05:08
  • 1
    It's really hard too keep up with all the changes in Angular2. I just tried to strengthen your point :) – Günter Zöchbauer Feb 23 '16 at 05:36
  • @MarkRajcok one quick question.. how does change detection engine know that change has happened on observable object? can you explain that please **OR** provide me a reference link, I'll go through it. Thanks :) – Pankaj Parkar Feb 23 '16 at 16:48
  • I haven't had the time to verify/try/look (at) this, but I've ticked it for the sake of visibility, I will try that at some point. Thanks for your effort Mark. – the_critic Feb 23 '16 at 17:15
  • I really like this answer and it calls into question to me the 'deeper' answer with things like Flux (http://blog.jhades.org/angular-2-application-architecture-building-applications-using-rxjs-and-functional-reactive-programming-vs-redux/). I think this approach, coupled with events that can be quickly 'filtered', should allow performant global 'event bus' type behavior without the extra engineering (or horrible 'reduce' function using marker classes). – JimB Mar 02 '16 at 17:17
  • So I love this answer and I tried it and works like a charm. I do have a question on how would you best manage a lot of changes going on the UI. Would you create a separate observable service, like your example, for each one? Or would you just create one observable service that has an object that tracks all changes? – Marin Petkov Mar 09 '16 at 14:31
  • Here is the plnkr to what I was talking about in previous comment: http://plnkr.co/edit/2s0PaKBVTCzI5iBvZylU?p=preview – Marin Petkov Mar 09 '16 at 16:10
  • 1
    @MarinPetkov, "it depends". For app-wide events, I'd probably create one service with multiple observables. For the rest, I would first think about what services I want in my app, then I would add observables to those services that needed them. I would not architect from the other direction, i.e., start with the events, and then create services based on events. – Mark Rajcok Mar 09 '16 at 17:51
  • 2
    Is there a way initialize the service and fire off a first event from within the Navigation component in the sample code above? The problem is that the `_observer` of the service object is at least not initialized at the time of `ngOnInit()` of the Navigation component being called. – ComFreek Mar 23 '16 at 19:24
  • @ComFreek, yes, just wrap the changeNav() call in a setTimeout() to give the service a chance to create the `_observer`: `ngOnInit() { setTimeout(_ => this._navService.changeNav(3)); }` – Mark Rajcok Mar 23 '16 at 21:02
  • 5
    May I suggest using [BehaviorSubject](http://reactivex.io/rxjs/manual/overview.html#behaviorsubject) instead of Observable. It's closer to `EventEmitter` because It's "hot" meaning it's already "shared", it's designed to save the current value and finally it implements both Observable and Observer which will save you at least five lines of code and two properties – Abdulrahman Alsoghayer May 10 '16 at 01:56
  • @Abdulrahman, great suggestion. I'll try to update my answer sometime in the next few days (and use RC.1). – Mark Rajcok May 10 '16 at 03:03
  • 2
    @PankajParkar, regarding "how does change detection engine know that change has happened on observable object" -- I deleted my previous comment response. I learned recently that Angular does not monkey-patch `subscribe()`, so it can not detect when an observable changes. Normally, there is some async event that fires (in my sample code, it is the button click events), and the associated callback will call next() on an observable. But change detection runs because of the async event, not because of the observable change. See also Günter comments: http://stackoverflow.com/a/36846501/215945 – Mark Rajcok May 10 '16 at 03:16
  • 9
    If you want to wait until a value is actually produced you can use `ReplaySubject(1)`. A `BehaviorSubject` requires an initial value which will be provided immediatly. The `ReplaySubject(1)` will always provide the most recent value, but does not have an initial value required so the service can do some async operation before returning it's first value, but still fire immediately on subsequent calls with the last value. If you're just interested in one value you can use `first()` on the subscription and not have to unsubscribe at the end because that will complete. – Jason Goemaat Jun 27 '16 at 20:18
  • Wondering, Used on a service that inserts a form into database and was supposed to return the id created. `ngOnInit() { console.log('Log1'); this.createdRowSubscription = this.donorService.createdRowObservable$.subscribe( item => { console.log('Log2',item); this.selectRow(item); //makes it the selected row, loading the id (among the rest of data) from the store } ); }`. Apparently it works and does get called (because if I comment ngOnInit the form stops selecting the created row). However, Log2's line is never printed. ever. Log1 gets printed on Init as expected. – gia Aug 07 '16 at 22:48
  • @harunurhan, you might be inadvertently creating multiple instances of the server. Create a plunker and post a new question for help. – Mark Rajcok Aug 17 '16 at 14:48
  • @MarkRajcok regarding your comment starting with "it depends". Wouldn't having a service with everything and injected in a bunch of component perform worse ? – Ced Oct 22 '16 at 03:07
  • `asObservable()` doesn't seem to be required for this to work. Am I wrong? – Stephen Paul Nov 04 '16 at 06:12
  • 1
    any tutorial or article about this solution ? thanks for the answer :) – Francis Manoj Fernnado Dec 07 '16 at 06:23
  • @MarkRajcok love this example with the exception that both `Navigation` and `ObservingComponent` need to rely on `NavService`. How would I go about making `ObservingComponent` dumb so that the data I need to observe could be passed in via an `@Input()`? – GFoley83 Dec 12 '16 at 15:18
  • Never mind. Answer was in my own question. Passing the `navItem$` observable in as an `@Input()` and using the `async` pipe means all logic can be removed from `ObservingComponent` making it completely dumb. [Plunker example](http://plnkr.co/edit/Ft8JoPN5KWjYccK3lnll?p=preview) – GFoley83 Dec 12 '16 at 18:11
  • I made a video using this solution to send a search query from a NavBar component to a un-related Results component page. https://www.youtube.com/watch?v=k8hMfoNIo4Y – venturz909 Apr 03 '17 at 08:19
  • Note: If NavService is a singleton service provided to multiple components, make sure that NavService is NOT in the providers array of any subscribed components. This will result in multiple instances of NavService and the subscribers won't update correctly. Maybe that's obvious, but thought I'd share in case others encounter a similar problem. – Stevethemacguy Sep 13 '17 at 19:11
  • useless, it gets back to zero when page is refreshed – Diego Sep 19 '17 at 10:48
  • 1
    @Diego What do you expect? Refreshing the page is closing the app (ie. dumping everything from memory) and restarting the app from scratch. So everything gets reinitialized, that's why it's zero again. If you don't want it to be zero at the start, then you need to read an initial value from storage (either local or from a server) – rmcsharry Oct 24 '17 at 10:05
  • @MarkRajcok When the route changes, the subscription still works and causes unwanted behavior. For instance, when I go back to the routes, the function inside subscription happen twice – TSR Jan 21 '19 at 15:08
  • This might help some people now that rxjs has been updated and Subscription import no longer works: https://stackoverflow.com/questions/50176029/rxjs-subscription-has-no-exported-member-subscription – timhc22 Nov 05 '19 at 15:08
33

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 NavService

constructor(private navService:NavService) {  // in ObservingComponent
   this.subscription = this.navService.getNavChangeEmitter().subscribe(data =>
     this.selectedNavItem(data));
}
Community
  • 1
  • 1
Mark Rajcok
  • 348,511
  • 112
  • 482
  • 482
  • I prefer this solution to the answer provided by Mr Zouabi, but I am not a fan of this solution either, to be honest. I don't care about unsubscribing on destruction, but I do hate the fact that we have to pass the component to subscribe to the event... – the_critic Dec 21 '15 at 23:06
  • I actually thought about this and decided to go with this solution. I'd love to have a slightly cleaner solution, but I'm not sure it's possible (or I'm probably not able to come up with something that's more elegant I should say). – the_critic Dec 25 '15 at 13:14
  • actually the 2nd bullet problem is that a reference to the Function is being passed instead. to fix: `this.subscription = this.navService.subscribe(() => this.selectedNavItem());` and on subscribe: `return this.navchange.subscribe(callback);` – André Werlang Feb 15 '16 at 13:45
1

If one wants to follow a more Reactive oriented style of programming, then definitely the concept of "Everything is a stream" comes into picture and hence, use Observables to deal with these streams as often as possible.

Krishna Ganeriwal
  • 1,555
  • 15
  • 15
1

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 subject emitted 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.

khizer
  • 444
  • 5
  • 8
0

You need to use the Navigation component in the template of ObservingComponent ( dont't forget to add a selector to Navigation component .. navigation-component for ex )

<navigation-component (navchange)='onNavGhange($event)'></navigation-component>

And implement onNavGhange() in ObservingComponent

onNavGhange(event) {
  console.log(event);
}

Last thing .. you don't need the events attribute in @Componennt

events : ['navchange'], 
Mourad Zouabi
  • 2,137
  • 2
  • 13
  • 20
  • This only hooks up an event for the underlying component. That's not what I am trying to do. I could have just said something like (^navchange) (the caret is for event bubbling) on the `nav-item` but I just want to emit an event that others can observe. – the_critic Dec 20 '15 at 17:55
  • you can use navchange.toRx().subscribe() .. but you will need to have a reference on navchange on ObservingComponent – Mourad Zouabi Dec 20 '15 at 19:26
0

you can use BehaviourSubject as described above or there is one more way:

you can handle EventEmitter like this: first add a selector

import {Component, Output, EventEmitter} from 'angular2/core';

@Component({
// other properties left out for brevity
selector: 'app-nav-component', //declaring selector
template:`
  <div class="nav-item" (click)="selectedNavItem(1)"></div>
`
 })

 export class Navigation {

@Output() navchange: EventEmitter<number> = new EventEmitter();

selectedNavItem(item: number) {
    console.log('selected nav item ' + item);
    this.navchange.emit(item)
}

}

Now you can handle this event like let us suppose observer.component.html is the view of Observer component

<app-nav-component (navchange)="recieveIdFromNav($event)"></app-nav-component>

then in the ObservingComponent.ts

export class ObservingComponent {

 //method to recieve the value from nav component

 public recieveIdFromNav(id: number) {
   console.log('here is the id sent from nav component ', id);
 }

 }
Ashish Sharma
  • 539
  • 5
  • 12
-2

I found out another solution for this case without using Reactivex neither services. I actually love the rxjx API however I think it goes best when resolving an async and/or complex function. Using It in that way, Its pretty exceeded to me.

What I think you are looking for is for a broadcast. Just that. And I found out this solution:

<app>
  <app-nav (selectedTab)="onSelectedTab($event)"></app-nav>
       // This component bellow wants to know when a tab is selected
       // broadcast here is a property of app component
  <app-interested [broadcast]="broadcast"></app-interested>
</app>

 @Component class App {
   broadcast: EventEmitter<tab>;

   constructor() {
     this.broadcast = new EventEmitter<tab>();
   }

   onSelectedTab(tab) {
     this.broadcast.emit(tab)
   }    
 }

 @Component class AppInterestedComponent implements OnInit {
   broadcast: EventEmitter<Tab>();

   doSomethingWhenTab(tab){ 
      ...
    }     

   ngOnInit() {
     this.broadcast.subscribe((tab) => this.doSomethingWhenTab(tab))
   }
 }

This is a full working example: https://plnkr.co/edit/xGVuFBOpk2GP0pRBImsE

  • 1
    Look at the best answer, it uses subscribe method too.. Actually nowday I would recommend using Redux or some other state control for solving this communication problem between components. It is infinity much better than any other solution though it add extra complexity. Either using Angular 2 components event handler sintax or explicitly using subscribe method the concept remains the same. My final thoughts are if you want an definitive solution for that problem use Redux, otherwise use services with event emitter. – Nicholas Marcaccini Augusto Apr 14 '17 at 19:32
  • subscribe is valid as long as angular does not remove the fact that it's observable. .subscribe() is used in the best answer, but not on that particular object. – Porschiey Apr 20 '17 at 20:36