12

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?

Community
  • 1
  • 1
gaa
  • 1,132
  • 11
  • 26
  • 1
    state management techniques is one solution, if it is a huge application – Aravind Mar 13 '17 at 13:18
  • not yet, problem is that I don't really know how big it will grow and I don't want to overinvest... but on the other hand maybe all in all it is already worth investing into it to save myself potential headaches later... Any concrete product/approach on your mind? sth like [Savkin's Tackling State](https://vsavkin.com/managing-state-in-angular-2-applications-caf78d123d02#.953430gqr), or all the way up to [ngrx/store](https://github.com/ngrx/store) or Redux? – gaa Mar 13 '17 at 13:57
  • Why having a shared service is that bad? It would be just a couple of lines and it would simplify a lot your implementation – Fabio Antunes Mar 15 '17 at 15:01
  • 1
    @FabioAntunes I'm not saying it is bad, I simply believe that in that context where you have child module which is lazily loaded it would have to go to CoreModule to be available across the injector borders and it would make it _quite_ global. When you have more of such module configurations (lazily loaded parent-lazily loaded child) on various depths it would make CoreModule (or differently… root injector) quite polluted with those "exchange services". Oh snap… forgot to mention that parent is lazily loaded as well so its providers do not belong to root injector and are no visible for child:P – gaa Mar 15 '17 at 17:39
  • You don't need to "pollute" core. You could provide your shared service at a component level rather than at a module level to avoid service instance confusion. You would define in it in the module with the parent component and provide it at the parent component. Then you are free to inject it into a child component from any other module and it will receive the "closest" provided copy, in this case, the one provided at the parent component level. – bryan60 Jun 14 '18 at 19:35
  • Just going to tack on that you have mutually exclusive ideas here. If your app is "not big enough" to warrant a state management solution, then it definitely isn't big enough to warrant lazy loading modules. – bryan60 Jun 14 '18 at 19:39

2 Answers2

7

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

Ben Dadsetan
  • 1,565
  • 11
  • 19
  • Child module is solely for management/import/etc (CRUD) one of the aspects of container which is model list bound to it. This module won't be used that often but it is essential at some stage of interaction (this is why lazy load). What I need is only `containerId` (not entire container) in this context to fetch all models which are bound to it. All in all you and @Aravind are right with state management and as app grows it will come sooner or later I simply believe that the right time has not come yet for it. – gaa Mar 17 '17 at 08:38
  • Understood. In that case, I am not sure what to recommend more than the minor rxchain usage cleanup (I just cleaned up my cleanup btw in an edit). I am not sure what is bothering you with your design then. I feel it is appropriate. – Ben Dadsetan Mar 17 '17 at 08:48
  • To be honest if no one will come with sth ground breaking till the end of bounty, it is yours. Problem is that that it cannot be used as general approach as it gets quite quickly out of the hands when you have more than one id like: model id is only unique within container so to go one level deeper lets say manipulate model details you have pair `containerId` and `modelId` and such stuff simply pile up in rx chain. All in all as said you and @Aravind opened my eyes that usage of state management is closer than I thought. – gaa Mar 17 '17 at 09:51
  • Isn't that the beauty of our craft? :-) It only gets messy when the requirements get messier. It could be that app state stores become more relevant, or maybe you want to just create chain pieces you can easily reuse with .let(), ... Either way, rxjs chains are indeed really to be adjusted a lot when requirements change. I still feel that they save lots of code and time to figure out code. Am sticking with my response. :-) – Ben Dadsetan Mar 18 '17 at 21:36
1

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