151

This is more of a "best practices" question. There are three players: a Component, a Service and a Model. The Component is calling the Service to get data from a database. The Service is using:

this.people = http.get('api/people.json').map(res => res.json());

to return an Observable.

The Component could just subscribe to the Observable:

    peopleService.people
        .subscribe(people => this.people = people);
      }

However, what I really want is for the Service to return an Array of Model objects that was created from the data that the Service retrieved from the database. I realized that the Component could just create this array in the subscribe method, but I think it would be cleaner if the service do that and make it available to the Component.

How can the Service create a new Observable, containing that array, and return that?

Michail Michailidis
  • 11,792
  • 6
  • 63
  • 106
Joseph Genchik
  • 1,821
  • 3
  • 15
  • 19

6 Answers6

170

UPDATE: 9/24/16 Angular 2.0 Stable

This question gets a lot of traffic still, so, I wanted to update it. With the insanity of changes from Alpha, Beta, and 7 RC candidates, I stopped updating my SO answers until they went stable.

This is the perfect case for using Subjects and ReplaySubjects

I personally prefer to use ReplaySubject(1) as it allows the last stored value to be passed when new subscribers attach even when late:

let project = new ReplaySubject(1);

//subscribe
project.subscribe(result => console.log('Subscription Streaming:', result));

http.get('path/to/whatever/projects/1234').subscribe(result => {
    //push onto subject
    project.next(result));

    //add delayed subscription AFTER loaded
    setTimeout(()=> project.subscribe(result => console.log('Delayed Stream:', result)), 3000);
});

//Output
//Subscription Streaming: 1234
//*After load and delay*
//Delayed Stream: 1234

So even if I attach late or need to load later I can always get the latest call and not worry about missing the callback.

This also lets you use the same stream to push down onto:

project.next(5678);
//output
//Subscription Streaming: 5678

But what if you are 100% sure, that you only need to do the call once? Leaving open subjects and observables isn't good but there's always that "What If?"

That's where AsyncSubject comes in.

let project = new AsyncSubject();

//subscribe
project.subscribe(result => console.log('Subscription Streaming:', result),
                  err => console.log(err),
                  () => console.log('Completed'));

http.get('path/to/whatever/projects/1234').subscribe(result => {
    //push onto subject and complete
    project.next(result));
    project.complete();

    //add a subscription even though completed
    setTimeout(() => project.subscribe(project => console.log('Delayed Sub:', project)), 2000);
});

//Output
//Subscription Streaming: 1234
//Completed
//*After delay and completed*
//Delayed Sub: 1234

Awesome! Even though we closed the subject it still replied with the last thing it loaded.

Another thing is how we subscribed to that http call and handled the response. Map is great to process the response.

public call = http.get(whatever).map(res => res.json())

But what if we needed to nest those calls? Yes you could use subjects with a special function:

getThing() {
    resultSubject = new ReplaySubject(1);

    http.get('path').subscribe(result1 => {
        http.get('other/path/' + result1).get.subscribe(response2 => {
            http.get('another/' + response2).subscribe(res3 => resultSubject.next(res3))
        })
    })
    return resultSubject;
}
var myThing = getThing();

But that's a lot and means you need a function to do it. Enter FlatMap:

var myThing = http.get('path').flatMap(result1 => 
                    http.get('other/' + result1).flatMap(response2 => 
                        http.get('another/' + response2)));

Sweet, the var is an observable that gets the data from the final http call.

OK thats great but I want an angular2 service!

I got you:

import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { ReplaySubject } from 'rxjs';

@Injectable()
export class ProjectService {

  public activeProject:ReplaySubject<any> = new ReplaySubject(1);

  constructor(private http: Http) {}

  //load the project
  public load(projectId) {
    console.log('Loading Project:' + projectId, Date.now());
    this.http.get('/projects/' + projectId).subscribe(res => this.activeProject.next(res));
    return this.activeProject;
  }

 }

 //component

@Component({
    selector: 'nav',
    template: `<div>{{project?.name}}<a (click)="load('1234')">Load 1234</a></div>`
})
 export class navComponent implements OnInit {
    public project:any;

    constructor(private projectService:ProjectService) {}

    ngOnInit() {
        this.projectService.activeProject.subscribe(active => this.project = active);
    }

    public load(projectId:string) {
        this.projectService.load(projectId);
    }

 }

I'm a big fan of observers and observables so I hope this update helps!

Original Answer

I think this is a use case of using a Observable Subject or in Angular2 the EventEmitter.

In your service you create a EventEmitter that allows you to push values onto it. In Alpha 45 you have to convert it with toRx(), but I know they were working to get rid of that, so in Alpha 46 you may be able to simply return the EvenEmitter.

