25

I have the following routing paths for a module of my Angular app:

@NgModule({
    imports: [
        RouterModule.forChild([
            {
                path: 'documents',
                data: { myObject: MyConstants.OPTION_ONE },
                children: [
                    {
                        path: ':ID_DOC',
                        children: [
                            { path: 'edit', component: EditDocumentComponent },
                            { path: '', component: DocumentDetailsComponent },
                        ]
                    },
                    { path: 'add', component: AddDocumentComponent },
                    { path: '', component: DocumentsListComponent }
                ]
            }
        ])
    ],
    exports: [
        RouterModule
    ]
})

export class DocumentsManagementRoutingModule {
}

As you can see I use data property to pass some data to every path in "documents", so I can get it from any of the Components declared in the routing paths:

For example here is how I get data in DocumentDetailsComponent:

export class DocumentDetailsComponent implements OnDestroy {
    private obsData: Subscription;
    private option: any;

    constructor(private route: ActivatedRoute) {
        this.obsData = this.route.data.subscribe(data => {
            this.option = data['myObject'];
        });
    }

    ngOnDestroy(): void {
        this.obsData.unsubscribe();
    }
}

Now I changed the routing structure of the entire app and I call the above module from other modules, in lazy loading, using loadChildren attribute. And I pass data in the same way:

@NgModule({
    imports: [
        RouterModule.forChild([
            {
                path: 'users',
                loadChildren: 'app/documents/documents.module#DocumentsModule',
                data: { myObject: MyConstants.OPTION_ONE }},
            {
                path: ':ID_USER',
                children: [
                    { path: 'edit', component: EditUserComponent },
                    { path: '', component: UserDetailsComponent },
                ]
            },
            { path: 'add', component: AddUserComponent },
            { path: '', component: UserListComponent }
        ])
    ],
    exports: [
        RouterModule
    ]
})

export class UsersManagementRoutingModule {
}

And I did the same for all the other modules that calls DocumentsManagementRoutingModule, changing myObject property to MyConstants.OPTION_TWO, MyConstants.OPTION_THREE and so on, according to my needs.

Then, since I don't know the module and its related path that calls my lazy loading module, how to get data property from the caller module?

