10

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);
  }
}

enter image description here

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:

enter image description here

With the resolver active:

enter image description here

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?

9
  • There are a couple of issues with RemoteEventService. First issue is that emitter is going to effectively always return before this.getEvents().subscribe() is finishing executing due to its asynchronous nature. Also can you please why you would be using EventEmitter inside an @Injectable() service? EventEmitter is used to emit custom events from @Component. You definitely need to refactor to instead return the Observable returned from getEventById() in combination with pipe() and an RxJS operator such as switchMap() to continue the observable. Commented May 23, 2019 at 19:38
  • If you need to perform side effects as a result of getEvents(), you can use pipe() with RxJS operators such as tap(), but effectively emitter.emit() will always execute after emitter has already been returned let alone that it would not be used with @Injectable(). Commented May 23, 2019 at 19:42
  • I use the EventEmitter there because I need to apply processing (the "find") between the receipt of the events array from the getEvents() call and the delivery of the one event to the component. Every which way I tried to use "pipe", it complained that it only allowed transformations from IEvent[] to IEvent[] and not IEvent[] to IEvent. I thought observables (from which EventEmitter is derived), don't kick off execution until they have been subscribed to, so I don't see how its value could be emitted before the client has subscribed to it. Commented May 23, 2019 at 20:08
  • What, effectively, is the difference between providing an emitter, after having called its emit() method, to a parent through a (click) attribute on an HTML tab and through a return statement? Either way, the client (component or caller) has to subscribe to it. Commented May 23, 2019 at 20:09
  • 1
    Are you familiar with promises? Imagine the following function foo() { let bar; new Promise(resolve => resolve(true)).then(result => bar = result); return bar; }. bar in that example would be returned before then() would execute. It's a very similar concept to what's happening in getEventById() right now. Yeah, pipe() and operators can have complex return types and messages, but EventEmitter simply shouldn't be in @Injectable(). Commented May 23, 2019 at 20:11

1 Answer 1

32

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.

Sign up to request clarification or add additional context in comments.

1 Comment

I'm going to have to study really hard to understand the flow of the resolver in order to understand just why this works but, yes, simply tacking ".pipe(take(1))" at the end did work. Thanks!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.