Reputation: 981
I'm trying to learn the reactive development in Angular by converting my old methods in a service to NOT subscribe at all in an Observable. So far, the simplest method that I already converted was the getAll method. What I want now is that, when I selected a single card in the card list, I will be redirected to another page to view it's details. Consider the below code that I'm currently working.
SERVICE
@Injectable({
'providedIn': 'root'
})
export class CardService {
private selectedCardIdSubject = new Subject<number>();
selectedCardIdAction$ = this.selectedCardIdSubject.asObservable();
/**
* Retrieve single card based on card id.
*/
card$ = this.selectedCardIdAction$
.pipe(
tap(id => console.log(`card$: ${this._cardsApi}/${id}`)),
concatMap(id =>
this._http.get<Card>(`${this._cardsApi}/${id}`)
.pipe(
map(card => ({
...card,
imageUrl: `${this._cardImageApi}/${card.idName}.png`
}))
)
),
catchError(this.handleError)
);
onSelectedCardId(id: number) {
this.selectedCardIdSubject.next(id);
console.log(`onSelectedCardId: ${id}`)
}
}
COMPONENT
@Component({})
export class CardDetailsComponent {
constructor(private _cardService: CardService,
private route: ActivatedRoute) {
this.selectedCardId = this.route.snapshot.params['id'];
this._cardService.onSelectedCardId(this.selectedCardId);
}
card$ = this._cardService.card$;
}
HTML
<div class="container mb-5" *ngIf="card$ | async as card">
<p>{{ card.name }}</>
</div>
In the code above, when redirected, it does not render my simple HTML to show the name of the selected card. I'm using a [routerLink]
to redirect in the card detail page.
Card List Component
<div *ngIf="cards$ | async as cards">
<div class="col-md-1 col-4 hover-effect" *ngFor='let card of cards'>
<a class="d-block mb-1">
<figure>
<img class="img-fluid img-thumbnail" [src]='card.imageUrl' [title]='card.name' [alt]='card.name'
[routerLink]="['/cards', card.id]" />
</figure>
</a>
</div>
</div>
Anyone who can enlighten me on what happen here?
Upvotes: 2
Views: 1599
Reputation: 1783
I think there is a second gotcha:
In your component code, where you access the routing parameters via the snapshot: this.route.snapshot.params['id']
Since the constructor is called only once, even if the parameter changes, I doubt that your solution is working when the selected card is changed.
Best practice would be to subscribe to the routing parameters via RxJs. To avoid a unecessarry subscription we can use the higher order observable operator mergeMap
to load a single card entry everytime the route param changes
@Component({
selector: "card",
template: `
CARD_DETAIL
<div *ngIf="card$ | async; let card">Test {{ card.name }}</div>
`
})
export class CardComponent {
card$;
constructor(private cardService: CardService, private route: ActivatedRoute) {
this.card$ = this.route.params.pipe(
map(getIdParameter),
mergeMap(this.cardService.loadSingleCard)
);
}
}
const getParameter = param => params => params[param];
const getIdParameter = getParameter("id");
This would also clean up your service and reduce it to one BehaviorSubject
:
@Injectable()
export class CardService {
card$ = new BehaviorSubject<Card>(null);
constructor(private httpClient: HttpClient) {}
loadSingleCard = (id: number) => {
const url = `https://api.punkapi.com/v2/beers/${id}`;
this.httpClient
.get<Card>(url)
.pipe(map(cards => cards[0]))
.subscribe(card => this.card$.next(card));
return this.card$;
};
}
Edit:
The following is a so called "Arrow function expression" that uses a functional approach, which is called "Partial application" (afaik). In our case it is a function that takes a specific param and returns another function that takes the whole params object. When the returned function is called we finally are able to return the specified param from the params object.
const getParameter = param => params => params[param];
is the same as:
const getParameter = function(param) {
return function(params) {
params[param];
};
}
const getIdParameter = getParameter("id");
// Could be called like that aswell:
const params = { id: 1, name: 'chris'};
const idParam = getParameter("id")(params); // hence the name partial application, i guess ;)
I agree that it does looks fancy, but now we are able to pass the getIdParameter
function reference into the map(getParameterdId)
, which is basically a shorter syntax as: map(params => getParameterId(params)
or map(params => getParameters(params, 'id'))
Cheers Chris
Upvotes: 1
Reputation: 11345
The reason it's not working is your $card is subscribed after the emission is already fired in your constructor and at this time the view is not rendered yet meaning aysnc pipe is not in place.
The scenario is very much like the below code order, Therefore you received no update
// constructor
const a=new Subject()
a.next('some value')
// rendering
a.subscribe(console.log) // never triggered
There are two approaches to tackle this issue:
1st cache the latest value with shareReplay(1)
, so it always emitted the last value when subscribed
selectedCardIdAction$ = this.selectedCardIdSubject.asObservable().pipe(shareReplay(1));
or use BehaviorSubject
private selectedCardIdSubject = new BehaviorSubject<number | null>(null);
.next()
to ngAfterViewInit - it fire after the view is created and async pipe subscribedPersonally i will go for the 1st approach, the result is more consistent if your code doesn't have much flaw
Upvotes: 4