smartmouse
  • 13,912
  • 34
  • 100
  • 166
  • Possible dup of https://stackoverflow.com/questions/44796671/pass-parameter-into-modules – Gambo Jul 05 '17 at 07:22
  • In my case the use of a lazy loading module is mandatory – smartmouse Jul 05 '17 at 07:30
  • @smartmouse there is a line `r.url.lastIndexOf("/"));` illegal. – e-cloud Jul 13 '17 at 14:21
  • And there are double paths for `UsersManagementRoutingModule` – e-cloud Jul 13 '17 at 15:09
  • @e-cloud fixed the first one. I don't understand the second message. Thank you – smartmouse Jul 13 '17 at 16:45
  • 1
    two path property in first level of routes config of `UsersManagementRoutingModule` – e-cloud Jul 13 '17 at 16:47
  • Also, `loadChildren` and `children` should not be used simultaneously. Define your child routes in `DocumentsModule` and then remove the `children` field from the `'users'` route within `UsersManagementRoutingModule`. Unless there's a typo and you meant to have two separate routes here, one with `loadChildren` and one without... – Mike Hill Jul 13 '17 at 17:11
  • @smartmouse are you saying that you have multiple routes to `DocumentsManagementRoutingModule`? Are they different routes (e.g., `'/users/documents'`, `'/pages/documents'`, etc)? What does your route hierarchy look like? You can try using Augury (https://augury.angular.io/) to get a nice route diagram, too. – Mike Hill Jul 13 '17 at 17:16
  • @e-cloud: I fixed all typos, thank you – smartmouse Jul 14 '17 at 15:29
  • @MikeHill Yes, I have different routes for the same module. About Augury I stopped to use it last year: https://stackoverflow.com/questions/38225172/angular-2-components-tree – smartmouse Jul 14 '17 at 15:30
  • @smartmouse, most of your post is description about your design. But you question is unclear, you'b better describe what's wrong if subscribe the `route.params`. Make some comparison. – e-cloud Jul 15 '17 at 04:28
  • @e-cloud Subscription to `route.params` looks for `:ID` paths, not `data` params. – smartmouse Jul 18 '17 at 13:19
  • I know this, `route.params` looks for `:ID`, `route.queryParams` looks for query param in the url, `route.data` looks for `data` property on a router path. Here is a plunker, as you can see the last child can't see `data`: https://plnkr.co/edit/GpNQxfWVi4BrF5MNc6Ml. Is it right that I have to use `route.parent.params` to make it working? And `route.parent.parent.params` if I add more children under last child? – smartmouse Jul 19 '17 at 08:38

5 Answers5

11

I think I may understand what's the question is asking about.


Problem

Here is the routing graph of the plunker @smartmouse provided.

- home
  - shop
  - contact
    - map
  - about

There is a data object configured on the home route config. The direct child routes can access this data object. The problem is, the non-direct child map wants to access the data object, but apparently, it can't.

And we can know @smartmouse wants push that even further -- nested non-direct child routes can access data from ancestor.

Why it can't?

See the plunker I've modified. https://plnkr.co/edit/FA7mGmspUXHhU8YvgDpT

You can see that I define a extra child component DefaultComponent for contact. It shows that it is the direct and default child of route contact, which can be interpreted as it is also the direct child of superior route home. So, it can access the data object.

Solution

Look at the plunker provided by me again.

NOTE: The Angular packages I'm using is v4.3.0

Approach #1 -- query params

I define some query params in home.component.ts for contact route. You can see how to use it.

Pros:

  • you can access query params across any level of routes.
  • works well with lazyload modules

Cons:

  • you can't define query params in route config.

    But you can use some route lifecycle hooks to define them for target ancestor routes

Approach #2 -- service

Define a data share service on top level of the application playing as Singleton Pattern. Also, use some util to prevent it from being initiated twice. Then you can put some data in it, and retrieve it anywhere.

Pros:

  • global visible via DI;
  • works well with lazyload modules

Cons:

  • you can't update data in route config.
  • you have to define data somewhere else. If you can't control it source, it may be a problem.

Approach #3 -- route resolve

Someone may find out I've also defined resolve sample code in the plunker. It's to show it has the same behavior like data in route config, except it actually is for dynamic data retrieving.

https://vsavkin.com/angular-router-understanding-router-state-7b5b95a12eab#5400
The data property is used for passing a fixed object to an activated route. It does not change throughout the lifetime of the application. The resolve property is used for dynamic data.

Take @smartmouse's plunkr as example.

We have data defined in 'home' route config. We can define a data resolver in ContactModule for contact's default route, and proxy the data passed from superior route. Then all its direct child routes can access the resolved data object.

Pros:

  • works well with any level of nested routes
  • works well with lazyload modules

Cons:

  • You may have to declare that resolve property in every parent route whose child routes wants to access that data. It's kind of dirty work.

There is no perfect and universal solution, for now, with common usage of Angular. Someone should weigh the pros and cons and choose the proper one.

e-cloud
  • 4,331
  • 1
  • 23
  • 37
  • I think you've posted a wrong Plunker link... Anyway as I figure out if a direct child have `component` in the router path it can see the `data` property but it eats it and then inhibits it for its children. That's the problem. – smartmouse Jul 19 '17 at 15:16
  • i am sorry, fix it now. – e-cloud Jul 19 '17 at 17:10
  • @e-cloud Can you elaborate on "Approach 1 Cons: But you can use some route lifecycle hooks to define them for target ancestor routes" – JGFMK Jul 19 '17 at 20:42
  • quote from https://angular.io/guide/router "If you need to see what events are happening during the navigation lifecycle, there is the enableTracing option as part of the router's default configuration. This outputs each router event that took place during each navigation lifecycle to the browser console. This should only be used for debugging purposes. You set the enableTracing: true option in the object passed as the second argument to the RouterModule.forRoot() method." - so half answered my own comment.. – JGFMK Jul 19 '17 at 20:49
  • @JGFMK, enableTracing is not what i suggested. [`Router` has lots of events](https://github.com/angular/angular/blob/4.3.1/packages/router/src/events.ts#L11-L26) you can listen to/subscribe on. Define query params in those event, that's what i mean. – e-cloud Jul 20 '17 at 02:39
  • @e-cloud "nested non-direct child routes can access data from ancestor". Did you mean "can't" ? – smartmouse Jul 20 '17 at 07:09
  • no, i just tried to describe your question more clearly, that text is what i mean. – e-cloud Jul 20 '17 at 09:39
9

You can traverse the parent routes by using activatedRoute.pathFromRoot, and get the data from the nearest ancestor that has it available. Something like this seems to work:

  ngOnInit(): void {
    let idx = this.activatedRoute.pathFromRoot.length - 1;
    while(this.sharedData == null && idx > 0){
      this.sub = this.activatedRoute.pathFromRoot[idx].data.subscribe(subData => {

        if(subData.id)
          this.sharedData = subData;

        console.log('shared data', this.sharedData);
      });
      idx--;
    }
  }

https://plnkr.co/edit/K5Rb1ZvTvSQNM6tOLeFp

rusmus
  • 1,665
  • 11
  • 18
  • Yes, I like your solution. Anyway I can't figure out why I have to loop on all `ActivatedRoute` instead of directly access to currect activated route to get the `data` (or `params`) I need... – smartmouse Jul 20 '17 at 07:14
  • 1
    It seems to be the intended behaviour https://github.com/angular/angular/issues/12767#issuecomment-260493205 – rusmus Jul 20 '17 at 07:21
  • traversing upward is kind of dirty and there are memory leaks without unsubscribe action. – e-cloud Jul 20 '17 at 09:38
  • Yes, it is kind of dirty, but this is the currect state in Angular. About memory leak you can just add `unsubscribe` method in the loop. – smartmouse Jul 21 '17 at 12:55
  • Could new `paramsInheritanceStrategy` added in Angular 5.2 fix this problem? – smartmouse Jan 11 '18 at 08:49
  • @smartmouse according to the documentation, yes! paramsInheritanceStrategy: Defines how the router merges params, data and resolved data from parent to child routes. Available options are: 'emptyOnly' | 'always' – 1in9ui5t Feb 15 '18 at 08:20
1
@NgModule({
    imports: [
        RouterModule.forChild([
            {
                path: 'users',
                loadChildren: 'app/documents/documents.module#DocumentsModule',
                resolve : {
                     data: DataResolver
                }
            {
        ])
    ],
    exports: [
        RouterModule
    ]
})

export class UsersManagementRoutingModule {
}


@Injectable()
export class DataResolverimplements Resolve<Data> {
  resolve() {
    return this.serice.getData();
  }
}
Yoav Schniederman
  • 5,253
  • 3
  • 28
  • 32
  • 1
    Did this work for anyone? I'm also trying this approach but the default component of lazy loaded module is never called. – Tu Shams Jan 17 '18 at 17:15
  • Yes this works https://angular.io/api/router/Resolve. You need to make sure to provide your service in the parent module (`providedIn: root` for example in newer versions) and make sure your Observable is invoked (https://angular.io/guide/router#resolve-pre-fetching-component-data I was using `map` instead of `mergeMap` in my pipe causing my observable not to be invoked) – Tim Jun 04 '20 at 19:14
0

Why don't you use service to share the data between the modules or any components. You can have this service imported at the main module provider and hence can be used in all other modules (lazy loading module) and share the data.

The service can have functions to set and get values and hence you can make use of these functions in services to get your required data

Kowsalya
  • 1,378
  • 16
  • 25
  • 1
    If I use shared service it works only if you don't go to lazy loading module by direct url. If you go to a page that belongs to lazy loading module not passing by previous module that send data to shared service, you have no way to get the data! – smartmouse Jul 13 '17 at 13:03
  • 1
    Yeah, I understood your problem now. – Kowsalya Jul 13 '17 at 16:27
0

If you end up here nowadays, check out the router configuration option paramsInheritanceStrategy. It allows you to refine 'how the router merges parameters, data, and resolved data from parent to child routes.' I had some resolved data that wasn't being sent to lazy loaded modules, and setting the parameter to 'always' solved this problem for me.

Joseph Zabinski
  • 693
  • 4
  • 23