Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preload modules modules based on canLoad guard result #20994

Open
aciccarello opened this issue Dec 13, 2017 · 4 comments
Open

Preload modules modules based on canLoad guard result #20994

aciccarello opened this issue Dec 13, 2017 · 4 comments
Labels
area: router feature: insufficient votes Label to add when the not a sufficient number of votes or comments from unique authors feature: votes required Feature request which is currently still in the voting phase feature Issue that requests a new feature freq2: medium router: lazy loading
Milestone

Comments

@aciccarello
Copy link
Contributor

aciccarello commented Dec 13, 2017

I'm submitting a...


[ ] Regression (a behavior that used to work and stopped working in a new release)
[ ] Bug report  
[x] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior

Currently, when using a preloadering strategy (like PreloadAllModules), the preloader does not process routes with a canLoad property and therefore does not preload them. There is no indication in the CanLoad interface documentation that this will happen.

// no config loaded, fetch the config
} else if (route.loadChildren && !route.canLoad) {
res.push(this.preloadConfig(ngModule, route));

The router guide does note this behavior and that it is by design. However, it does not give any indication as to why.

The PreloadAllModules strategy does not load feature areas protected by a CanLoad guard. This is by design.

The language in the guide also isn't very clear. When I originally read that part of the guide, I misunderstood "areas protected by a CanLoadGuard" as only referring to areas where the canLoad guard check returned false.

Expected behavior

Preloaders should decide whether to preload based on the boolean result of the canLoad guard (or the boolean result of the observable/promise).

If this is not possible, it should be more clear that canLoad negates any preloading strategy for that module. This could be through console warnings and/or better documentation.

Minimal reproduction of the problem with instructions

What is the motivation / use case for changing the behavior?

  • The canLoad guard allows preventing navigation before a lazy loaded module is downloaded/bootstraped
  • Preloading requires "loading" a module, therefore a canLoad option still makes logical sense
  • The suggested use of canActivate over canLoad when preloading wastes network/memory/cpu resources
  • The disabling of preloading by the canLoad option happens silently
  • canLoad returns an observable or promise so it can delay loading for an async task (i.e. user login)
  • custom preloading strategies are slightly harder to implement than a canLoad hook
  • custom preloading can only be configured on a root or lazy loaded module level
  • PreloadAllModules could be used in concert with canLoad for a simple selective loading strategy

Environment


Angular version: 5.0.3


Browser:
- [x] Chrome (desktop) version 63.0.3239.84 (Official Build) (64-bit)
- [ ] Chrome (Android) version XX
- [ ] Chrome (iOS) version XX
- [ ] Firefox version XX
- [ ] Safari (desktop) version XX
- [ ] Safari (iOS) version XX
- [x] IE version 11
- [ ] Edge version XX
 
For Tooling issues:
- Node version: v8.9.1  
- Platform: Windows 7 64-bit 

Others:

- Angular CLI: 1.5.4
@jasonaden jasonaden added freq2: medium feature Issue that requests a new feature labels Dec 19, 2017
@ngbot ngbot bot added this to the Backlog milestone Jan 23, 2018
@poddarkhushbu07
Copy link

poddarkhushbu07 commented Jun 7, 2019

Yes, I had the same issue. I needed to apply my authentication CanLoad guard and preload my module at a same time.
This feature is currently not supported. But I applied a workaround in my application. I used Custom Pre-loading Strategy which includes similar condition available in CanLoad guard along with pre-loading strategy conditions.

@angular-robot angular-robot bot added the feature: votes required Feature request which is currently still in the voting phase label Jun 4, 2021
@angular-robot
Copy link
Contributor

angular-robot bot commented Jun 4, 2021

Just a heads up that we kicked off a community voting process for your feature request. There are 20 days until the voting process ends.

Find more details about Angular's feature request process in our documentation.

@angular-robot
Copy link
Contributor

angular-robot bot commented Jun 26, 2021

Thank you for submitting your feature request! Looks like during the polling process it didn't collect a sufficient number of votes to move to the next stage.

We want to keep Angular rich and ergonomic and at the same time be mindful about its scope and learning journey. If you think your request could live outside Angular's scope, we'd encourage you to collaborate with the community on publishing it as an open source package.

You can find more details about the feature request process in our documentation.

@angular-robot angular-robot bot added the feature: insufficient votes Label to add when the not a sufficient number of votes or comments from unique authors label Jun 26, 2021
@hodossy
Copy link

hodossy commented Sep 20, 2021

Old issue, but I have found myself in a situation where this is exactly what I need. I work a project where additional modules can be purchased and the app is deployed on-premise. The modules are implemented as lazy-loaded routes, we should only pre-load the module when it is needed, i.e. the customer purchased it (checked via an HTTP request). I still however want to pre-load the modules when needed, so the CanActivate trick is not an option. I managed to make this work via a custom RouterPreloader. This is a real pain to implement outside of Angular due to some classes are not part of the public api, therefore I think it is not really possible as a third party lib within the Angular ecosystem.

canload_router_preloader.ts

import { Compiler, Injectable, Injector, NgModuleFactoryLoader, NgModuleRef, OnDestroy } from '@angular/core';
import {
  NavigationEnd,
  PreloadingStrategy,
  Route,
  RouteConfigLoadEnd,
  RouteConfigLoadStart,
  Router,
  Routes,
  UrlSegment,
} from '@angular/router';
import { prioritizedGuardValue } from '@angular/router/esm2015/src/operators/prioritized_guard_value';
import { wrapIntoObservable } from '@angular/router/esm2015/src/utils/collection';
import { isCanLoad, isFunction } from '@angular/router/esm2015/src/utils/type_guards';
import { RouterConfigLoader, LoadedRouterConfig } from '@angular/router/esm2015/src/router_config_loader';
import { EMPTY, from, Observable, of, Subscription } from 'rxjs';
import { concatMap, filter, map, mergeAll, mergeMap, switchMap } from 'rxjs/operators';

@Injectable()
export class CanLoadRouterPreloader implements OnDestroy {
  private loader: RouterConfigLoader;
  private subscription?: Subscription;

  constructor(
    private router: Router,
    moduleLoader: NgModuleFactoryLoader,
    compiler: Compiler,
    private injector: Injector,
    private preloadingStrategy: PreloadingStrategy,
  ) {
    const onStartLoad = (r: Route) => (router as any).triggerEvent(new RouteConfigLoadStart(r));
    const onEndLoad = (r: Route) => (router as any).triggerEvent(new RouteConfigLoadEnd(r));

    this.loader = new RouterConfigLoader(moduleLoader, compiler, onStartLoad, onEndLoad);
  }

  setUpPreloading(): void {
    this.subscription = this.router.events
      .pipe(
        filter((e) => e instanceof NavigationEnd),
        concatMap(() => this.preload()),
      )
      .subscribe(() => {});
  }

  preload(): Observable<any> {
    const ngModule = this.injector.get(NgModuleRef);
    return this.processRoutes(ngModule, this.router.config);
  }

  /** @nodoc */
  ngOnDestroy(): void {
    if (this.subscription) {
      this.subscription.unsubscribe();
    }
  }

  private processRoutes(ngModule: NgModuleRef<any>, routes: Routes): Observable<void> {
    const res: Observable<any>[] = [];
    for (const route of routes) {
      // we already have the config loaded, just recurse
      if (route.loadChildren && !route.canLoad && (route as any)._loadedConfig) {
        const childConfig = (route as any)._loadedConfig;
        res.push(this.processRoutes(childConfig.module, childConfig.routes));

        // no config loaded, fetch the config
      } else if (route.loadChildren) {
        if (!route.canLoad) {
          res.push(this.preloadConfig(ngModule, route));
        } else {
          res.push(
            this.runCanLoadGuards(ngModule.injector, route, []).pipe(
              switchMap((guardVal) => {
                return guardVal ? this.preloadConfig(ngModule, route) : EMPTY;
              }),
            ),
          );
        }

        // recurse into children
      } else if (route.children) {
        res.push(this.processRoutes(ngModule, route.children));
      }
    }
    return from(res).pipe(
      mergeAll(),
      map((_) => void 0),
    );
  }

  private preloadConfig(ngModule: NgModuleRef<any>, route: Route): Observable<void> {
    return this.preloadingStrategy.preload(route, () => {
      const loaded$ = (route as any)._loadedConfig
        ? of((route as any)._loadedConfig)
        : this.loader.load(ngModule.injector, route);
      return loaded$.pipe(
        mergeMap((config: LoadedRouterConfig) => {
          (route as any)._loadedConfig = config;
          return this.processRoutes(config.module, config.routes);
        }),
      );
    });
  }

  private runCanLoadGuards(moduleInjector: Injector, route: Route, segments: UrlSegment[]): Observable<boolean> {
    const canLoad = route.canLoad;
    if (!canLoad || canLoad.length === 0) return of(true);

    const canLoadObservables = canLoad.map((injectionToken: any) => {
      const guard = moduleInjector.get(injectionToken);
      let guardVal;
      if (isCanLoad(guard)) {
        guardVal = guard.canLoad(route, segments);
      } else if (isFunction(guard)) {
        guardVal = guard(route, segments);
      } else {
        throw new Error('Invalid CanLoad guard');
      }
      return wrapIntoObservable(guardVal);
    });

    return of(canLoadObservables).pipe(
      prioritizedGuardValue(),
      map((result) => result === true),
    );
  }
}

The idea is essentially to run the CanLoad guards almost exactly as found in the apply_redirect.ts file, except that we don't care if there is a redirect, thus the tap operator is deleted and UrlTree results are considered as false.

I would like to propose a new RouterPreloader implementation (like above, but properly implemented), for which developers would be able to opt-in by providing it:

app.module.ts excerpt

  providers: [
    {
      provide: RouterPreloader,
      useClass: CanLoadRouterPreloader,
    },
  ],

As an alternative, I can imagine a CanPreload guard, which would have the following interface and would be respected by the default RouterPreloader:

can_preload.ts

export interface CanPreLoad {
  canPreLoad(route: Route):
      Observable<boolean>|Promise<boolean>|boolean;
}

export type CanPreLoadFn = (route: Route) =>
    Observable<boolean>|Promise<boolean>|boolean;

What is your view on my proposals? Which approach is best here? I would probably go with the second option, but I needed a solution now, so that is why I implemented another RouterPreloader.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: router feature: insufficient votes Label to add when the not a sufficient number of votes or comments from unique authors feature: votes required Feature request which is currently still in the voting phase feature Issue that requests a new feature freq2: medium router: lazy loading
Projects
None yet
Development

No branches or pull requests

6 participants