Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there better way to get ancestor route params in Angular2?

I need to get param from route which is not parent and not even grand-parent, it's simply somewhere above, along the path to root. I am aware of approaches like shown in Angular 2: getting RouteParams from parent component or through shared service but situation which I put myself into (at least I think so) makes those not well suited for it. So here is what I got:

I have lazily loaded sub-module which needs id from parent context. Situation more or less looks in routing configuration like this :

// Parent/outer module routing
const outerRoutes: Routes = [
    {
        path: 'container/:containerId',
        component: ContainerComponent,
        children: [
            { path: 'model', loadChildren: 'app/container/model/model.module#ModelModule' },
            { path: '', component: ContainerDetailsComponent }
        ]
    }
];

// Child/inner module routing
const innerRoutes: Routes = [
    {
        path: '',
        component: ModelComponent,
        children: [
            { path: ':id', component: ModelDetailsComponent },
            { path: '', component: ModelsComponent }
        ]
    }
];

ModelsComponent needs to load all Model instances belonging to given Container. So I decided to go through .parent approach but my teeth started to hurt after writing 2nd .parent and there was still 3rd to come:

this.route.parent.parent.parent.params
  .switchMap((params: Params) => this.modelService.getAllBelongingTo(params['containerId']))
  .subscribe(
    (models: Model[]) => this.models = models,
    error => this.raceEvent = null
  );

Shared service is somehow an option but due to the fact that child module is lazily loaded I would have to put it in core module making it quite global (which I don't like).

After a bit of struggling I came to following piece of code, which simply takes all ActivatedRoute up to the root, combines them, does lookup for param and consumes it. I don't like it as in my opinion it too complex , unreadable and stinks from a mile (or in other words is gory as heck) but works and I don't have to take care whether nesting changes. I simply have to make sure that :containerId is somewhere up there on the path to root.

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { Observable } from 'rxjs/Observable';

import { Model } from './model';
import { ModelService } from './model.service';

@Component({
    ...
})
export class ModelsComponent implements OnInit {
    models: Model[];

    constructor(
        private route: ActivatedRoute,
        private modelService: ModelService) { }

    ngOnInit() {
        const paramsArr = this.route.pathFromRoot.map(route => route.params);

        Observable.combineLatest(paramsArr)
            .switchMap(arrOfParams => {
                const params = arrOfParams.find(p => p['containerId'] !== undefined),
                      id = params && params['containerId'];
                return id ? this.modelService.getAllBelongingTo(id) : [];
            })
            .subscribe(
                (models: Model[]) => {
                    // yay! we got sth
                    this.models = models;
                },
                error => {
                    // handle big nono
                }
            );
    }
}

Is there a better way to handle depicted situation?

like image 881
gaa Avatar asked Mar 08 '17 15:03

gaa


2 Answers

I ran into the same problem and after finding this answer I created this function called getParams() that gets all the params of the parents and the current root.

It combines all the params and reduces into a single map.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ServicosService } from '@service/core/servicos.service';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { map, combineAll } from 'rxjs/operators';

@Injectable()
export abstract class HttpServiceRouted {
  constructor(
    protected http: HttpClient,
    protected servicos: ServicosService,
    protected route: ActivatedRoute
  ) {}

  getParams() {
    return Observable.from(this.route.pathFromRoot.concat(this.route)).pipe(
      map(route => route.params),
      combineAll(),
      map((params: any[]) =>
        params.reduce(
          // tslint:disable-next-line:no-shadowed-variable
          (map: Map<string, string>, obj) => {
            Object.keys(obj).forEach(key => {
              map.set(key, obj[key]);
            });
            return map;
          },
          new Map<string, string>()
        )
      )
    );
  }
}

Usage:

this.getParams().subscribe((params: ParamMap) => {
  const id = params.get('id');
});
like image 108
João Francisco Gonçalves Avatar answered Sep 18 '22 13:09

João Francisco Gonçalves


Generally, I think your approach does make sense.

I do not know the exact background for the code, but it does feel like a child component wanting to know lots about parent instance data, and I would agree with @Aravind's comment that this is likely best dealt with in global state management solutions such as redux or ngrx. That being said, if this is all you would need it for, I understand you would not want to introduce that level of complexity.

I would only make minor adjustments to your rxchain so it is more readable.

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { Observable } from 'rxjs/Observable';

import { Model } from './model';
import { ModelService } from './model.service';

@Component({
    ...
})
export class ModelsComponent implements OnInit {
    models: Model[];

    constructor(
        private route: ActivatedRoute,
        private modelService: ModelService) { }

    ngOnInit() {
        const paramsArr = this.route.pathFromRoot;

        Observable.from(paramsArr)
            .map(route => route.params)
            .filter(params => params.containerId)
            .switchMap(containerId => this.modelService.getAllBelongingTo(containerId))
            .subscribe(
                (models: Model[]) => {
                    // yay! we got sth
                    this.models = models;
                },
                error => {
                    // handle big nono
                }
            );
    }
}

Note however this implementation will only set this.models when there is a containerId as a parameter to a parent

EDIT 1: Fixed typos

like image 45
Ben Dadsetan Avatar answered Sep 18 '22 13:09

Ben Dadsetan