How to add providers to Injector dynamically? - javascript

Each component can specify new Providers using its providers property in ComponentMetadata.
Is there a way to specify providers dynamically from, say, constructor of the component?

I used useFactory to determine what class will be used for provide.
I share for whom concern.
In component ts
#Component({
selector: 'app-chart',
templateUrl: './chart.component.html',
styleUrls: ['./chart.component.scss'],
providers: [
{ provide: DateTimeAdapter, useClass: MomentDateTimeAdapter },
{ provide: OWL_DATE_TIME_FORMATS, useValue: CUSTOM_FORMATS },
{ provide: OwlDateTimeIntl, deps: [SettingService],
useFactory: (settingsService) => settingsService.getLanguage()
}
]
})
In service ts get class instance
#Injectable()
export class SettingService {
public getLanguage(){
return this.translate.currentLang == "ko" ? new KoreanIntl() : new DefaultIntl;
}
}

Below are 3 broader steps you need to follow to implement dynamic providers. Please note i have commented many part of the code so that we focus on the main answer. If you want to see detailed step refer this Angular tutorial
Step 1 :- Create the collection of the providers
Create the collection and you can use the push method to add DI objects dynamically.
var providerscoll:any = [];
providerscoll.push({ provide: "1", useClass: DialogLogger });
providerscoll.push({ provide: "2", useClass: ConsoleLogger });
Step 2 :- Provide the providers collection in "NgModule" .
Please see the Square bracket syntax.
#NgModule({
// code removed for clarity
providers: [providerscoll]
})
export class MainModuleLibrary { }
Step 3 :- Get the Injector object in constructor
Get injector object in the constructor using DI and you can then look up using the "Get" method and the token. So if you provide "1" then you get something and if you provide "2" you get something.
// code removed for clarity
import { Injector } from '#angular/core';
// code removed for clarity
export class CustomerComponent {
constructor(public injector: Injector){
this.logger = this.injector.get("2");
}
}

I've done it in the bootstrap part.
bootstrap(AppComponent,[
provide( RequestOptions, { useClass: DefaultRequestOptions } ),
provide(Http, { useFactory:
function(backend, defaultOptions) {
return new Http(backend, defaultOptions); },
deps: [XHRBackend, RequestOptions]}),
]);
I'm guessing it can be done in a component too:
https://angular.io/docs/ts/latest/api/http/Http-class.html
You make it dynamic by adding decisions into the factory function instead of just returning the same object.

There is one way to create component using ViewContainerRef, where is allowed to pass injector so I guess this should be possible but it is limited to create components dynamically:
Create Injector:
static create(options: {providers: StaticProvider[], parent?: Injector, name?: string}): Injector;
// #param injector The injector to use as the parent for the new component.
abstract createComponent<C>(
componentFactory: ComponentFactory<C>, index?: number, injector?: Injector,
projectableNodes?: any[][], ngModule?: NgModuleRef<any>): ComponentRef<C>;
Pseudo code:
class FooComponent {
constructor(
private readonly injector: Injector,
private readonly viewContainer: ViewContainerRef){
const customInjector = this.injector.create({ providers: [FooService], parent: injector });
this.viewContainer.createComponent(componentFactory, 0, customInjector );
...
}
}
Or similarly use Portal from Angular CDK.

Related

How can i render angular binding, directives and methods in innerHTML/Dynamically from S3

I am working on a sass based platform which is developed in Angular and NodeJs and I want to showcase my codebase on my platform so that client can easily edit their code and can go live. But for that i have to render my code dynamically.
I tried to render angular Bindings, Directives and Methods in innerHTML but it's not working it's working as a string. If anyone has any idea how can i render angular bindings, directives, and methods dynamically in innerHTML or other ways. Please share your comments. Thanks in advance
Template strings need to be "compiled" to be used dynamicly in a component.
Here is a working example for that :
#Component({
selector: 'hello',
template: '<div #container></div>',
})
export class HelloComponent implements AfterViewInit {
#ViewChild('container', { read: ViewContainerRef })
container: ViewContainerRef;
constructor(
private injector: Injector,
private environement: EnvironmentInjector
) {}
ngAfterViewInit() {
this.environement.runInContext(() => {
// important part to allow DI with inject()
// Define the component using Component decorator.
const component = Component({
selector: 'test',
template:
'<div>This is the dynamic template. Test value: {{test}}</div>',
styles: [':host {color: red}'],
providers: [{ provide: Foo, useClass: Foo }],
})(
class {
private foo = inject(Foo); // DI
constructor() {
console.log(this.foo.rand);
}
test = 'some value';
}
);
// Define the module using NgModule decorator.
const module = NgModule({ declarations: [component] })(class {});
const componentRef = this.container.createComponent(component, {
injector: this.injector,
ngModuleRef: createNgModuleRef(module, this.injector),
});
setTimeout(() => (componentRef.instance.test = 'some other value'), 2000);
});
}
}
Stackblitz

Pass variable to module using forRoot

