Delegation: EventEmitter or Observable in Angular
Asked Answered
G

7

282

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 ?

Groundnut answered 20/12, 2015 at 0:32 Comment(1)
According to the docs: > The EventEmitter class extends Observable.Cachepot
W
491

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.

Windpipe answered 23/2, 2016 at 4:37 Comment(29)
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.Canary
@GünterZöchbauer, thanks for the info. I can't keep up with SO, much less github issues.Windpipe
It's really hard too keep up with all the changes in Angular2. I just tried to strengthen your point :)Canary
@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 :)Breeden
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.Groundnut
I really like this answer and it calls into question to me the 'deeper' answer with things like Flux (blog.jhades.org/…). 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).Britten
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?Nihilism
Here is the plnkr to what I was talking about in previous comment: plnkr.co/edit/2s0PaKBVTCzI5iBvZylU?p=previewNihilism
@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.Windpipe
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.Warrant
@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)); }Windpipe
May I suggest using 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 propertiesLucindalucine
@Abdulrahman, great suggestion. I'll try to update my answer sometime in the next few days (and use RC.1).Windpipe
@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: https://mcmap.net/q/49340/-increase-performance-on-angular2-inputfieldWindpipe
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.Wattage
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.Sheeran
@harunurhan, you might be inadvertently creating multiple instances of the server. Create a plunker and post a new question for help.Windpipe
@MarkRajcok regarding your comment starting with "it depends". Wouldn't having a service with everything and injected in a bunch of component perform worse ?Callen
asObservable() doesn't seem to be required for this to work. Am I wrong?Lurlene
any tutorial or article about this solution ? thanks for the answer :)Digestif
@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()?Whitehouse
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 exampleWhitehouse
I made a video using this solution to send a search query from a NavBar component to a un-related Results component page. youtube.com/watch?v=k8hMfoNIo4YOyler
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.Sighted
useless, it gets back to zero when page is refreshedForwardlooking
@Forwardlooking 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)Theodoratheodore
@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 twiceDerril
This might help some people now that rxjs has been updated and Subscription import no longer works: #50176529Execration
Why using navItem$ in NavService? It looks unnecessaryTimothytimour
W
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));
}
Windpipe answered 21/12, 2015 at 18:51 Comment(3)
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...Groundnut
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).Groundnut
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);Gosney
P
3

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.

Pellet answered 29/4, 2019 at 11:56 Comment(0)
T
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.

Touching answered 18/8, 2017 at 4:37 Comment(0)
C
1

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);
 }

 }
Corrosive answered 5/10, 2017 at 9:43 Comment(0)
K
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'], 
Keare answered 20/12, 2015 at 10:23 Comment(2)
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.Groundnut
you can use navchange.toRx().subscribe() .. but you will need to have a reference on navchange on ObservingComponentKeare
I
-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

Intermediate answered 13/12, 2016 at 12:51 Comment(2)
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.Intermediate
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.Kaczmarek

© 2022 - 2024 — McMap. All rights reserved.