#Limit a provider to a Module without lazy-loading

46 messages · Page 1 of 1 (latest)

junior oriole
#

We building a map-widget at the moment. You should be able to provide different tile URLs by using the module:

import: [
  MapWidgetModule.config('https://stamen-tiles.a.ssl.fastly.net/toner/{z}/{x}/{y}.png'),
  MapWidgetModule.config('https://osm.com/{z}/{x}/{y}.png'),
]

We are providing them via an injection token as ModuleWithProviders on the config() method:

{
  provide: MAP_TILE_LAYER,
  useValue: url
}

However the url is always 'https://osm.com/{z}/{x}/{y}.png' (last imported Module). When reading about providers, this seems to be the natural behavior of non-lazy-loaded modules and providers.

My question is: Is there any way to have a per-module configuration? So I can do this kind of simple configuration already at the module import?

Still not clear?
This Stackblitz (not mine) is exactly showing the issue with the InjectionToken DISPLAY_LIFE_TOKEN: https://stackblitz.com/edit/angular-ivy-r3ctm9?file=src%2Fapp%2Fweb%2Fweb.module.ts

Thanks, Jan

A angular-cli project based on @angular/animations, @angular/compiler, @angular/core, @angular/common, @angular/platform-browser-dynamic, @angular/forms, @angular/platform-browser, rxjs, tslib, zone.js and @angular/router.

marsh sigil
#

Routes can have providers

#
{
  component: ..,
  path: ..,
  providers: ..
}
#

Another possibility if you lazy load modules instead of routes is

loadChildren: import('xx').then(m => m.forChild({paramsHere}))
#

Plenty of ways really

junior oriole
#

mh, okay that is bound to routes. My use-case is wihout any route.

#

(*I know the stackblitz is with routes, sorry for that, just stolen it as I was lazy)

marsh sigil
#

Then maybe the second one

junior oriole
#

loadChildren is a route option, or not?

marsh sigil
#

It's both, it can load routes or a module

junior oriole
#

yeah, but can you use loadChildren wirthout any Router?

marsh sigil
#

Ah sorry I thought you said you had lazy-loading

keen wraith
#

When you dynamically load/instantiate a component, you can provide your own EnvironmentInjector with whatever providers you want for it.

marsh sigil
#

Well then you can do something like

@NgModule({})
export class MyModule {
  static forWhatever(options): NgModuleWithProviders<MyModule> /* iirc */ {
    return {
      imports: [...],
      providers: [
        {
          provide: MAP_TILE_LAYER,
          useValue: url
        }
     ]
    };
  }
}
@NgModule({
  imports: [
    MyModule.forWhatever('https://osm.com/{z}/{x}/{y}.png')
  ]
})
class AppModule {}
keen wraith
#
// Create a new EnvironmentInjector that has the services you want.
const environmentInjector = createEnvironmentInjector(this.envInjector, [ScopedService]);

// Dynamically import and render a component with that injector.
const FooCmp = await import('xx').then(m => m.FooCmp);
const ref = vcr.createComponent(FooCmp, {environmentInjector});
junior oriole
#

That is what I do. But when I use it like this:

@NgModule({
  imports: [
    MyModule.forWhatever('https://osm.com/{z}/{x}/{y}.png')
    MyModule.forWhatever('https://ouch')
  ]
})
class AppModule {}

Both modules resolve the provider to:
https://ouch

I wonder if there is really no way to make this work per module import.

marsh sigil
#

That gives you 2 MAP_TILE_LAYERs but still just 1 service injecting it no?

junior oriole
keen wraith
#

Yep - that's all the router does internally anyway.

#

That's because you're providing MAP_TILE_LAYER twice, in the same AppModule, and the last provider wins.

junior oriole
marsh sigil
#

Yeah but you're supposed to scope the provider and the injected token

#

It won't guess where it's called from

#

So you need separate instances

#

(not in the same module)

keen wraith
#

It is always flattend on the injector and then it resolves to the last imported.
This is a great description of how DI works - when an injector is created, all the providers referenced by the NgModule(s) are flattened, and the last one for any given token wins.

junior oriole
keen wraith
#

If you want different components / contexts to see different values for the same token, they need to have different injectors.

#

Oh! No, lazy loading is unnecessary.

marsh sigil
#

Here you could do without the await

#

(well, without the import)

keen wraith
#
import {FooCmp} from 'somewhere';
// Create a new EnvironmentInjector that has the services you want.
const environmentInjector = createEnvironmentInjector(this.envInjector, [ScopedService]);

const ref = vcr.createComponent(FooCmp, {environmentInjector});
junior oriole
#

and that would be possible in the abstract config() call?

what is this.envInjector in this case?

keen wraith
#

The current EnvironmentInjector (which you can just inject)

marsh sigil
#

(imagining injecting the injector to get the environment injector so that you can create a new environment injector to inject)

keen wraith
#

Hahaha.

#

I don't know how the config call you described is supposed to work.

junior oriole
#

as @marsh sigil desceribed already:

@NgModule({})
export class MyModule {
  static config(url): NgModuleWithProviders<MyModule> /* iirc */ {
    return {
      module MyModule,
      providers: [
        {
          provide: MAP_TILE_LAYER,
          useValue: url
        }
     ]
    };
  }
}
keen wraith
#

Right, but like - that provides a specific URL for MAP_TILE_LAYER

#

You could use that config function to create providers for the child injector for each component that you want to render.

junior oriole
#

something like this:

providers: [
        {
          provide: MAP_CONFIG,
          useFactory: (envInj) => {
              // const environmentInjector = createEnvironmentInjector(this.envInjector, [ScopedService]);
              // const ref = vcr.createComponent(FooCmp, {environmentInjector});
          },
          deps: [EnviormentInjector]
        }
     ]

Mh, noo... don't get it. How could I do that?

keen wraith
#

I still don't understand what that expects to accomplish

junior oriole
#

So we have a dashboard... there we have a map-widget. Customers can define which widgets can be added to those dashboards in code by just importing a module. They might want to have one widget using e.g. the open street map tiles and another one using the open see map tiles. They should be able to configure that module multiple times with different layers. I was just not sure if there isn't a concept for this and why there is none. As it seems, there is no concept for this.

#

But you helped me already. Those components for the widgets are also loaded dynamically. So we might be able to add there scoped providers with the createEnvironmentInjector code you shared. thanks

keen wraith
#

Customers can define which widgets can be added to those dashboards in code by just importing a module.
Just importing an NgModule is not sufficient to configure an instance of something, because (as you've noticed) there is no distinction between multiple instances of the same NgModule being imported. If you want to instantiate multiple components with different behavior, either configure them via inputs or create them with different injectors.