I've developed a library with shared components in Angular and I want to pass configuration there.
Everything is basically the same as in:
Pass config data using forRoot
Problem is, that I need to pass user to this module, which is fetched on start of application and saved in Redux state.
Is it possible to pass observable with user using forRoot while importing module? Maybe it's possible to pass something to this module 'later' with some lazy loading?
#select() private user$: Observable<User>
#NgModule({
imports: [
LibModule.forRoot({
user: user$
})
...
})
I've made it another way - by injecting my implementation of abstract service for getting settings. Code below:
Lib module declaration:
export interface LibSettings {
user: User;
url: string;
}
export abstract class AbstractLibSettingsService {
abstract getSettings(): LibSettings;
}
#NgModule({
declarations: [...],
imports: [...],
exports: [...]
})
export class LibModule {
static forRoot(implementationProvider: Provider): ModuleWithProviders {
return {
ngModule: LibModule,
providers: [
implementationProvider
]
}
}
}
Lib service, where I needed the user:
constructor(private settingsService: AbstractGlassLibSettingsService) {
}
In application that uses the lib, module declaration with import:
export const LIB_SETTINGS_PROVIDER: ClassProvider = {
provide: AbstractLibSettingsService,
useClass: LibSettingsService
};
#NgModule({
imports: [...
LibModule.forRoot(LIB_SETTINGS_PROVIDER)
],
...
})
Finally, the implementation of the service in application:
#Injectable()
export class LibSettingsService extends AbstractLibSettingsService {
#select() private user$: Observable<User>;
private user: User;
constructor() {
super();
this.user$.subscribe(user => this.user = user);
}
public getSettings(): GlassLibSettings {
...
}

Nest can't resolve dependencies of guard wrapped inside a decorator

I'm trying to inject a provider inside a guard that is wrapped in a decorator, but Nest is not being able to resolve dependencies, giving me the next error:
[ExceptionHandler] Nest can't resolve dependencies of the SecuredGuard (Reflector, ?). Please make sure that the argument at index [1] is available in the SecuredGuard context.
The main purpose of my approach is to avoid using two separate decorators like this:
#Controller()
export class AppController {
#Get()
#Secured('admin')
#UseGuards(SecuredGuard)
getHello(): string {
return this.appService.getHello();
}
}
And instead insert the #UseGuards(SecuredGuard) inside my custom decorator #Secured('admin') so it ends up like this:
#Controller()
export class AppController {
#Get()
#Secured('admin')
getHello(): string {
return this.appService.getHello();
}
}
This is how I'm implementing my custom decorator:
export function Secured(...roles: string[]){
const setMetadata = ReflectMetadata('roles', roles)
const setupGuard = UseGuards(SecuredGuard)
return (target: any, key?: string, descriptor?: any) => {
setMetadata(target, key, descriptor);
setupGuard(target, key, descriptor);
}
}
And this is my SecuredGuard, the AuthService is the dependency that couldn't be resolved:
#Injectable()
export class SecuredGuard implements CanActivate {
constructor(
private readonly _reflector: Reflector,
private readonly _authService: AuthService
) { }
async canActivate(context: ExecutionContext): Promise<boolean> {...}
}
Both secured.guard.ts and secured.decorator.ts are part of secured.module.ts
#Module({
imports: [
SecuredGuard,
AuthModule
],
exports: [
SecuredGuard
],
providers: [
AuthService
]
})
export class SecuredModule {}
Which is using the AuthService being exported from auth.module.ts
#Module({
controllers: [
AuthController
],
providers: [
AuthService
],
imports: [
EmailModule
],
exports: [
AuthService
]
})
export class AuthModule { }
And secured.module.ts is being imported by app.module.ts
#Module({
imports: [
SecuredModule
],
controllers: [
AppController
],
providers: [
AppService
],
})
export class AppModule { }
I don't know if I'm using the appropriate approach, or even if it's possible what I'm trying to do, any clues would be really appreciated!
In general, your solution seems to work, see this running example:
However, there are some mistakes in your module declarations:
1) In your AppModule, the AuthService is not available, since neither is the AuthModule imported directly or exported by the SecuredModule. That's why you're getting the error.
2) You don't have to declare your guards in any module, they will just be available globally. Only put modules in your imports array.
3) You're providing the AuthService multiple times, so you will have different instances of it. You should only provide it once and then only export (or re-export) your provider, but not provide it again.
4) ReflectMetadata was deprecated in v6; use SetMetadata instead.

Angular 6 Universal service provided in Injector needs another app injected variable

