I'm planning on making a standalone Angular service for Angular 8+, and I've been reading up on how to make it tree tree-shakable.
I believe all we need to do is this:
#Injectable({
providedIn: 'root',
useFactory: () => new Service('dependency'),
})
export class Service {
constructor(private dep: string) {
}
}
As this tells Angular how to construct the service.
Is this all we need to do? I've seen other NPM modules like this one use ModuleWithProviders like this:
export class Module {
static forRoot(): ModuleWithProviders {
return {
ngModule: Module,
providers: [
StripeScriptTag
],
}
}
I assume that this is not necessary because the providers array still does not describe how to instantiate the service.
All you need is the
providedIn: 'root'
Services that are provided in root are tree shakeable. If it is provided in the root then you get the same singleton instance in the entire application. You only need to add it to a providers array if you want a new instance of the service across your module instead of the global singleton instance.
Related
I am trying to implement a global module in nest.js
I have created a service like below
export interface ConfigData {
DB_NAME: string;
}
#Injectable()
export class ConfigManager {
private static _instance?: ConfigManager;
public static configData:ConfigData | null;
private constructor() {
console.log(ConfigManager)
if (ConfigManager._instance)
ConfigManager._instance = this;
else{
ConfigManager._instance = new ConfigManager()
ConfigManager.configData = <ConfigData|null><unknown>ConfigManager.fetchSecretData()
}
}
private static async fetchSecretData():Promise<ConfigData|null>{
// some data from db
}
// static get instance() {
// return ConfigManager._instance ?? (ConfigManager._instance = new ConfigManager());
// //return ConfigManager._instance ?? (ConfigManager._instance = ConfigManager.fetchSecretData()) //new ConfigManager());
// }
}
configuration.module.ts
#Global()
#Module({
providers: [ConfigManager],
exports: [ConfigManager],
})
export class ConfigurationModule {}
and in app.module.ts added ConfigurationModule in imports.
Also adding private constructor on service unable it to add in module.ts file.
I am expecting that I should be able to configData anywhere without importing the ConfigManager. but it's not working...
ConfigManager is not available without import.
You've only marked your module with #Global decorator, but the NestJS needs to somehow initialize that module and make it globally available.
What this means is that you have to add this module to your core application module and NestJS will do the rest for you, so something like this (or however your root module is named):
#Module({
imports: [ConfigurationModule],
})
export class AppModule {}
From the documentation
The #Global() decorator makes the module global-scoped. Global modules
should be registered only once, generally by the root or core module.
Global modules in nest only means you don't have to include that module in the imports: [] of every other module that requires it's providers. The providers in the global module still behaves as as normal providers i.e. you need to inject it where you need it.
So in your case since you have already added #Global() to ConfigManager and imported ConfigurationModule in app.module.ts, you will not need to add ConfigurationModule in imports for any other modules who want to use ConfigManager. You would, however, still need to inject the ConfigManager provider - that's what #Injectable() means :).
To do the injection, you need a constructor(private configManager: ConfigManager) {} in the class that needs to use ConfigManager, and since you need access to the type of the class, you'll need import { ConfigManager } from '../path/to/ConfigManager' as well.
this is 'per-design' of es6/ts: you cant use the class without importing it.
you are mixing the concepts of di (instantiation/composition) with importing (defining which classes are available in the module scope)
The structure of my Angular App (and relevant portions) is as follows:
app/app.module.ts
import { DbService } from './db.service'
const APP_CONTAINERS = [
DefaultLayoutComponent,
DefaultHeaderComponent,
];
#NgModule({
declarations: [ ...APP_CONTAINERS ],
providers: [ DbService ]
})
export class AppModule {}
app/containers/default-layout.component.ts
import { Component, HostBinding, Inject } from '#angular/core';
import { DbService } from '../../db.service';
import { navItems } from './_nav';
import { Injectable } from '#angular/core';
#Injectable({
providedIn: 'root'
})
#Component({
selector: 'app-dashboard',
templateUrl: './default-layout.component.html'
})
export class DefaultLayoutComponent {
#HostBinding('class.c-app') cAppClass = true;
constructor(private DbService: DbService) {}
async ngOnInit() {
console.log('working')
}
}
I use this same service successfully in other portions of my site but they all have specific module.ts files. I followed the Angular tutorial to get to where I am but I am still getting errors no matter how I try to inject this service.
I also tried using #Inject in my constructor but received the same error.
I currently receive the following error in my console:
ERROR Error: Uncaught (in promise): NullInjectorError: R3InjectorError(e)[e -> e -> e -> e]:
NullInjectorError: No provider for e!
NullInjectorError: R3InjectorError(e)[e -> e -> e -> e]:
NullInjectorError: No provider for e!
I use this same service successfully in other portions of my site but they all have specific module.ts files
So it is unclear how you divide your modules. It doesn't look from the module file like that component is in the module that provides the service. The service has to be in the providers for the injection tree where the component resides. If that module file is for a different module, then the injector has to way to know what you want provided for the service.
One solution would be to add it in the providers for your component. That would get a different instance of the service than in your other modules however:
#Component({
selector: 'app-dashboard',
templateUrl: './default-layout.component.html',
providers: [DbService]
})
export class DefaultLayoutComponent {
You could also add it to the providers for the module your component resides in, but that would get a different instance as well.
I think the best way would be to define the service so that it should be provided in the root module. This way you don't have to add it in the providers anywhere, angular will automatically add it to the root module's providers when it is required. That leads to what Octavian was saying in his comment. I think you just put this code incorrectly in the component instead of the service. Move this to the service and remove the service from the providers arrays in your module(s):
#Injectable({
providedIn: 'root'
})
I am developing an angular library. it has an internal service. which is defined like below.
Used providedIn to be tree-shakable. and didn't use providedIn:'root' because its internal and just used in the module scope.
#Injectable({
providedIn: MyChartModule,
})
export class LineChartLibService {
and when i wanted to add forRoot in module definition to have just one instance in lazy loading, it encounter Circular dependency.
export class MyChartModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: MyChartModule,
providers: [LineChartLibService],
};
}
}
what should we do in this situations?
is it possible to have a lazy load-able tree-shakable service in library?
WARNING: Circular dependency: dist\my-chart\esm2015\lib\my-chart.module.js -> dist\my-chart\esm2015\lib\line-chart\line-chart.component.js
-> dist\my-chart\esm2015\lib\line-chart-lib.service.js -> dist\my-chart\esm2015\lib\my-chart.module.js
I have previously answered a (non-duplicate) question that provides you with alternatives to this approach.
https://stackoverflow.com/a/60377431/5367916
Take this setup:
my-module.ts
declarations: [
MyComponent
]
my-service.ts
#Injectable({ providedIn: MyModule })
my-component.ts
constructor(private myService: MyService) {}
my-service imports my-module
my-module imports my-component
my-component imports my-service
There is a circular dependency.
A workaround
The workaround for this is to create a service module and import that into your module.
my-module.ts
imports: [
MyModuleServices
],
declarations: [
MyComponent
]
my-module-services.ts
// no imports or declarations
my-service.ts
#Injectable({ providedIn: MyModuleServices })
my-component.ts
constructor(private myService: MyService) {}
Alternative
The much more straightforward way is to add the service to your module's providers.
#NgModule({
providers: [ MyService ]
})
export class MyModule {}
I think what happens is the following:
You have the line-chart.component.js that probably calls for the LineChartLibService.
Within the LineChartLibService you say that this Injectable is provided in MyChartModule.
Within the MyChartModule you probably declare the line-chart.component.js to be part of this module.
This means the module asks for the component, which asks for the service, which asks for the module which then asks for the component again: A circular dependency.
If you add more code and context to your question, we might be able to make a better suggestion ;-)
In my NestJS application - I have TypeScript classes that have other classes and values injected into them. The only thing is that I'm importing the TypeScript classes with import statements, and also using the DI system to inject them. Is there some way to remove the import statements and just let the DI system handle it?
TL;DR
import -> class reference
DI -> class instantiation
Matching by string token is possible, but class reference is preferred.
Encapsulation
The dependency injection system mainly handles the instantiation of the classes. This is great, because you do not need to care about the transitive dependencies that the class you want to inject requires.
Example: I want to use the UserService in my UserController. The UserService requires the UserModel for instantiation. However, this second-level dependency is hidden in the UserController. This is great because when the UserService gets a new dependency like a LoggingService, the UserController does not have to be changed.
So instead of
class UserController {
constructor() {
const userModel = new UserModel();
this.userService = new UserService(userModel);
}
}
you can just do
class UserController {
// the transitive dependency on UserModel is hidden
constructor(private userService: UserService) {}
}
Class Reference
But for the DI to know which service to inject you need some link from the #Inject declaration to an actual class to instantiate. Of course, this mechanism depends on the implementation of the DI system. The reference could be by name (string matching), by interface (DI decides which implementation to use: UserService -> UserServiceImpl / MockUserServiceImpl) or in the default case of nestjs directly by the class to be instantiated.
Although matching by name is possible in nestjs, matching by class is preferred because it makes refactoring much easier.
When you create a custom provider you can choose what kind of token you want to use for the matching. This is needed, when you want to inject a value (no class for matching)
const connectionProvider = {
provide: 'Connection',
useValue: connection,
};
#Module({
providers: [connectionProvider],
})
or a dynamically instantiated class.
const configServiceProvider = {
provide: ConfigService,
useClass: process.env.NODE_ENV === 'development'
? DevelopmentConfigService
: ProductionConfigService,
};
#Module({
providers: [configServiceProvider],
})
I'm working on an ionic2 project and I need to create a new custom JSON config file. I found some tutorials to create one and access it through http.get but I think it's weird to call it through a get request. I want it in the root folder (where all the config JSONs are) and I open/read the file directly.
I don't know if it's possible, or even recommended ? This is why I'm posting here to have some opinions and solutions :)
Thanks
Personally I don't like the read the config.json file by using the http.get way to handle configuration information, and even though there must be another way to just include and read the json file in your code, since we're using Angular2 and Typescript, why not using classes, interfaces and doing it in a more fancy way?
What I'll show you next may seem more complicated than it should at first (although after reading it you will find it very straightforward and easy to understand), but when I started learning Angular2 I saw an example of how they handled config files in the Dependency Injection guide and I followed that in the apps I've worked on to handle config information (like API endpoints, default values, and so on).
According the docs:
Non-class dependencies
[...]
Applications often define configuration objects with lots of small
facts (like the title of the application or the address of a web API
endpoint) but these configuration objects aren't always instances of a
class.
One solution to choosing a provider token for non-class dependencies
is to define and use an OpaqueToken
So you would need to define a config object with the urls and so on, and then an OpaqueToken to be able to use it when injecting the object with your configuration.
I included all my configuration in the app-config.ts file
// Although the ApplicationConfig interface plays no role in dependency injection,
// it supports typing of the configuration object within the class.
export interface ApplicationConfig {
appName: string;
apiEndpoint: string;
}
// Configuration values for our app
export const MY_CONFIG: ApplicationConfig = {
appName: 'My new App',
apiEndpoint: 'http://www...'
};
// Create a config token to avoid naming conflicts
export const MY_CONFIG_TOKEN = new OpaqueToken('config');
What OpaqueToken is may be confusing at first, but it just a string that will avoid naming conflicts when injecting this object. You can find an amazing post about this here.
Then, you just need to include it in the page you need it like this:
import { NavController } from 'ionic-angular/index';
import { Component, OpaqueToken, Injectable, Inject } from "#angular/core";
// Import the config-related things
import { MY_CONFIG_TOKEN, MY_CONFIG, ApplicationConfig } from 'app-config.ts';
#Component({
templateUrl:"home.html",
providers: [{ provide: MY_CONFIG_TOKEN, useValue: MY_CONFIG }]
})
export class HomePage {
private appName: string;
private endPoint: string;
constructor(#Inject(MY_CONFIG_TOKEN) private config: ApplicationConfig) {
this.appName = config.appName;
this.endPoint = config.apiEndpoint;
}
}
Please notice how to include it in the providers array
providers: [{ provide: MY_CONFIG_TOKEN, useValue: MY_CONFIG }]
And how to tell the injector how it should obtain the instance of the config object
#Inject(MY_CONFIG_TOKEN) config: ApplicationConfig
UPDATE
OpaqueToken has been deprecated since v4.0.0 because it does not support type information, use InjectionToken<?> instead.
So instead of these lines:
import { OpaqueToken } from '#angular/core';
// Create a config token to avoid naming conflicts
export const MY_CONFIG_TOKEN = new OpaqueToken('config');
Now we should use
import { InjectionToken } from '#angular/core';
// Create a config token to avoid naming conflicts
export const MY_CONFIG_TOKEN = new InjectionToken<ApplicationConfig>('config');
After reading and reading different solutions I ended up using this hacky implementation. Hopefully there will be a nice and native solution available soon:
import { NgModule } from '#angular/core';
import { environment as devVariables } from './environment.dev';
import { environment as testVariables } from './environment.test';
import { environment as prodVariables } from './environment.prod';
export function environmentFactory() {
const location = window.location.host;
switch (location) {
case 'www.example.org': {
return prodVariables;
}
case 'test.example.org': {
return testVariables;
}
default: {
return devVariables;
}
}
}
#NgModule({
providers: [
{
provide: 'configuration',
useFactory: environmentFactory
}
]
})
export class EnvironmentsModule {}
and then where ever needed, e.g.:
import { Injectable, Injector, Inject } from '#angular/core';
import { AuthenticationService } from '../authentication';
#Injectable()
export class APIService {
private http: Http;
private apiURL: string;
protected authentication: AuthenticationService;
constructor(
public injector: Injector,
#Inject('configuration') public configuration: any
) {
this.http = injector.get(Http);
this.authentication = injector.get(AuthenticationService);
this.apiURL = configuration.apiURL;
};
...