1

Below is my code snippets. I wonder why it NEVER run into the completion handler? What I wanted to do is, first call serviceA to get objectA with given route param['key'], and then call serviceB to get objectB. So, objectB depends on result of objectA which depends on given param['key'].

p.s. I am using Angular 7 with rxjs6

ngOnInit() {
    this.route.params.pipe(
      mergeMap(
        (params: Params) => {
          this.key = params.key;
          return this.serviceA.getObjectA(this.key);    // http request service to backend
        }
      ),
      mergeMap(
        (objectA: ObjectA) => {
          // do something with objectA

          return this.serviceB.getListOfObjectB();  // http request service to backend
        }
      )
    ).subscribe(
      (objectBList: ObjectB[]) => {
        for (const b of objectBList) {
          // do something with objectB
        }
        // the code execution ends here
      },
      () => {
        // error handler
      },
      () => {
        // completion handler
        // the code execution NEVER comes to here, WHY??
      }
    );
  }
Angus
  • 31
  • 4

1 Answers1

3

I assume that route is the injected ActivatedRoute.

Each ActivatedRoute is bound to a routed component and when a route changes occurs, the current component being displayed is going to be destroyed, as well as its bound ActivatedRoute, so that's why you won't get a complete notification.


Here's how an ActivatedRoute is created:

function createActivatedRoute(c: ActivatedRouteSnapshot) {
  return new ActivatedRoute(
      new BehaviorSubject(c.url), new BehaviorSubject(c.params), new BehaviorSubject(c.queryParams),
      new BehaviorSubject(c.fragment), new BehaviorSubject(c.data), c.outlet, c.component, c);
}

Now, how are ActivatedRoutes bound to routed component?

Assuming you have a configuration that looks like this:

{
  path: 'a/:id',
  component: AComponent,
  children: [
    {
      path: 'b',
      component: BComponent,
    },
    {
      path: 'c',
      component: CComponent,
    },
  ]
}

and an issued URL like a/123/b

you'd end up having a tree of ActivatedRoutes:

      APP
       |
       A
       |
       B

Whenever you schedule a navigation(e.g router.navigateToUrl()), it has to go through some important phases:

  • apply redirects: checking for redirects; loading lazy-loaded modules; finding NoMatch errors
  • recognize: creating the ActivatedRouteSnapshot tree
  • preactivation: comparing the resulted tree with the current one; this phase also collects canActivate and canDeactivate guards, based on the found differences
  • running guards
  • create router state: where ActivatedRoute tree is created
  • activating the routes: this is the cherry on the cake and the place where the ActivatedRoute tree is leveraged

    It is also important to mention the role that router-outlet plays.

    Angular keeps track of the router-outlets with the help of a Map object.

    Here's what happens when you have a <router-outlet></router-outlet> in your app:

@Directive({selector: 'router-outlet', exportAs: 'outlet'})
export class RouterOutlet implements OnDestroy, OnInit {
  private activated: ComponentRef<any>|null = null;
  private _activatedRoute: ActivatedRoute|null = null;
  private name: string;

  @Output('activate') activateEvents = new EventEmitter<any>();
  @Output('deactivate') deactivateEvents = new EventEmitter<any>();

  constructor(
      private parentContexts: ChildrenOutletContexts, private location: ViewContainerRef,
      private resolver: ComponentFactoryResolver, @Attribute('name') name: string,
      private changeDetector: ChangeDetectorRef) {
    this.name = name || PRIMARY_OUTLET;
    parentContexts.onChildOutletCreated(this.name, this);
  }
}

Note the presence of activated(which is a component) and _activatedRoute!.

And here are the relevant bits of ChildrenOutletContexts:

export class ChildrenOutletContexts {
  // contexts for child outlets, by name.
  private contexts = new Map<string, OutletContext>();

  /** Called when a `RouterOutlet` directive is instantiated */
  onChildOutletCreated(childName: string, outlet: RouterOutlet): void {
    const context = this.getOrCreateContext(childName);
    context.outlet = outlet;
    this.contexts.set(childName, context);
  }
}

where childName is by default 'primary'. For now, focus your attention only on the context.outlet part.

So, for our route configuration:

{
  path: 'a/:id',
  component: AComponent,
  children: [
    {
      path: 'b',
      component: BComponent,
    },
    {
      path: 'c',
      component: CComponent,
    },
  ]
}

the router-outlet Map would look like this(roughly):

{
  primary: { // Where `AComponent` resides [1]
    children: {
      // Here `AComponent`'s children reside [2]
      primary: { children: { } }
    }
  }
}

Now, let's see how a route is activated:

// This block of code will be run for [1] and [2] (in this order!)
const context = parentContexts.getOrCreateContext(future.outlet);
/* ... */

const config = parentLoadedConfig(future.snapshot);
const cmpFactoryResolver = config ? config.module.componentFactoryResolver : null;

context.attachRef = null;
context.route = future;
context.resolver = cmpFactoryResolver;
if (context.outlet) {
  context.outlet.activateWith(future, cmpFactoryResolver);
}

this.activateChildRoutes(futureNode, null, context.children);

context.outlet.activateWith(future, cmpFactoryResolver); is what what we're looking for(where outlet is the RouterOutlet directive instance):

  activateWith(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver|null) {
    if (this.isActivated) {
      throw new Error('Cannot activate an already activated outlet');
    }
    this._activatedRoute = activatedRoute;
    const snapshot = activatedRoute._futureSnapshot;
    const component = <any>snapshot.routeConfig!.component;
    resolver = resolver || this.resolver;
    const factory = resolver.resolveComponentFactory(component);
    const childContexts = this.parentContexts.getOrCreateContext(this.name).children;
    const injector = new OutletInjector(activatedRoute, childContexts, this.location.injector);
    this.activated = this.location.createComponent(factory, this.location.length, injector);
    // Calling `markForCheck` to make sure we will run the change detection when the
    // `RouterOutlet` is inside a `ChangeDetectionStrategy.OnPush` component.
    this.changeDetector.markForCheck();
    this.activateEvents.emit(this.activated.instance);
}

Note that this.activated holds the routed component(e.g AComponent) and this._activatedRoute holds the ActivatedRoute for this component.

Let's see now what happens when we're navigating to another route and this current view is destroyed:

deactivateRouteAndOutlet(
    route: TreeNode<ActivatedRoute>, parentContexts: ChildrenOutletContexts): void {
  const context = parentContexts.getContext(route.value.outlet);

  if (context) {
    const children: {[outletName: string]: any} = nodeChildrenAsMap(route);
    const contexts = route.value.component ? context.children : parentContexts;

    // Deactivate children first
    forEach(children, (v: any, k: string) => this.deactivateRouteAndItsChildren(v, contexts));

    if (context.outlet) {
      // Destroy the component
      context.outlet.deactivate();
      // Destroy the contexts for all the outlets that were in the component
      context.children.onOutletDeactivated();
    }
  }
}

where RouterOutlet.deactivate() looks like this:

deactivate(): void {
  if (this.activated) {
    const c = this.component;
    this.activated.destroy(); // Destroying the current component
    this.activated = null;
    // Nulling out the activated route - so no `complete` notification
    this._activatedRoute = null;
    this.deactivateEvents.emit(c);
  }
}
Andrei Gătej
  • 11,116
  • 1
  • 14
  • 31