class EventService {
  _emitter: EventEmitter = new EventEmitter();
  rxEmitter: any;
  constructor() {
    this.rxEmitter = this._emitter.toRx();
  }
  doSomething(data){
    this.rxEmitter.next(data);
  }
}

This way has the single EventEmitter that your different service functions can now push onto.

If you wanted to return an observable directly from a call you could do something like this:

myHttpCall(path) {
    return Observable.create(observer => {
        http.get(path).map(res => res.json()).subscribe((result) => {
            //do something with result. 
            var newResultArray = mySpecialArrayFunction(result);
            observer.next(newResultArray);
            //call complete if you want to close this stream (like a promise)
            observer.complete();
        });
    });
}

That would allow you do this in the component: peopleService.myHttpCall('path').subscribe(people => this.people = people);

And mess with the results from the call in your service.

I like creating the EventEmitter stream on its own in case I need to get access to it from other components, but I could see both ways working...

Here's a plunker that shows a basic service with an event emitter: Plunkr

dlsso
  • 7,209
  • 1
  • 21
  • 31
Dennis Smolek
  • 8,480
  • 7
  • 30
  • 39
  • I tried this approach but got "Cannot use 'new' with an expression whose type lacks a call or construct signature" -error. Anyone have an idea of what to do? – Spock Jan 15 '16 at 21:53
  • 3
    @Spock the spec has seemed to update since this original question. You no longer need the "new" for the observable as it does this for you. Simply remove the new and let me know what happens. I'm messing with some stuff now, if it works for you as well I'll update this answer – Dennis Smolek Jan 18 '16 at 17:39
  • 1
    Using `EventEmitter` for anything but `@Output()` is discouraged. See also http://stackoverflow.com/questions/34376854/delegation-eventemitter-or-observable-in-angular2/35568924 – Günter Zöchbauer Mar 28 '16 at 15:15
  • @GünterZöchbauer, Yes it is now... At the time it was going to be EventEmitters all over but they've since standardized on Rx Observables. My Observable example still works but if you were going to use the EventEmitter example I gave I suggest using Subjects directly: https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/subjects/subject.md – Dennis Smolek May 05 '16 at 23:45
  • 1
    @maxisam Thanks for the edit, although the answer was/is relative to the Alpha removing "new" for the Observable is correct now – Dennis Smolek Jun 15 '16 at 12:52
  • Hmmm, why I can't do this code let project = new ReplaySubject(1); project.subscribe(result => console.log('Subscription Streaming:', result)); into Service ? project is undefined in this case; – Eugene Shmorgun Oct 11 '16 at 08:22
  • @EugeneShmorgun you should be able to. It would be just like adding `this.activeProject.subscribe(result => console.log(result))` to the service. – Dennis Smolek Oct 11 '16 at 22:03
  • I tried to get the code running but i receive "Cannot read property 'next' of undefined" error at this line "this.http.get('/projects/' + projectId).subscribe(res => this.activeProject.next(res));" – miholzi Dec 06 '16 at 12:11
  • are you setting this.activeProject as a ReplaySubject? It's saying you haven't defined it... – Dennis Smolek Dec 06 '16 at 21:54
  • Just an FYI, you can use ` new BehaviorSubject(initialValue)` in place of `new ReplaySubject(1)`, but just be aware that a `BehaviorSubject` requires an initial value – gonzofish Jul 19 '17 at 20:13
  • What about the subscription in the service, how would we ensure that gets closed? Wouldn't this approach just potentially keep tons of subscriptions open inside services? – Hanna Nov 20 '17 at 19:43
  • @Johannes, In the examples I gave I used an AsyncSubject which closes, the http's close automatically as well. Yes, you leave open a stream but that's the point anyway. You want to be able to pull from it at any time. – Dennis Smolek Nov 22 '17 at 12:34
  • Why load() function in ProjectService needs to return the Subject ? #JustCurious – BeingSuman Mar 30 '18 at 10:16
  • @BeingSuman because we don't know when exactly the call will return. You could return the call itself but I generally like to return the main thread. – Dennis Smolek Apr 04 '18 at 01:39
31

This is an example from Angular2 docs of how you can create and use your own Observables :

The Service

import {Injectable} from 'angular2/core'
import {Subject}    from 'rxjs/Subject';
@Injectable()
export class MissionService {
  private _missionAnnouncedSource = new Subject<string>();
  missionAnnounced$ = this._missionAnnouncedSource.asObservable();

  announceMission(mission: string) {
    this._missionAnnouncedSource.next(mission)
  }
}

The Component

    import {Component}          from 'angular2/core';
    import {MissionService}     from './mission.service';

    export class MissionControlComponent {
      mission: string;

      constructor(private missionService: MissionService) {

        missionService.missionAnnounced$.subscribe(
          mission => {
            this.mission = mission;
          })
      }

      announce() {
        this.missionService.announceMission('some mission name');
      }
    }

