Angular router: how to pass data to lazy loading module?
Asked Answered
C

5

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?

Cooks answered 5/7, 2017 at 7:16 Comment(13)
Possible dup of #44797171Telemetry
In my case the use of a lazy loading module is mandatoryCooks
@Cooks there is a line r.url.lastIndexOf("/")); illegal.Langill
And there are double paths for UsersManagementRoutingModuleLangill
@Langill fixed the first one. I don't understand the second message. Thank youCooks
two path property in first level of routes config of UsersManagementRoutingModuleLangill
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...Mcmahon
@Cooks 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 (augury.angular.io) to get a nice route diagram, too.Mcmahon
@e-cloud: I fixed all typos, thank youCooks
@MikeHill Yes, I have different routes for the same module. About Augury I stopped to use it last year: https://mcmap.net/q/539708/-angular-2-components-treeCooks
@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.Langill
@Langill Subscription to route.params looks for :ID paths, not data params.Cooks
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: 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?Cooks
I
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

Incidentally answered 20/7, 2017 at 5:22 Comment(6)
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...Cooks
It seems to be the intended behaviour github.com/angular/angular/issues/12767#issuecomment-260493205Incidentally
traversing upward is kind of dirty and there are memory leaks without unsubscribe action.Langill
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.Cooks
Could new paramsInheritanceStrategy added in Angular 5.2 fix this problem?Cooks
@Cooks 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'Graycegrayheaded
L
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.

Langill answered 19/7, 2017 at 12:42 Comment(7)
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.Cooks
i am sorry, fix it now.Langill
@Langill Can you elaborate on "Approach 1 Cons: But you can use some route lifecycle hooks to define them for target ancestor routes"Beguine
quote from 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..Beguine
@JGFMK, enableTracing is not what i suggested. Router has lots of events you can listen to/subscribe on. Define query params in those event, that's what i mean.Langill
@Langill "nested non-direct child routes can access data from ancestor". Did you mean "can't" ?Cooks
no, i just tried to describe your question more clearly, that text is what i mean.Langill
I
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

Incidentally answered 20/7, 2017 at 5:22 Comment(6)
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...Cooks
It seems to be the intended behaviour github.com/angular/angular/issues/12767#issuecomment-260493205Incidentally
traversing upward is kind of dirty and there are memory leaks without unsubscribe action.Langill
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.Cooks
Could new paramsInheritanceStrategy added in Angular 5.2 fix this problem?Cooks
@Cooks 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'Graycegrayheaded
I
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();
  }
}
Isotherm answered 20/7, 2017 at 5:44 Comment(2)
Did this work for anyone? I'm also trying this approach but the default component of lazy loaded module is never called.Flightless
Yes this works 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 (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)Embolus
S
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

Smallscale answered 13/7, 2017 at 12:40 Comment(2)
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!Cooks
Yeah, I understood your problem now.Smallscale
K
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.

Katiekatina answered 24/9, 2021 at 17:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.