angular icon indicating copy to clipboard operation
angular copied to clipboard

MODULE_INITIALIZER like APP_INITIALIZER

Open pantonis opened this issue 6 years ago • 92 comments

I'm submitting a ...


[ ] Regression (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

I was wondering if like APP_INITIALIZER a MODULE_INITIALIZER can be implemented. I have a scenario where multiple lazy load modules exist. Each module has a service that has injected in its constructor a config of type ConfigA. ConfigA is fetched from server. This service is then injected into several module components. With APP_INITIALIZER I cannot do this since the same type ConfigA is used for all modules and a singleton will be created.

pantonis avatar Jun 19 '17 10:06 pantonis

Could you please describe your use case more precisely ?

How do you lazy load your module ? If you use the router you might want to use a resolver to fetch data from the serve ?

vicb avatar Jun 21 '17 18:06 vicb

Hi,

Please see the sample code I created above that is implemented with resolver and is not working

@NgModule({
    providers: [
        StompServiceProvider,        
        ConfigResolve
    ]

})
export class FeatureModule { }


import { Http } from '@angular/http';
import { OverrideStompService } from "./services/override-stomp.service";
import { ActivatedRoute } from '@angular/router';

export let StompServiceProvider =
    {
        provide: OverrideStompService,
        useClass: OverrideStompService
    };


import { Http } from "@angular/http";
import { Injectable } from '@angular/core';
import { StompService, StompConfig } from "@stomp/ng2-stompjs";


@Injectable()
export class OverrideStompService extends StompService {
    constructor(stompConfig: StompConfig) {
        super(stompConfig);
    }   
}


import { Http, Response } from "@angular/http";
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
import { StompConfig } from '@stomp/ng2-stompjs';
import 'rxjs/add/operator/map';

@Injectable()
export class ConfigResolve implements Resolve<StompConfig>{
    constructor(private http: Http) {
    }

    resolve(route: ActivatedRouteSnapshot) {
        return this.http.get('app/config/brokerConfigFile.json')
            .map((response: Response) =>{ 
                debugger;
                return response.json()});        
    }
}

pantonis avatar Jun 22 '17 14:06 pantonis

APP_INITIALIZER works but I have to load all modules configurations in the RootModule. This raises security and isolation concerns as Feature module A will know the config of Feature module B, Feature Module A will know config of Feature module C and so on...

pantonis avatar Jun 22 '17 14:06 pantonis

... hmm, do you have a plunker that implements the resolver? Because in your code I can't se where you use the resolver service. It should be related to a route ... I can't see any route definition.

There should be something like:

{ 
    path: 'contact/:id',
    component: ContactsDetailComponent,
    resolve: {
      contact: 'contact'
    }
}

mlc-mlapis avatar Jun 22 '17 14:06 mlc-mlapis

Yes I Have this and forgot to add this in the code above. The problem here is that OverrideStompService is created before the resolver is called

pantonis avatar Jun 22 '17 14:06 pantonis

... for sure, it is logical. What to think using some identification of the module and then inject that value to the resolver service to know what module is processed and how it should be processed:

static forChild(): ModuleWithProviders {
     return {
          ngModule: MyLazyLoadedModule,
          providers: [
               {provide: ModuleConfig, useValue: {id:12, config: '...', ...} }
          ]
     };
}

mlc-mlapis avatar Jun 22 '17 15:06 mlc-mlapis

No I dont like the idea with passing module id etc.. I still think idea of MODULE_INITIALIZER is more clean and will also help other scenarios that I cant think of right now.

pantonis avatar Jun 22 '17 15:06 pantonis

Maybe yes, in some cases but actually you don't have it and doesn't seem that someting like that will get the support in near future.

As I know there should be something called module hook in Angular 5.x what will replace actual APP_INITIALIZER. The question is what exactly there will be implemented (one or more hooks on the module level and which, ...) and if it would be usable even for your case. There is a good chance that it will.

mlc-mlapis avatar Jun 22 '17 15:06 mlc-mlapis

@mlc-mlapis how do you know that it doesnt seem that will get the support in near future? I opened this ticket to write a suggestion what it would be good to have. Lets wait for the ng team to see what they will reply.

pantonis avatar Jun 22 '17 15:06 pantonis

Sure. The intention wasn't to say anything against, just a guess. 👀

mlc-mlapis avatar Jun 22 '17 16:06 mlc-mlapis

Is there any viable workaround? Basically APP_INITIALIZER functionality on sub module (1 level below app module) level?

leak avatar Feb 28 '18 15:02 leak

@leak ... and constructor on a module level is not enough for you?

mlc-mlapis avatar Feb 28 '18 16:02 mlc-mlapis

Constructor kicks in too early for me.

const appRoutes: Routes = [
    { path: '', loadChildren: './home/home.module#HomeModule' },
    { path: 'login', component: LoginPageComponent },
    { path: '**', component: NotFoundPageComponent }

Even when I'm loading the LoginPageComponent the Home module constructor is called.

What I need is some sort of life cycle hook which only kicks in when the Home module is actually active.

My actual use case: Loading a user profile. One way is through the login page, but when the auth is already good and the app is loading in another browser tab, I don't want to go through the login component again and rather just fetch the user profile from an api.

Maybe I'm completely on the wrong train, I just started digging into this issue.

leak avatar Feb 28 '18 19:02 leak

@leak ... oh, I thought constructor of HomeModule lazy loaded module ... it should be called only when that module is loaded, not earlier, certainly not when LoginPageComponent is instantiated.

mlc-mlapis avatar Feb 28 '18 19:02 mlc-mlapis

Just tested again to confirm:

export class HomeModule {
    constructor() {
        console.log("HomeModule ctor");
    }
}

Sadly it shows up on console when I start the application on /login.

leak avatar Feb 28 '18 19:02 leak

@leak ... ah, you have that module as a default route ... so no surprise that it is loaded immediately ... { path: '', loadChildren: './home/home.module#HomeModule' } but then its sense as a lazy loaded module is just problematic = doesn't have a sense.

mlc-mlapis avatar Feb 28 '18 20:02 mlc-mlapis

Nice catch. Leaves me short of the ctor option though. Guess I have to resort to dabbling with routing events...

leak avatar Feb 28 '18 20:02 leak

I also like the Idea of having a MODULE_INITIALIZER similar to APP_INITIALIZER very much, because in the module constructor there is no way to wait for async operations, while APP_INITIALIZER may return a promise that is resolved before going on.

hoeni avatar Nov 22 '18 12:11 hoeni

I'm looking for the same feature, I've to run some initialization code when the lazy module loads. APP_INITIALIZER doesn't seems to works with lazy module.

BenDevelopment avatar Dec 05 '18 09:12 BenDevelopment

I am looking forward to a similar feature.

cdutta avatar Dec 07 '18 16:12 cdutta

+1 for MODULE_INITIALIZER. I need to populate the store in my feature module with query parameters from the url, currently I am doing it using APP_INITIALIZER but this means the root module needs to know the correct action to dispatch to the feature which feels wrong.

An initialiser hook when the lazy module is loaded seems a reasonable request.

Woodsyla avatar Jan 24 '19 09:01 Woodsyla

MODULE_INITIALIZER is a good way to keep the separation of concerns. Example: in my application, there are 10 lazy loaded modules and each module has their configurations. Loading all configurations on App_Load will increase app load time. Also, if a user never loads a particular lazy loaded module, then all configurations loaded for these modules will be useless.

MODULE_INITAILIZER is very much required.

bhavnaberi avatar Feb 06 '19 12:02 bhavnaberi

Would also be good for lazy loading global styles/scripts when navigating to a lazy loaded module, global styles/scripts that are only necessary for that particular module.

christofferfleat avatar Apr 17 '19 07:04 christofferfleat

Oh no, I was hoping this was available (my root module needs to load some local config for authentication) then, I need to load extra data on child modules,MODULE_INITIALIZER would have been the perfect solution for this

blemaire avatar May 02 '19 19:05 blemaire

Also from me a +1 for MODULE_INITIALIZER

mirogrenda avatar Jul 12 '19 14:07 mirogrenda

+1 for MODULE_INITIALIZER

mdnaserhassan avatar Jul 29 '19 15:07 mdnaserhassan

+1 for MODULE_INITIALIZER

kalpeshhpatel avatar Aug 21 '19 11:08 kalpeshhpatel

As I can see, there are two limitations, that can't be realized currently by an angular app (using the module constructor):

  • Using asynchronous calls by returning a Promise, like APP_INITIALIZER does
  • Doing things requiring the module already initialized, like dynamically pushing a route to component that's defined here (component not yet initialized)

hoeni avatar Aug 23 '19 13:08 hoeni

+1 for MODULE_INITIALIZER

nivrith avatar Sep 03 '19 06:09 nivrith

Is there anything we can do to give this issue some core developer attention? Though I don't have knowledge of Angular internals (yet :-), I can't image that this would be a huge one to implement. On the other hand it would be so helpful on crafting a better application architecture in many use cases.

hoeni avatar Sep 03 '19 09:09 hoeni

+1 for MODULE_INITIALIZER

hareesh996 avatar Sep 12 '19 06:09 hareesh996

+1 for MODULE_INITIALIZER

BhishmaS avatar Sep 12 '19 07:09 BhishmaS

3 years after 3years. Is there any related work doing now?

Alexzjt avatar Oct 11 '19 14:10 Alexzjt

Another option would be to support ngOnInit and ngOnDestroy for modules.. Take a look at this issue: #10244

vdjurdjevic avatar Oct 16 '19 09:10 vdjurdjevic

+1 for MODULE_INITIALIZER

dontboyle avatar Nov 04 '19 18:11 dontboyle

If it should be possible if the following code would be added in the RouterConfigLoader. https://github.com/angular/angular/blob/1e9eeafa9e3fbfad5e94d697b8079cf649db626e/packages/router/src/router_config_loader.ts#L35-L44 to

return moduleFactory$.pipe(switchMap((factory: NgModuleFactory<any>) => {
      if (this.onLoadEndListener) {
        this.onLoadEndListener(route);
      }

      const module = factory.create(parentInjector);
       const initStatus = module.injector.get(ApplicationInitStatus);
       initStatus.runInitializers();
       return from(initStatus.donePromise.then(() => {
             return new LoadedRouterConfig(flatten(module.injector.get(ROUTES)).map(standardizeConfig), module);
         }));
}));    

With that in place, it would be possible to add the ApplicationInitStatus Provider inside the module, along with APP_INITIALIZER providers

@NgModule({
 providers: [
    {
      provide: ApplicationInitStatus,
      useClass: ApplicationInitStatus,
      deps: [[new Optional(), APP_INITIALIZER]]
    },
    {
      provide: APP_INITIALIZER,
      useFactory: () => {
        // DO Something
      }
    }
  ]
})
export class Feature

If the ApplicationInitStatus isn't added as a provider, the root one is used, and since this one is already initialized nothing will happen. If provided, only the APP_INITIALIZER defined here will run.

kristofdegrave avatar Feb 03 '20 15:02 kristofdegrave

+1 for MODULE_INITIALIZER

RyannGalea avatar May 04 '20 05:05 RyannGalea

+1 for MODULE_INITIALIZER

jamalsoueidan avatar Jun 06 '20 23:06 jamalsoueidan

+1 for MODULE_INITIALIZER

JoseStall avatar Jun 12 '20 23:06 JoseStall

+1 for MODULE_INITIALIZER

armanozak avatar Jun 19 '20 10:06 armanozak

While I also think a MODULE_INITIALIZER would be really useful (because it would allow asynchronous initialization in lazy-loaded modules), I believe most of the use-cases for a module initializer could be satisfied if there were support for async factory functions. If async factory functions were supported, classes requiring the async module initialization logic could just take a dependency on a type that is provided by an async factory.

Alternatively, if there were a MODULE_INITIALIZER, it would be possible to complete async initialization for dependencies in the MODULE_INITIALIZER.

johnc-ftl avatar Jul 30 '20 15:07 johnc-ftl

It doesn't seem like there's been any discussion here about using canActivate as your module initializer at the root of the forChild module.

RouterModule.forChild([
  {path: '', canActivate: [MyInitializerGuard], children: [...]}
]);

If you think of canActivate as "can this route activate yet?" rather than simply "is it allowed to activate at all?", this conceptually makes sense. This guard would also satisfy the need for an asynchronous blocker to activating any components within the module. This approach seems like it would solve most, if not all, of the use-cases here.

atscott avatar Aug 10 '20 22:08 atscott

@atscott - that's a good point, and certainly a useful piece to hack around no module initializer or async factory functions.

However, Angular allows you to lazy-load modules independent from routing (I implemented this just last week to reduce initial request size and overhead). While it's probably less common than lazy-loaded modules in routing, it's an important use case.

Also, the semantics are quite different. A route guard is "can this route be activated" vs "run me on initialization". For example, one would expect module initialization to run once only, whereas the route guard would run every time it is accessed. Yes, you can work around the semantic and behavioral differences, but I think there's still a strong case for async factory functions (or MODULE_INITIALIZER, if you prefer).

johnc-ftl avatar Aug 10 '20 23:08 johnc-ftl

@johnc-ftl Absolutely good points. Agreed that these are things that cannot be solved in the router. It may still be a good option for many who have commented here but won’t work for all. We need to be sure the problem and solution are appropriately scoped. For example, the solution provided in #36084 does not solve your use-case and may not necessarily provide additional value beyond canActivate. We should probably close that one then since it doesn’t solve the root problem and is only a partial solution that expands the public API unnecessarily.

atscott avatar Aug 11 '20 00:08 atscott

+1 for MODULE_INITIALIZER

jochelo avatar Sep 17 '20 14:09 jochelo

+1 for MODULE_INITIALIZER

wndproc1 avatar Sep 23 '20 04:09 wndproc1

+1 for MODULE_INITIALIZER

Alex-hv avatar Sep 25 '20 18:09 Alex-hv

+1 for MODULE_INITIALIZER

callebstrom avatar Oct 08 '20 16:10 callebstrom

+1 for MODULE_INITIALIZER

Really need this classic feature.

Djay77 avatar Nov 02 '20 16:11 Djay77

+1 but the limitations of https://github.com/angular/angular/issues/23279 seem to apply here as well. If we can ensure APP_INITIALIZERS are resolved and we can pass values to forRoot and modules...would be awesome.

LPCmedia avatar Nov 03 '20 06:11 LPCmedia

Here is great option to implement such feature, thanks to @kristofdegrave! https://github.com/angular/angular/issues/34351#issuecomment-577095385

when you're importing your module, you can do like this:

    import('./some/some.module')
      .then(({ SomeModule }) => {
        // here you can do any async logic, and by using @kristofdegrave solution
        // pass the result of operation as provider to your lazy module
        return  makeRequest().pipe(
          map(YOUR_DATA_HERE) => new LazyNgModuleWithProvidersFactory<T>(SomeModule.forChild(YOUR_DATA_HERE))
        ).toPromise()
      });

in your module:

export class SomeModule {
  public static forChild(data: SOMEDATA): ModuleWithProviders<SomeModule> {
    return {
      ngModule: SomeModule,
      providers: [{ provide: TOKEN, useValue: data }],
    };
  }
}

and in all module's components you'll be able to get your lazy loaded data by token you've provided

the same you can do with routes, define routes for module when you loading that exact module

      providers: [{ provide: ROUTES, useValue: routes }], // where routes:Route[]

Alex-hv avatar Nov 03 '20 09:11 Alex-hv

+1 for this This will be very important requirement with Module Federation. The microfrontend/plugin loaded using module federation can be a completely independent application which has to do so some initialisation. Considering the modules are loaded as lazy modules we can not use APP_INITIALIZERS

parasharsh avatar Nov 21 '20 05:11 parasharsh

+1 for MODULE_INITIALIZER

gunjankhanwilkar avatar Nov 26 '20 17:11 gunjankhanwilkar

+1 for MODULE_INITIALIZER

suaha avatar Dec 09 '20 10:12 suaha

+1 for MODULE_INITIALIZER

dperetz1 avatar Dec 31 '20 14:12 dperetz1

+1 for MODULE_INITIALIZER

mmoustafa-salama avatar Jan 05 '21 20:01 mmoustafa-salama

+1 for MODULE_INITIALIZER

m-refaat avatar Jan 05 '21 20:01 m-refaat

+1 for MODULE_INITIALIZER

facundoBungee avatar Jan 14 '21 17:01 facundoBungee

+1 for MODULE_INITIALIZER

jamesblunt1973 avatar Jan 23 '21 08:01 jamesblunt1973

+1 for MODULE_INITIALIZER

YauheniyBaihot avatar Jan 28 '21 04:01 YauheniyBaihot

+1 for MODULE_INITIALIZER

cerireyhan avatar Jan 28 '21 08:01 cerireyhan

+1 for MODULE_INITIALIZER

kthrmnd avatar Jan 28 '21 12:01 kthrmnd

+1 for MODULE_INITIALIZER

marcio199226 avatar Jan 28 '21 16:01 marcio199226

+1 for MODULE_INITIALIZER

ahnpnl avatar Mar 03 '21 14:03 ahnpnl

Would love to have a MODULE_INITIALIZER too, adding my voice to this.

graphicsxp avatar Mar 12 '21 14:03 graphicsxp

Why hasn't this been added yet?? +1 for this request! I want to run some asynchronous code when a module is lazy loaded, and right now cannot

bamsir avatar Apr 01 '21 15:04 bamsir

Still miss this... https://stackoverflow.com/a/57626816/256646

hoeni avatar Apr 01 '21 16:04 hoeni

Please implement this! Its really important e.g. to load external libs that cannot be bundled (i am thinking of a payment-provider lib etc) as a dependecy of a dynamic module

micru avatar Jun 29 '21 17:06 micru

Hi all, what about routing Guard CanLoad for lazy loading routes or CanActivate for regular routes? You can implement your logic inside the guard before the module is loaded but always return from that guard Observable to load this module after module init logic is done.

AlexeyApplicature avatar Oct 06 '21 09:10 AlexeyApplicature

CanLoad and CanActivate Guards are not made for this. As the names suggests they guard a route. They check if you can do it, but not to perform any mutation or do other logic.

kristofdegrave avatar Oct 06 '21 09:10 kristofdegrave

CanLoad and CanActivate Guards are not made for this. As the names suggests they guard a route. They check if you can do it, but not to perform any mutation or do other logic.

@kristofdegrave Please, suggest your solution.

AlexeyApplicature avatar Oct 06 '21 09:10 AlexeyApplicature

@AlexeyApplicature https://github.com/angular/angular/issues/34351#issuecomment-577095385 This is a workaroud, the real solution would be to have a hook on the router when a module gets loaded, and have the possibility to inject a ModuleWithProviders.

My workaround works but it needs code that shouldn't exist outside the framework

btw, this was my PR I proposed: https://github.com/angular/angular/pull/36084

kristofdegrave avatar Oct 06 '21 10:10 kristofdegrave

Another vote to request this feature. My use case is for 'regular' modules, not even lazy loaded. Each module needs to load its resource bundles (help texts, labels, etc.) before it is ready for use.

It would be nice to extend the concept to @Directive, maybe with a new type of PromiseInjectionToken, where angular automatically waits for the promise to resolve before injecting that parameter. That way appropriate instance variables can be initialized directly in the constructor and declared readonly.

P.S. The PromiseInjectionToken is much more general purpose and even removes the need for a special APP_INITIALIZER

sparrowhawk-ea avatar Nov 30 '21 20:11 sparrowhawk-ea

just to add my use case in here...

so I have a workflow where a user authenticates, and then once logged in their configuration is requested over HTTP.

this configuration is used to build the full routing tree of all pages and sub-pages in the application which they have access to.

without knowing the configuration, the full set of routes are not known.

so it would be this scenario here: https://stackoverflow.com/a/57626816/1061602

adam-marshall avatar Jan 21 '22 13:01 adam-marshall

Just want to share, if your module is lazy loaded, you can hook between the loading and actual delivery to do any async initialization like this:

loadChildren: () => Observable.fromPromise(import('./fall-apart-layout/fall-apart-layout.module'))
          .pipe(
            switchMap((i) => {
              return navigationService.$routesLoaded
                .pipe(
                  map(() => i)
                );
            })
          )
          .toPromise()
          .then(m => m.FallApartLayoutModule),

With that in mind an playing with injection, you can get any bootstrap data you need for a dinamically loaded module that you can even use to provide the router module with dinamically defined routes.

Then in the dinamically loaded module, use a factory to provide the routes instead of calling "RouterModule.forChild":

imports: [
    {
      ngModule: RouterModule,
      providers: [
        {
          provide: ANALYZE_FOR_ENTRY_COMPONENTS,
          multi: true,
          useFactory: getRoutes,
          deps: [NavigationService]
        },
        {
          provide: ROUTES, multi: true, useFactory: getRoutes, deps: [NavigationService]
        },
      ]
    }

david-garcia-garcia avatar Apr 22 '22 16:04 david-garcia-garcia

@david-garcia-garcia this is very nice, thanks for sharing!

micru avatar Apr 22 '22 16:04 micru

I was able to do it with empty ROUTES token

@NgModule({
  imports: [EffectorModule.forFeature([DocumentListEffect])]
})
export class DocumentsModule {}
import { NgModule, ModuleWithProviders, Type } from '@angular/core';
import { ROUTES } from '@angular/router';

@NgModule({})
export class EffectorModule {
  static forFeature(
    featureEffects: Type<any>[] = []
  ): ModuleWithProviders<EffectorModule> {
    return {
      ngModule: EffectorModule,
      providers: [
        ...featureEffects,
        {
          provide: ROUTES,
          useFactory: () => () => [],
          deps: featureEffects,
          multi: true
        }
      ]
    };
  }
}

Lonli-Lokli avatar May 03 '22 13:05 Lonli-Lokli

+1 for adding this into Angular

nthornton2010 avatar May 11 '22 23:05 nthornton2010

still missing this feature

RockNHawk avatar May 24 '22 15:05 RockNHawk

+1 when you need to read settings and module federation is involved, it is a necessary feature, I hope it will be added as soon as possible.

karanba avatar Jun 26 '22 23:06 karanba

+1 Need this feature

KirilToshev avatar Jun 28 '22 12:06 KirilToshev

We can see that this feature request generates lots of interest which means that there are legitimate and common use-cases that are / were not not well supported. I was reading and re-reading this issue today (as well as associated issues) and if my understanding is correct, we are mostly talking about use-cases where we need to lazy-load some form of configuration before instantiating a DI service.

It is true that Angular's DI system doesn't offer any solution in this respect (there is a tracking request #23279 to add async capabilities to our DI system, but this would require a major overhaul / re-design of the DI system). But we can do lots of things before DI kicks in!

More specifically, with all the changes done in v14 (and more specifically - with the loadChildren and providers changes for the router) we can:

  • dynamically load router configuration without any NgModule,
  • specify providers on the lazy-loaded routes.

If we combine the above functionality we can come up with a pattern where we can lazy-load all the needed configuration and then return router + providers configuration. The gist of it could look like:

export async function lazyRoutes() {
  const config = await import('./lazy-config');

  return [
    {
      path: '',
      providers: [
        { provide: LazyService, useFactory: () => new LazyService(config) },
      ],
      component: LazyComponent,
    },
  ];
}

Such functions could be then used in a router configuration:

RouterModule.forRoot([
      {
        path: 'lazy',
        loadChildren: () =>
            import('./lazy-with-config/lazy-routes').then((m) => m.lazyRoutes()),
      },
    ]),

Here is a working stackblitz: https://stackblitz.com/edit/angular-ivy-e2dmmp?file=src%2Fapp%2Fapp.module.ts

With this approach we don't really need any NgModule and all the async logic is contained in regular async JavaScript functions. I do believe that this is a much simpler pattern.

With all the changes we've been doing to Angular lately we move towards the World where NgModules are optional and play less prominent role. As such we don't want to invest in adding more functionality to NgModules and it is very unlikely that we would want to implement MODULE_INITIALIZER]. Again, this is based on the assumption that simpler solutions exist today.

I was trying to understand the described use-cases to my best ability but I can't exclude that I've missed some important scenarios. If you still see use-cases that are not well covered today, please add your comment in this issue. But if we don't discover anything new here I'm leaning towards closing this issue as solved by all the recent v14 changes.

pkozlowski-opensource avatar Jun 30 '22 09:06 pkozlowski-opensource

@pkozlowski-opensource from what you present, it should support the use-cases I had. I have been out for a while because I changed customer. I Think angular is taking some good steps with making NgModule optional. On the other side I'm a fan of working with modules, because lazy loading modules looks for me as a better fit then lazy loading every 'smart' component separately. I have been working with Vue and that is the case there. But the thing I missed there the most was a way to lazy load modules including child routing. When going to Micro Frontends, this is the level I would need.

kristofdegrave avatar Jun 30 '22 11:06 kristofdegrave

@pkozlowski-opensource Thanks for the reply. Unfortunately I'm not sure how this helps, consider the following scenario:

To use firebase I need to call provideFirebaseApp(() => initializeApp(firebaseConfig)) inside of the imports of my AppModule. How can I do this if firebaseConfig must be loaded asynchronously? The reason I want to load the firebaseConfig asynchronously is because I want to seperate config from artifact, to be able to have a clean CI/CD pipeline (build once, run on any environment, get config from server).

What I would like to do:

let config: { firebase: any };

function initializeAppFactory(httpClient: HttpClient): () => Observable<any> {
  return () => httpClient.get('https://myserver.com/api/config').pipe(c => config = c)
}

@NgModule({
  imports: [
    BrowserModule,
    HttpClientModule,
    provideFirebaseApp(() => {
      return initializeApp(config.firebase); 
      // THIS DOES NOT WORK, because the APP_INITIALIZER is async! How can we also make this import async?!?
    })],
  declarations: [AppComponent],
  bootstrap: [AppComponent],
  providers: [{
    provide: APP_INITIALIZER,
    useFactory: initializeAppFactory,
    deps: [HttpClient],
    multi: true,
  }],
})
export class AppModule {}

patricsteiner avatar Jul 10 '22 21:07 patricsteiner

Nice to have feature

dpereverzev avatar Jul 21 '22 12:07 dpereverzev

We can see that this feature request generates lots of interest which means that there are legitimate and common use-cases that are / were not not well supported. I was reading and re-reading this issue today (as well as associated issues) and if my understanding is correct, we are mostly talking about use-cases where we need to lazy-load some form of configuration before instantiating a DI service.

It is true that Angular's DI system doesn't offer any solution in this respect (there is a tracking request #23279 to add async capabilities to our DI system, but this would require a major overhaul / re-design of the DI system). But we can do lots of things before DI kicks in!

More specifically, with all the changes done in v14 (and more specifically - with the loadChildren and providers changes for the router) we can:

  • dynamically load router configuration without any NgModule,
  • specify providers on the lazy-loaded routes.

If we combine the above functionality we can come up with a pattern where we can lazy-load all the needed configuration and then return router + providers configuration. The gist of it could look like:

export async function lazyRoutes() {
  const config = await import('./lazy-config');

  return [
    {
      path: '',
      providers: [
        { provide: LazyService, useFactory: () => new LazyService(config) },
      ],
      component: LazyComponent,
    },
  ];
}

Such functions could be then used in a router configuration:

RouterModule.forRoot([
      {
        path: 'lazy',
        loadChildren: () =>
            import('./lazy-with-config/lazy-routes').then((m) => m.lazyRoutes()),
      },
    ]),

Here is a working stackblitz: https://stackblitz.com/edit/angular-ivy-e2dmmp?file=src%2Fapp%2Fapp.module.ts

With this approach we don't really need any NgModule and all the async logic is contained in regular async JavaScript functions. I do believe that this is a much simpler pattern.

With all the changes we've been doing to Angular lately we move towards the World where NgModules are optional and play less prominent role. As such we don't want to invest in adding more functionality to NgModules and it is very unlikely that we would want to implement MODULE_INITIALIZER]. Again, this is based on the assumption that simpler solutions exist today.

I was trying to understand the described use-cases to my best ability but I can't exclude that I've missed some important scenarios. If you still see use-cases that are not well covered today, please add your comment in this issue. But if we don't discover anything new here I'm leaning towards closing this issue as solved by all the recent v14 changes.

I think you've missed a key scenario here. We're using micro-frontends via module federation. A typical example of one of our micro-frontend apps has 2 entry points: the standard, bootstrapped AppModule and also a RemoteEntryModule. so that the application can be run standalone (both for production and development purposes) and as a remote module for a legacy application. The AppModule is just a shell to import the routermodule root and BrowserModule, and finally importing the "real" application module which is RemoteEntryModule. Before RemoteEntry loads, I have a configuration service that makes an HTTP request to request a json configuration. I would prefer to use Angular's HttpClient which has to be provided via DI, I can't do what I need to do without hacky workarounds. And each application has to get its own configuration, I can't lump all of that within our federation host (the legacy app). Module initialization would be great for this. Also upgrading to 14 isn't possible at the moment for us.

SheepReaper avatar Jul 29 '22 03:07 SheepReaper

Have a similar use case where I have a 3rd party library that uses APP_INITIALIZER to init itself which is not working for lazy loaded feature modules.

I understand that this library probably was designed to be used as an eagerly loaded module, but I don't see big reasons why it shouldn't work with lazy loaded feature modules and theoretical MODULE_INITIALIZER.

Unfortunately, the following workaround can't be used as a solution to refactor this 3rd party library

RouterModule.forRoot([
  {
    path: 'lazy',
    loadChildren: () =>
        import('./lazy-with-config/lazy-routes').then((m) => m.lazyRoutes()),
  },
]),

because lazyRoutes() function here is not DI-aware, but the problematic library uses DI in APP_INITIALZER provider like that

export function startupServiceFactory(alfrescoApiService: AlfrescoApiService) {
    return () => alfrescoApiService.load();
}
...
providers: [{
    provide: APP_INITIALIZER,
    useFactory: startupServiceFactory,
    deps: [
        AlfrescoApiService
    ],
    multi: true
}],

Den-dp avatar Aug 03 '22 11:08 Den-dp