Full and working example can be found here : https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#bidirectional-service

Chrillewoodz
  • 27,055
  • 21
  • 92
  • 175
Tiberiu Popescu
  • 4,486
  • 2
  • 26
  • 38
23

I would like to add that if the object that is created is static and not coming through http something like that can be done:

public fetchModel(uuid: string = undefined): Observable<string> {
      if(!uuid) { //static data
        return Observable.of(new TestModel()).map(o => JSON.stringify(o));
      }
      else {
        return this.http.get("http://localhost:8080/myapp/api/model/" + uuid)
                .map(res => res.text());
      }
    }

Edit: For Angular 7.x.x mapping needs to be done using pipe() as described here (https://stackoverflow.com/a/54085359/986160):

import {of,  Observable } from 'rxjs';
import { map } from 'rxjs/operators';
[...]
public fetchModel(uuid: string = undefined): Observable<string> {
      if(!uuid) { //static data
        return of(new TestModel());
      }
      else {
        return this.http.get("http://localhost:8080/myapp/api/model/" + uuid)
                .pipe(map((res:any) => res)) //already contains json
      }
    }

from answer to my question about observers and static data: https://stackoverflow.com/a/35219772/986160

Michail Michailidis
  • 11,792
  • 6
  • 63
  • 106
18

I'm a little late to the party, but I think my approach has the advantage that it lacks the use of EventEmitters and Subjects.

So, here's my approach. We can't get away from subscribe(), and we don't want to. In that vein, our service will return an Observable<T> with an observer that has our precious cargo. From the caller, we'll initialize a variable, Observable<T>, and it will get the service's Observable<T>. Next, we'll subscribe to this object. Finally, you get your "T"! from your service.

First, our people service, but yours doesnt pass parameters, that's more realistic:

people(hairColor: string): Observable<People> {
   this.url = "api/" + hairColor + "/people.json";

   return Observable.create(observer => {
      http.get(this.url)
          .map(res => res.json())
          .subscribe((data) => {
             this._people = data

             observer.next(this._people);
             observer.complete();


          });
   });
}

Ok, as you can see, we're returning an Observable of type "people". The signature of the method, even says so! We tuck-in the _people object into our observer. We'll access this type from our caller in the Component, next!

In the Component:

private _peopleObservable: Observable<people>;

constructor(private peopleService: PeopleService){}

getPeople(hairColor:string) {
   this._peopleObservable = this.peopleService.people(hairColor);

   this._peopleObservable.subscribe((data) => {
      this.people = data;
   });
}

We initialize our _peopleObservable by returning that Observable<people> from our PeopleService. Then, we subscribe to this property. Finally, we set this.people to our data(people) response.

Architecting the service in this fashion has one, major advantage over the typical service: map(...) and component: "subscribe(...)" pattern. In the real world, we need to map the json to our properties in our class and, sometimes, we do some custom stuff there. So this mapping can occur in our service. And, typically, because our service call will be used not once, but, probably, in other places in our code, we don't have to perform that mapping in some component, again. Moreover, what if we add a new field to people?....

LargeDachshund
  • 916
  • 3
  • 12
  • 25
  • I agree that the formatting should be in the service and I posted a standard Observable method too but the advantage of Subjects in a service is that other functions can trigger on it. If you're always only needing the direct http call then I'd use the Observable method.. – Dennis Smolek Jun 15 '16 at 13:05
11

In the service.ts file -

a. import 'of' from observable/of
b. create a json list
c. return json object using Observable.of()
Ex. -

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';

@Injectable()
export class ClientListService {
    private clientList;

    constructor() {
        this.clientList = [
            {name: 'abc', address: 'Railpar'},
            {name: 'def', address: 'Railpar 2'},
            {name: 'ghi', address: 'Panagarh'},
            {name: 'jkl', address: 'Panagarh 2'},
        ];
    }

    getClientList () {
        return Observable.of(this.clientList);
    }
};

In the component where we are calling the get function of the service -

this.clientListService.getClientList().subscribe(res => this.clientList = res);
symfonydevpl
  • 567
  • 1
  • 6
  • 20
Anirban Bhadra
  • 221
  • 3
  • 5
7

Notice that you're using Observable#map to convert the raw Response object your base Observable emits to a parsed representation of the JSON response.

If I understood you correctly, you want to map again. But this time, converting that raw JSON to instances of your Model. So you would do something like:

http.get('api/people.json')
  .map(res => res.json())
  .map(peopleData => peopleData.map(personData => new Person(personData)))

So, you started with an Observable that emits a Response object, turned that into an observable that emits an object of the parsed JSON of that response, and then turned that into yet another observable that turned that raw JSON into an array of your models.

julioolvr
  • 469
  • 4
  • 11