8

I am emitting an event in my main component:

main.component.ts

this.sharedService.cartData.emit(this.data);

Here is my sharedService.ts

import { Component, Injectable, EventEmitter } from '@angular/core';
export class SharedService {
    cartData = new EventEmitter<any>();
} 

In my other (Sub) Component, I want to access this value, but somehow, the subscription does not work:

dashboard.ts

private myData: any;

constructor(private sharedService: SharedService) {
    this.sharedService.cartData.subscribe(
        (data: any) => myData = data,
        error => this.errorGettingData = <any>error,
        () => this.aggregateData(this.myData));
}

Am I missing something? It works fine when I pass the data as an Injectable. Emitting the event (in the main component) happens after some REST calls.

Update

So the problem is that the Subcomponent is created after the first emit of the event. I guess in this case it is better to inject the data into the subcompnent directly.

Chris Stillwell
  • 10,266
  • 10
  • 67
  • 77
Stef
  • 644
  • 1
  • 8
  • 25
  • 1
    where are you providing the sharedService? it is possible that the instances of service are not same, it happens if you provide service in different modules. – Madhu Ranjan Jul 27 '17 at 13:10
  • I declared it in the parent app.module.ts providers: [SharedService ...], – Stef Jul 27 '17 at 13:12
  • and how are both components loaded at the same time? In differet routes? – Madhu Ranjan Jul 27 '17 at 13:14
  • Did you decorate the service with `@Injectable()`? – jacekbj Jul 27 '17 at 13:18
  • For main.components.ts I do use a route, but the dashboard component is created in the main.component.html as a selector (). – Stef Jul 27 '17 at 13:19
  • @dzejdzej: I started of by using @Injectable() but I thought using an event emitter might be the better idea (even if the Injectable way is working) ... so right now the shared.service.ts from my question is the actual file – Stef Jul 27 '17 at 13:20
  • @Stef You need to decorate the service, otherwise I'm pretty sure it won't be shared between components. I would also move the code making requests to the service and have components subscribe to the results of requests (if you're not using a store, i.e. ngrx-store). – jacekbj Jul 27 '17 at 13:23
  • Same thing with the @Injectable ... the data is still "undefined" ... I will keep digging. – Stef Jul 27 '17 at 14:14
  • Actually the data is undefined because the listener does not fire at all – Stef Jul 27 '17 at 14:21
  • Ok so the event fires when I click on the screen (after everyhting is loaded). Which is strange since I fire the event at the very end of everyhting else, but it somehow does not get to the other component. – Stef Jul 27 '17 at 14:30
  • Please see the correct way to implement a shared service: https://angular.io/guide/component-interaction#parent-and-children-communicate-via-a-service It's not recommended to use eventemitter to share data in service: https://stackoverflow.com/questions/36076700/what-is-the-proper-use-of-an-eventemitter – AT82 Jul 28 '17 at 13:34
  • Alright thanks for the link – Stef Jul 28 '17 at 17:58

2 Answers2

10

Update: Plunker example no longer maintained please use StackBlitz example here https://stackblitz.com/edit/stackoverflow-questions-45351598-angular?file=src%2Fapp%2Fapp.component.ts

I have created a working plunker example using the code you provided above. https://plnkr.co/edit/LS1uqB?p=preview

import { Component, NgModule, Injectable, EventEmitter, AfterViewInit } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';


@Injectable()
export class SharedService {
    cartData = new EventEmitter<any>();
} 

@Component({
  selector: 'app-app',
  template: `
    <h1>
      Main Component <button (click)="onEvent()">onEvent</button>
    </h1>
    <p>
      <app-dashboard></app-dashboard>
    </p>
  `,
})
export class App implements AfterViewInit {
  data: any = "Shared Data";

  constructor(private sharedService: SharedService) {
  }

  ngAfterViewInit() {
    this.sharedService.cartData.emit("ngAfterViewInit: " + this.data);
  }

  onEvent() {
    this.sharedService.cartData.emit("onEvent: " + this.data);
  }
}

@Component({
  selector: 'app-dashboard',
  template: `
    <h2>
      Dashboard component
    </h2>
    <p>
      {{myData}}
    </p>
  `,
})
export class AppDashboard implements AfterViewInit {
  myData: any;

  constructor(private sharedService: SharedService) {
          this.sharedService.cartData.subscribe(
          (data: any) => {
            console.log(data);
            this.myData = data;
          });
  }

}


@NgModule({
  imports: [ BrowserModule ],
  declarations: [ App, AppDashboard ],
  providers: [ SharedService ],
  bootstrap: [ App ]
})
export class AppModule {}

View lifecycle hooks here https://angular.io/guide/lifecycle-hooks

J J B
  • 8,540
  • 1
  • 28
  • 42
  • This works fine, since it works with a click listener. I am trying to figure out which function I have to use when I emit the event in my code. – Stef Jul 27 '17 at 14:32
  • It also works with the ngAfterViewInit, which is part of the angular lifecycle hooks. Check out https://angular.io/guide/lifecycle-hooks – J J B Jul 27 '17 at 14:39
  • I changed my version to ngAfterViewInit and it works when I click somewhere. When the App is initially loaded, the sub component loads after the first event is fired, since I declare it in my main.component.html. 1) Emitting Event 2) Dashboard Constructor 3) Dashboard AfterViewInit – Stef Jul 27 '17 at 14:51
  • Could you elaborate what you mean by it works when you click somewhere? If it's not functioning how intended, please make a plunker example and I'll be happy to try and resolve the issue. – J J B Jul 27 '17 at 15:13
  • Yeah sorry, by clicking somewhere I meant if I emit the event on an event after the page has loaded. But as I described in my udpated post, I think in my case it makes more sense to inject the data than emitting it. – Stef Jul 28 '17 at 17:59
1

Try this:

export class SharedService {
    private dataPusher = new Subject<any>();
    cartData = dataPusher.asObservable().pipe(shareReplay(1));

    pushData(value:any) {
       this.dataPusher.next(value);
    }
} 

What that does is that it will replay the last emitted value for the "late" subscriber. If you want to have a initial value being emitted, you could use BehaviourSubject - it takes initial value in the constructor.

Or you could pipe/chain it with startWith operator.

cartData = dataPusher.asObservable().pipe(startWith("someValue"), shareReplay(1));
dK-
  • 582
  • 3
  • 12