I'm having a situation with Angular where a routing data resolver seems like it's poised to return data correctly, but then resolution never happens. It's especially odd because I have a parallel arrangement for another component and it's working fine there.
The application retrieves data about an array of events via HTTP. In an EventListComponent
, all the events are returned by the resolver in response to /events
, and the component properly displays them. In an EventDetails
component, in my current arrangement, I'm still retrieving all the events via HTTP and then, in a resolver in response to /events/[the event ID]
, selecting the event that should have its details displayed. (This is from the Pluralsight Angular Fundamentals course, in case it sounds familiar. But I tend to watch the videos and then work through them in my own order to try to consolidate the skills in my head.)
remote-event.service.ts
import { Injectable, EventEmitter } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { IEvent } from './event.model';
@Injectable()
export class RemoteEventService {
constructor(
private http: HttpClient
) {}
getEvents(): Observable<IEvent[]> {
return this.http.get<IEvent[]>('/api/ngfdata/events.json');
}
getEventById(id: number): Observable<IEvent> {
console.log(`In getEventById: id = ${id}`);
const emitter = new EventEmitter<IEvent>();
this.getEvents().subscribe(
(events) => {
emitter.emit(events.find((event) => event.id === id));
}
);
return emitter;
}
export interface ISessionSearchResult {
eventId: number;
sessionId: number;
sessionName: string;
}
If I don't use the resolver, the EventDetails Component works fine. This works:
eventRoutes (this is a child route branching off from /events/)
import { Routes } from '@angular/router';
import { EventListComponent, EventDetailsComponent,
CreateEventComponent, UnsavedNewEventGuard,
EventListResolver, EventDetailResolver
} from './index';
export const eventRoutes: Routes = [
{ path: 'create', component: CreateEventComponent,
canDeactivate: [UnsavedNewEventGuard]
},
{ path: ':id', component: EventDetailsComponent/*,
resolve: { event: EventDetailResolver }*/
},
{ path: '', component: EventListComponent,
resolve: { events: EventListResolver }
}
];
event-details.component.ts
import { Component, Input, OnInit, inject, Inject } from '@angular/core';
import { RemoteEventService } from '../shared/remote-event.service';
import { ActivatedRoute, Params } from '@angular/router';
import { IEvent } from '../shared/event.model';
import { TOASTR_TOKEN } from 'src/app/common/3rd-party/toastr.service';
@Component(
{
selector: 'event-detail',
templateUrl: './event-details.component.html',
styles: [`
.container { padding-left: 20px; padding-right: 20px; }
.event-image { height: 100px; }
.btn-group:first-child {
margin-right: 24px;
}
.btn-group {
border: medium solid green;
}
.btn-group .btn:not(:first-child) {
border-left: thin solid green;
}
`]
}
)
export class EventDetailsComponent implements OnInit {
event: IEvent;
filterBy = 'all';
sortBy = 'name';
constructor(
private eventService: RemoteEventService,
private route: ActivatedRoute,
@Inject(TOASTR_TOKEN) private toast
) {
console.log('In EventDetailsComponent.constructor');
}
/* ngOnInit() {
console.log('At start of EventDetailsComponent.ngOnInit');
this.event = this.route.snapshot.data['event'];
console.log('At end of EventDetailsComponent.ngOnInit');
}
*/
ngOnInit() {
console.log('At start of EventDetailsComponent.ngOnInit');
this.route.params.forEach((params: Params) => {
this.eventService.getEventById(+params.id).subscribe(
(event) => this.event = event
);
});
console.log('At end of EventDetailsComponent.ngOnInit');
}
flashSessionSummary(message: string) {
this.toast.info(message);
}
}
When I uncomment the resolver reference in the routing list above, and switch which of the two copies of ngOnInit is commented out in the component code above, nothing is displayed other than the navigation bar at the top.
I have route tracing enabled. Without using the resolver:
With the resolver active:
Here's the resolver:
event-detail-resolver.service.ts
import { Injectable, Input } from '@angular/core';
import { RemoteEventService } from '../shared/remote-event.service';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
import { IEvent } from '../shared/event.model';
@Injectable()
export class EventDetailResolver implements Resolve<IEvent> {
constructor(
private eventService: RemoteEventService
) {}
resolve(route: ActivatedRouteSnapshot) {
console.log(`In resolve(): id = ${route.params.id}`);
const e = this.eventService.getEventById(+route.params.id);
console.log(`The observable that resolve() is about to return: ${JSON.stringify(e)}`);
e.subscribe((evt) => console.log(`The value that the observable resolves to: ${JSON.stringify(evt)}`));
return e;
}
}
As you can see, before returning the Observable, I subscribe to it so I can demonstrate here within the resolver the value that it will resolve to--and it's the correct event object value. Lest you say that subscribing to it here is preventing the resolver from resolving, well, no, I added that in for debugging purposes after it was already not working. When I comment it back out, I get exactly the same result (except that that console.log call isn't executed): the resolver never resolves.
Which is weird, since my explicit subscribe on the Observable demonstrates that it's slated to yield the correct value.
Confirming that this never gets past resolution, note that the console.log statement in the component's constructor is never executed, as it was before I ran the request through the resolver.
Any thoughts?
Try using
take(1)
or first
operator to mark the completion of Observable
. The resolve waits for Observable to complete before continuing. If the Observable doesn't complete the resovler will not return.
Your code would be something like this:
import { Injectable, Input } from '@angular/core';
import { RemoteEventService } from '../shared/remote-event.service';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
import { take } from 'rxjs/operators';
import { IEvent } from '../shared/event.model';
@Injectable()
export class EventDetailResolver implements Resolve<IEvent> {
constructor(
private eventService: RemoteEventService
) {}
resolve(route: ActivatedRouteSnapshot) {
console.log(`In resolve(): id = ${route.params.id}`);
const e = this.eventService.getEventById(+route.params.id);
console.log(`The observable that resolve() is about to return: ${JSON.stringify(e)}`);
e.subscribe((evt) => console.log(`The value that the observable resolves to: ${JSON.stringify(evt)}`));
return e.pipe(take(1));
}
}
Have a look at this github discussion on this behavior.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With