9

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?

asimhashmi
  • 4,258
  • 1
  • 14
  • 34
Green Grasso Holm
  • 468
  • 1
  • 4
  • 18
  • 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. – Alexander Staroselsky May 23 '19 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()`. – Alexander Staroselsky May 23 '19 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. – Green Grasso Holm May 23 '19 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. – Green Grasso Holm May 23 '19 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()`. – Alexander Staroselsky May 23 '19 at 20:11
  • `getEventById(id: number): Observable { return this.getEvents().pipe(map(events => events.find((event) => event.id === id))` }. – Alexander Staroselsky May 23 '19 at 20:15
  • @AlexanderStaroselsky, I think that's the sort of code I started with when first exploring pipe, and it screamed at me "No! Pipe here needs a IEvent[] => IEvent[]!" Though I've tried so many permutations, it's hard to remember. – Green Grasso Holm May 23 '19 at 20:26
  • @AlexanderStaroselsky But--it's working now. Thanks! – Green Grasso Holm May 23 '19 at 20:32
  • https://www.positronx.io/angular-route-resolvers/ – Billu May 17 '22 at 06:53

1 Answers1

29

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.

metodribic
  • 1,561
  • 17
  • 27
asimhashmi
  • 4,258
  • 1
  • 14
  • 34
  • 1
    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! – Green Grasso Holm May 23 '19 at 20:18