I am using Angular Universal. I have created a PlatformService to detect which platform I am currently working on.
/* platform.service.js */
import { Injectable, Inject, PLATFORM_ID } from '#angular/core';
import { isPlatformBrowser, isPlatformServer } from '#angular/common';
#Injectable({
providedIn: 'root'
})
export class PlatformService {
constructor(
#Inject(PLATFORM_ID) private platformId: Object
) {
this.platformId; // this is coming out undefined
}
isBrowser() {
return isPlatformBrowser(this.platformId);
}
isServer() {
return isPlatformServer(this.platformId);
}
}
I am creating a BaseComponent for common handling of my route binded components.
/* base.component.ts */
import { Component, OnInit, Inject } from '#angular/core';
import { InjectorHolderService } from '#core/services/util/injector-holder.service';
import { PlatformService } from '#core/services/util/platform.service';
#Component({
selector: 'app-base',
template: '',
})
export class BaseComponent implements OnInit {
protected platformService: PlatformService;
constructor() {
this.platformService = InjectorHolderService.injector.get(PlatformService);
console.log(this.platformService);
}
}
Since this component will be inherited by many components, I didn't want to pass the PlatformService through super(). So I decided to go with creating an Injector.
/* app.module.ts */
import { InjectorHolderService } from '#core/services/util/injector-holder.service';
import { PlatformService } from '#core/services/util/platform.service';
#NgModule({ ... })
export class AppModule {
constructor() {
InjectorHolderService.injector = Injector.create({
providers: [
{
provide: PlatformService,
useClass: PlatformService,
deps: [], // I think i need to put something here, but not sure.
}
]
});
}
}
And a service which can hold all the injected module for future use.
/* injector-holder.service.ts */
import { Injectable, Injector } from '#angular/core';
#Injectable({
providedIn: 'root'
})
export class InjectorHolderService {
static injector: Injector;
}
But #Inject(PLATFORM_ID) private platformId: Object is giving out undefined, because of which I am not able to detect the platform.
What am I missing here? or If there is a better approach to achieve the above functionality.
Please let me know if you guys need to see any other file.
I am not sure whether the following approach is good or bad, currently, it is the only thing working for me. Would love to hear any new approach to it.
Since PlatformService needed #Inject(PLATFORM_ID) which is provided only from AppModule, the new Injector I created was not able to find any value for #Inject(PLATFORM_ID) and hence undefined.
So, instead of using class PlatformService in Injector, now I am using PlatformService's instantiated object and hence was able to access everything fine in BaseComponent.
Modified my AppModule like following:
/* app.module.ts */
import { InjectorHolderService } from '#core/services/util/injector-holder.service';
import { PlatformService } from '#core/services/util/platform.service';
#NgModule({ ... })
export class AppModule {
constructor(
private platformService: PlatformService,
) {
InjectorHolderService.injector = Injector.create({
providers: [
{
provide: PlatformService,
useValue: this.platformService, // notice the change of key, using value not class
deps: [],
}
]
});
}
}
Will try to add a minimal repo to recreate this issue and share with you guys, If anyone wants to explore more.

Use fetched config in forRoot

I'm writing an angular app which uses #ngx-translate. With TranslateModule.forRoot(...) i provide a TranslateLoader:
#NgModule({
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient, ConfigService, LogService]
}
})
]
})
Also i have a ConfigService which loads an config.json utilizing APP_INITIALIZER.
The problem is, the TranslateLoader needs an url from the config. But forRoot() runs before APP_INITIALIZER which leads to ConfigService not having loaded the config and an empty url.
Is there another way to do this?
Currently i'm thinking about manually bootstrapping angular.
For anyone still looking at this, I found a way to load translations using the TranslateLoader provider after App Init. The ngx-translate lib allows you to override the current loader. So we can pass a new factory after the app has completed bootstrapping.
export function HttpLoaderFactory(handler: HttpBackend, valueAvailableAfterInit) {
const http = new HttpClient(handler);
return new TranslateHttpLoader(http, valueAvailableAfterInit, '.json');
}
export class AppModule {
constructor(
private translate: TranslateService,
private handler: HttpBackend
) {}
ngDoBootstrap() {
const valueAccessableAfterBootstrap = `I'll leave this to your use-case. For me it is an environment variable overwritten via ngOnInit in app.component`;
this.translate.currentLoader = HttpLoaderFactory(this.handler, valueAccessableAfterBootstrap); // replace loader
this.translate.reloadLang('en-US').pipe(take(1)).subscribe(); // reload translations with new loader
}
}
I think the following solution is simpler, since you don't need to reload the translations:
#NgModule({
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient, AppConfigService],
},
})
]
})
export function HttpLoaderFactory(http: HttpClient, appConfigService: AppConfigService) {
return new CustomTranslateHttpLoader(http, appConfigService, '', `?v=${environment.version}`);
}
export class CustomTranslateHttpLoader extends TranslateHttpLoader {
constructor(http: HttpClient,
private readonly appConfigService: AppConfigService,
prefix?: string,
suffix?: string) {
super(http, prefix, suffix);
}
override getTranslation(lang: string): Observable<Object> {
this.prefix = `${this.appConfigService.getConfig().apiUrl}api/translations/`;
return super.getTranslation(lang);
}
}
You can retrieve your config values after the app is initialized, as soon as getTranslation is called.
You could also resolve the translation service and make the http call to the endpoint. This only works if your services resolve the api url as well.
export class CustomTranslateHttpLoader extends TranslateLoader {
constructor(private readonly injector: Injector) {
super();
}
override getTranslation(lang: string): Observable<Object> {
const translationService = this.injector.get(TranslationsService);
return translationService.getAllTranslations(lang, environment.version)
.pipe(map(response => {
return response as Object;
}));
}
}

Categories

Resources