angular / angular

Deliver web apps with confidence 🚀
https://angular.dev
MIT License
95.11k stars 24.9k forks source link

MODULE_INITIALIZER like APP_INITIALIZER #17606

Closed pantonis closed 2 months ago

pantonis commented 7 years ago

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.

Alex-hv commented 3 years ago

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[]
parasharsh commented 3 years ago

+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

gunjankhanwilkar commented 3 years ago

+1 for MODULE_INITIALIZER

suaha commented 3 years ago

+1 for MODULE_INITIALIZER

dperetz1 commented 3 years ago

+1 for MODULE_INITIALIZER

mmoustafa-salama commented 3 years ago

+1 for MODULE_INITIALIZER

m-refaat commented 3 years ago

+1 for MODULE_INITIALIZER

LiteAppHub commented 3 years ago

+1 for MODULE_INITIALIZER

jamesblunt1973 commented 3 years ago

+1 for MODULE_INITIALIZER

YauheniyBaihot commented 3 years ago

+1 for MODULE_INITIALIZER

cerireyhan commented 3 years ago

+1 for MODULE_INITIALIZER

kthrmnd commented 3 years ago

+1 for MODULE_INITIALIZER

marcio199226 commented 3 years ago

+1 for MODULE_INITIALIZER

ahnpnl commented 3 years ago

+1 for MODULE_INITIALIZER

graphicsxp commented 3 years ago

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

bamsir commented 3 years ago

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

hoeni commented 3 years ago

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

micru commented 2 years ago

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

AlexeyApplicature commented 2 years ago

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.

kristofdegrave commented 2 years ago

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.

AlexeyApplicature commented 2 years ago

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.

kristofdegrave commented 2 years ago

@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

ghost commented 2 years ago

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

adam-marshall commented 2 years ago

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

david-garcia-garcia commented 2 years ago

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]
        },
      ]
    }
micru commented 2 years ago

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

Lonli-Lokli commented 2 years ago

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
        }
      ]
    };
  }
}
nthornton2010 commented 2 years ago

+1 for adding this into Angular

RockNHawk commented 2 years ago

still missing this feature

karanba commented 2 years ago

+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.

KirilToshev commented 1 year ago

+1 Need this feature

pkozlowski-opensource commented 1 year ago

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:

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.

kristofdegrave commented 1 year ago

@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.

patricsteiner commented 1 year ago

@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 {}
dpereverzev commented 1 year ago

Nice to have feature

SheepReaper commented 1 year ago

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.

Den-dp commented 1 year ago

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
}],
lucas-labs commented 1 year ago

What about something similar to NestJS? Nest provides onModuleInit hook which is awaited before services and controllers (components) onInit methods. They can be used inside Modules.

@NgModule({
  ...
})
export class MyModule implements OnModuleInit {
  async ngOnModuleInit() {
    await ....
    // runs before injectables/components ngOnInit
    // and before "child" modules OnInit and its awaited ...
  }
}
grg-kvl commented 1 year ago

I need the same functionality for firebase initialization from remote configurations. The "MODULE_INITIALIZER" looks like the most idiomatic way for that feature in Angular.

@patricsteiner have to found a workaround for that use case?

grg-kvl commented 1 year ago

Eventually I solved it by providing FIREBASE_OPTIONS token in appModule

    {
      provide: FIREBASE_OPTIONS,
      useValue: environment.firebase
    },

and in environment.ts I have

{
...
firebase: getConfig()
}

getConfig relies on synchronous XMLHttpRequest.

Works fine.

vmagalhaes commented 1 year ago

+1 This is important for module federation integration between two applications

johncrim commented 1 year ago

I recently needed an initializer to run when a lazy-loaded module is loaded, and @pkozlowski-opensource 's example wasn't sufficient, but the general idea that there are more extension points than there used to be was helpful.

I still think a MODULE_INITIALIZER would be useful, but here's an updated analysis of what we currently can do (ng 15-16):

1. Async import continuation in loadChildren

loadChildren: () => import('./lazy/lazy-routes').then((m) => m.lazyRoutes()),

lazyRoutes() has to be a function or static class method that returns a Route[]. It may be async. The downside is that it can't use inject() or otherwise obtain dependencies.

If you need inject() (I did) you can:

2. Factory function for ROUTES

The old recommendation for lazy-loaded route modules was using RouterModule.forChild():

const routes: Routes = [{ ... }];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class CustomersRoutingModule { }

If you look at forChild(), it's pretty simple:

  static forChild(routes: Routes): ModuleWithProviders<RouterModule> {
    return {
      ngModule: RouterModule,
      providers: [{provide: ROUTES, multi: true, useValue: routes}],
    };
  }

So, you can replace the call to forChild() with a factory fn for ROUTES and an import for RouterModule. Here's an example, which calls inject() to initialize a service before returning the routes:

const routes: Routes = [{ ... }];

@NgModule({
  imports: [RouterModule],
  exports: [RouterModule],
    {
      provide: ROUTES,
      multi: true,
      useFactory: () => {
        // initialize the CustomerLoggingService before any of the routes are used.
        inject(CustomerLoggingService);
        return routes;
      }
    },
})
export class CustomersRoutingModule { }

The downside of this approach is that the initialization function cannot be async (or at least you can't wait for an async initialization fn to complete).

If you need an async initializer in a lazy-loaded module with dependency injection, I would use one of the route guards. You'll have to deal with the fact that guards can be called whenever the route is tested, so you'll have to protect against repeated initialization. Both of the suggestions I've provided here have the advantage that they're only called once.

blemaire commented 11 months ago

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.

The main issue with this is that it requires routing... I do not have routing in my case..

nthornton2010 commented 11 months ago

My use case doesn't have anything to do with routing configuration. I'm trying to eager load data (via 1 or more service calls) when the module is initialized that one or more components in the module will use at some point later.

I've begun calling the services directly from each module's constructor as I don't have another choice here using fire and forget, the service caches the data. Calling it from the component is too late as the user would then have to wait for the data retrieval. It's worked out well for us but feels very dirty and anti-Angular.

pkozlowski-opensource commented 11 months ago

My use case doesn't have anything to do with routing configuration. I'm trying to eager load data (via 1 or more service calls) when the module is initialized that one or more components in the module will use at some point later.

Why not start this logic from a service's constructor in that case?

nthornton2010 commented 11 months ago

@pkozlowski-opensource the services are provided in root. I think we'd then be loading more data than needed which is why we wanted to wait to load them until the module was being loaded, though maybe loading all that extra data is not necessarily a bad thing and an interesting idea.

pkozlowski-opensource commented 11 months ago

I think we'd then be loading more data than needed which is why we wanted to wait to load them until the module was being loaded, though maybe loading all that extra data is not necessarily a bad thing and an interesting idea.

Root or not, services are not created eagerly. So constructors would be called only if there is a class that injects them (which I would assume is a strong indication that something needs those data).

Could you give it a try?

harsh-julka-bb commented 11 months ago

Issue: Facing same issue, while trying to load a remote app through module federation. Does not make sense to include all the peer dependencies in the remote root module.

Angular version: 13.3.11 Module Federation version: 13..0.1

Request: While using angular with module federation, The providers should also be loaded with lazy loaded module rather than root module.

alecoder commented 3 months ago

Any updates on this topic? I can see the issue is neither closed nor evolved.

alxhub commented 2 months ago

Wow, I just rediscovered this issue, but we've had the fix in for a while now.

Meet ENVIRONMENT_INITIALIZER :)