1

How can I activate a RouteGuard or Resolve on child routes within another module?

Example Scenario:

I have an application which has many separate modules, each defining their own routes. Consider these modules definining the following routes:

Module1 -> ['/a', '/b', '/c']
Module2 -> ['/d', '/e', '/f']

Now, I need to make sure that every route within the application has the following resolve:

resolve: { config: AppConfiguration} 

We can use:

{ path: '',  component: AppComponent, resolve: { config: AppConfiguration}  }

However that achieves nothing -/ executes the resolver, but /a does not.

The only way I've found to make sure routes /a, /b and /c call the resolver, is if I make them children of the root as follows:

AppModule -> [ { path: '', component: 'MainComponent', resolver: {..}, children: [
    ...Module1Routes
    ...Module2Routes
] ]}

But by doing that this means the application is no longer structured in the way recommended by the Angular documentation, RouterModule.forChild() is no longer used in other modules.

I'm sure this is a pretty common use case - is there a better way?

Community
  • 1
  • 1
David
  • 15,652
  • 26
  • 115
  • 156
  • Is there a reason why config is a resolver? Should it be reinstantiated on every route change? – Estus Flask May 07 '17 at 23:05
  • I have another case which makes sense - had to change the variable names because it's super secret and stuff :) – David May 07 '17 at 23:13
  • Also, if you have it in the root, the objective is to NOT have it re instantiated on every route change, but rather loaded once when root is loaded, (ie; when I press F5 or restart the app) – David May 07 '17 at 23:15
  • So going from /a, to /b to /c would call the resolver once, but I can still go to /c and it would call the resolver. Attaching the resolver to each route individually would call it three times. – David May 07 '17 at 23:16
  • Yes, `children` is the way to do this. But if you want them to be feature modules and use `forChild`, the whole idea breaks apart - you decouple things from root module, yet you try to couple them again via common resolver. Doesn't make much sense. – Estus Flask May 07 '17 at 23:24
  • Probably you should pick names better, so it would provide better idea of what is your case and how to treat it properly. From my understanding, AppConfiguration should be just a provider on root injector. – Estus Flask May 07 '17 at 23:24
  • @estus Let's say you want to call a /profile call every time the user reloads the app, (but not when navigating between routes) on all routes except the login. I think that would fit the scenario I described. – David May 07 '17 at 23:27
  • Ok, I guess it looks similar to 'configuration' case, more or less. The solution may differ a bit if there are details that stayed out of scope of the question, but the options stay the same as in suggested answer. – Estus Flask May 08 '17 at 01:23

1 Answers1

2

Since AppConfiguration is supposed to be a singleton that is shared by all routes, there are two common patterns to do that.

Considering that a major benefit of resolvers is to access data that was unwrapped from asynchronous resolver as activatedRoute.snapshot.data.resolverName, one way is to use a singleton service that is unwrapped by all resolvers, e.g.:

@Injectable()
class Config {
  data: IConfig;
  data$: Observable<IConfig>;

  constructor(private http: Http) {}

  load(forceReload = false) {
    if (!this.data$ || forceReload) {
      this.data$ = this.http.get('...')
      .map(res => res.json())
      .do(data => {
        this.data = data
       })
      .publishReplay(1)
      .refCount();
    }

    return this.data$;
  }
}

load method returns an observable with cached server response that is functionally equal to a promise. But since it is known that observables are natively used by a router, it is natural to use them in this context.

Then all resolvers can return config.load() and ensure that request will be performed only once (unless it is called like config.load(true)):

@Injectable()
class ConfigResolver {
  constructor(private config: Config) {}
  resolve() {
    return this.config.load();
  }
}

...
{ path: ..., component: ... resolve: { config: ConfigResolver } } 

It is possible to make code DRYer and provide a function or a class to add config resolver for all routes, though this is not recommended since this approach is not compatible with AoT. In this case WETter is better.

Another way is to use poorly documented APP_INITIALIZER provider, which is explained in detail in this question. While route resolvers postpone route change, APP_INITIALIZER works in the opposite way and postpones application initialization.

It is multi-provider and should be defined in root injector:

export function configInitializerFactory(config: Config) {
  return () => config.load();
}

@NgModule({
  ...
  providers: [
    ...
    Config,
    {
      provide: APP_INITIALIZER,
      useFactory: configInitializerFactory,
      deps: [Config],
      multi: true
    }
  ]
})
...

No resolvers have to be involved because data has been already resolved during app initialization.

Fortunately, data$ observable is already being unwrapped to data in Config, so resolved data is already available on injection as config.data.

Community
  • 1
  • 1
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • @David Yes, APP_INITIALIZER is good for that. I guess you're judging from A1 experience where route resolvers were the only recipe for delaying init process. Luckily, this was fixed in A2. – Estus Flask May 08 '17 at 20:58