Service:
@Injectable({
providedIn: 'root'
})
export class DeckService{
private decks$ = new BehaviorSubject<Deck[]>([]);
constructor(private http: HttpClient) {}
public init(): void {
this.http.get<Deck[]>('/api/decks').subscribe((decks : Deck[]) => {
this.decks$.next(decks);
});
}
public getDecks(): Observable<Deck[]> {
return this.decks$.asObservable();
}
public getDeck(deck_id : number) : Observable<Deck> {
return this.decks$.pipe(
map(decks => decks.filter(deck => deck.deck_id === deck_id)[0])
);
}
}
Component:
public decks$ : Observable<Deck[]>;
public deckSelected$! : Observable<Deck | undefined>;
constructor(private deckService : DeckService) {
this.decks$ = this.deckService.getDecks();
this.deckService.init();
}
ngOnInit() {
this.deckSelected$ = this.deckService.getDecks().pipe(
map((decks: Deck[]) => decks.find(deck => deck.deck_id === 58))
);
}
}
HTML:
<div *ngIf="deckSelected$ | async as deck">
<h2>{{deck.deck_id}}</h2>
</div>
Deck Model:
export interface Deck {
deck_id: number;
name: string;
description: string;
}
So i want to get a card deck specified by a id. Therefore i want to first fetch all decks in an observable and then filter it.
Because using two endpoints ist overkill as i already have them loaded in the app.
However when running this it just gives me an blank page without any rendered items.
0
Generally the service contains only the http client API calls, the component will contain the subscribe part of the code, this is because you can control the order of execution of observable calls.
In the below service, there is no subscribe inside the methods, instead replaced with tap
to perform the side effects like setting the value for the BehaviorSubject
.
@Injectable({
providedIn: 'root'
})
export class DeckService{
private decks$ = new BehaviorSubject<Deck[]>([]);
constructor(private http: HttpClient) {}
public init() {
return this.http.get<Deck[]>('/api/decks').pipe(
tap((decks : Deck[]) => {
this.decks$.next(decks);
})
);
}
public getDecks(): Observable<Deck[]> {
return this.decks$.asObservable();
}
public getDeck(deck_id : number) : Observable<Deck> {
return this.decks$.pipe(
map(decks => decks.filter(deck => deck.deck_id === deck_id)[0])
);
}
}
In the controller, you can make the main api call that fetches the decks then just filter the returned value directly instead of relying on the behaviour subject.
public decks$ : Observable<Deck[]>;
public deckSelected$! : Observable<Deck | undefined>;
constructor(private deckService : DeckService) {
this.decks$ = this.deckService.getDecks();
this.deckSelected$ = this.deckService.init().pipe(
tap((decks: Deck[]) => decks.find(deck => deck.deck_id === 58)),
);
}
ngOnInit() {
this.deckSelected$ = this.deckService.getDecks().pipe(
map((decks: Deck[]) => decks.find(deck => deck.deck_id === 58))
);
}
}
2
Your code actually works, I’m assuming your test data doesn’t have an items where deck_id===58
.
Your service already has a method that does the filtering based on the deck_id
, so there’s no need to filter both in the component and in the service, so let’s put the filtering logic in only one place. A case could be made for either, but I’ll go with leaving it in the service.
Also, as Naren Murali, mentioned, generally it’s beneficial to only subscribe in your components, not in your services. This allows that your observables to remain lazy and only fetch data when there is a subscriber.
In your service, we can simplify by declaring decks$
as an observable, rather than use a Subject. This alleviates the need to subscribe.
export class DeckService{
public decks$ = this.http.get<Deck[]>('/api/decks').pipe(shareReplay(1));
constructor(private http: HttpClient) { }
public getDeck(deck_id: number): Observable<Deck|undefined> {
return this.decks$.pipe(
map(decks => decks.find(deck => deck.deck_id === deck_id))
);
}
}
Here we used shareReplay
to ensure only a single http call is made and also to reuse the previous result on future subscriptions.
The getDecks
method is no longer needed since you already have decks$
observable.
You can also simplify the component. It’s not necessary to split the declaration and initialization. We can do it all at once like this:
export class Component {
public decks$ = this.deckService.decks$;
public deckSelected$ = this.deckService.getDeck(58);
constructor(private deckService : DeckService) { }
}
Here’s a Stackblitz of this simplified code.
Of course you’ll want to remove the hardcoded id of 58
and make it changeable. This is a good place to use a Subject, which you could do like this:
private deckId$ = new Subject<number>();
public deckSelected$ = this.deckId$.pipe(
switchMap(id => this.deckService.getDeck(id))
);
public selectDeck(deck: Deck) {
this.deckId$.next(deck.deck_id);
}
Here’s a working Stackblitz