I am using Angular 10.0 and I have a problem with --prod compiling.
I need to instantiate classes manually and need to support dependency injection.
The following code works fine during development to instantiate my classes:
public instantiateWithDi(parentInjector: Injector, myClass: any): any {
const reflector = ReflectiveInjector.resolveAndCreate([], parentInjector);
const newInstance = reflector.resolveAndInstantiate(myClass);
return newInstance;
}
When I build my project with --prod (or --optimization=true), then I get the following error at runtime:
ERROR Error: Cannot resolve all parameters for 'e'(?). Make sure that all the parameters are decorated with Inject or have valid type annotations and that 'e' is decorated with Injectable.
Decorating the constructor parameters of the classes with #Inject did not work either. Using injection tokens does not help as well.
The classes are already decorated with #Injectable() and in the "providers" array of their respective angular module.
I know, the ReflectiveInjector is deprecated, but simply using the get method of the injector does not work either, because it seems to cache the classes once created and does not re-instantiate them each time I call my "instantiateWithDi" method.
Example usage
I've created a small demo at stackbliz for this: https://stackblitz.com/edit/angular-plugin-mechanism?file=src/app/plugin-execution.service.ts
Basically the magic happens here (plugin-execution.service.ts):
#Injectable({
providedIn: 'root'
})
export class PluginExecutionService {
public static readonly eventListeners = [];
constructor(private injector: Injector){}
private instantiateWithDi(parentInjector: Injector, myClass: any): any {
const reflector = ReflectiveInjector.resolveAndCreate([], parentInjector);
const newInstance = reflector.resolveAndInstantiate(myClass);
return newInstance;
}
public onApplicationEvent(event: ApplicationEvent){
const injector = Injector.create({
parent: this.injector,
providers: [{
provide: ApplicationEvent,
useValue: event
}]
});
PluginExecutionService
.eventListeners
.forEach(cls => this.instantiateWithDi(injector, cls));
}
}
This allows developers to create a class and push their class into a eventListener array. It gets executed every time, an application event occurs.
See the example "plugin" some.plugin.ts in the stackblitz example.
The real usecase is of course much more complex and involves custom decorators and stuff, but that would be quite an overkill for a demo.
You see the result in the console. The "plugins" work fine as intended. But when i build it using --prod, the app does not work any longer...
Any help is very much appreciated!
Thanks,
Manuel
Related
I am new to nodejs and am trying to implement NestJS's CQRS 'recipe'. I have a service with Request scope with QueryBus injection:
#Injectable({scope: Scope.REQUEST})
export class CustomerService {
constructor(
#Inject(REQUEST) private readonly req: Request,
private readonly queryBus: QueryBus,
) {}
I have defined a handler class CustomerHandler to handle CustomerQuery:
#QueryHandler(CustomerQuery)
export class CustomerHandler implements IQueryHandler<CustomerQuery> {
constructor(
private readonly repository: CustomerRepository,
) {}
async execute(query: CustomerQuery) {
const {response, id, name} = query;
this.repository.getCustomer(response, id, name);
}
But upon execution I got an error message:
UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'getCustomer' of undefined at CustomerHandler.execute
which means, if I am not wrong, repository injection failed. (which caused code to fail for statement this.repository.getCustomer(response, id, name);)
I have added my repository class CustomerRepository in providers array of my module CustomerModule:
#Module({
imports: [Logger, CqrsModule],
providers: [CustomerService, ...QueryHandlers, CustomerRepository],
exports: [CustomerService],
})
export class CustomerModule {}
Here's my repository class, for reference:
#Injectable()
export class CustomerRepository {
constructor(
#Inject(REQUEST) private readonly req: Request,
) {}
Am I missing something here? Why is my repository class not being instantiated, if thats not the case, why is the repository injection failing in handler. Any input would be appreciated :)
Documentaion I am following: https://docs.nestjs.com/recipes/cqrs
Github example I referred: https://github.com/kamilmysliwiec/nest-cqrs-example
EDIT:
Handler (CustomerHandler) is not able to perform any sort of injection. I tried injecting logger (PinoLogger), which led to similar issue. So, it looks like, the problem is not with CustomerRepository, but with CustomerHandler.
UPDATE:
So basically, the problem is that every CqrsModule provider is
statically scoped which mean that they cannot depend on request-scoped
providers. Once you define your command handler as a request-scoped
provider, either CommandBus or QueryBus won't be able to reference it.
This is not an issue, but rather a design decision that sits behind
the entire module.
Source: https://github.com/nestjs/cqrs/issues/60#issuecomment-483288297
i.e. #QueryHandler() cannot be request scoped (source: comment on question - NestJS undefined dependencies and answer to the same https://stackoverflow.com/a/61916353/10011503).
And, this is also an open issue.
Reading nestjs doc, i saw that all handlers for command and query handlers are resolve en default scope, so, all dependencies with request or trasient scope are not provide in handlers. Solution is inject factory objects that resolve dependencies when are necesary
I'm creating a #Log() Decorator Function for debugging purposes;
I want that Decorator to delegate some of it's logic to a LoggingService that in turn depends on other services from the app...
I've been trying a lot of different things, the simplest/most straightforward way was to cache the Main (or Shared) Module's Injector as a static prop on the module itself (see StackBlitz example linked below), and that works for lazy-loaded modules, but not for eagerly loaded ones...
Non-working poc: https://stackblitz.com/edit/angular-j1bpvx?file=app%2Fdecorator.ts
Is there a way I could mkae use of that Service in there??
Thanks!
Class decorator is executed once on class definition. In order to avoid race condition when calling AppModule.injector.get(LoggingService) it should be moved to the place where AppModule.injector is already defined, i.e. class method.
It should be:
constructor.prototype[hook] = function (args) {
const loggingService = AppModule.injector.get(LoggingService);
loggingService.log({ ... })
...
This also creates tight coupling with AppModule and prevents the units from being reused or tested separately from it. It's recommended to use another object to hold injector property, e.g. assign injector not in main but in child module that is imported into AppModule:
export class InjectorContainerModule {
static injector: Injector;
constructor(injector: Injector) {
InjectorContainerModule.injector = injector;
}
}
Try stackblitz fixed
This will print
LoggingService: HelloComponent - ngOnInit was called
Minor changes - basically using ReflectiveInjector as in angular Injector#example
import { ReflectiveInjector } from '#angular/core';
const injector = ReflectiveInjector.resolveAndCreate([
{provide: 'loggingService', useClass: LoggingService}
]);
const loggingService = injector.get('loggingService');
I am sure you can use useExisting and use LoggingService as provider in your app module.
I have built a shared data service that's designed to hold the users login details which can then be used to display the username on the header, but I cant get it to work.
Here's my (abbreviated) code:
// Shared Service
#Injectable()
export class SharedDataService {
// Observable string source
private dataSource = new Subject<any>();
// Observable string stream
data$ = this.dataSource.asObservable();
// Service message commands
insertData(data: Object) {
this.dataSource.next(data)
}
}
...
// Login component
import { SharedDataService } from 'shared-data.service';
#Component({
providers: [SharedDataService]
})
export class loginComponent {
constructor(private sharedData: SharedDataService) {}
onLoginSubmit() {
// Login stuff
this.authService.login(loginInfo).subscribe(data => {
this.sharedData.insertData({'name':'TEST'});
}
}
}
...
// Header component
import { SharedDataService } from 'shared-data.service';
#Component({
providers: [SharedDataService]
})
export class headerComponent implements OnInit {
greeting: string;
constructor(private sharedData: SharedDataService) {}
ngOnInit() {
this.sharedData.data$.subscribe(data => {
console.log('onInit',data)
this.greeting = data.name
});
}
}
I can add a console log in the service insertData() method which shoes the model being updated, but the OnInit method doesn't reflect the change.
The code I've written is very much inspired by this plunkr which does work, so I am at a loss as to what's wrong.
Before posting here I tried a few other attempts. This one and this one again both work on the demo, but not in my app.
I'm using Angular 2.4.8.
Looking through different tutorials and forum posts all show similar examples of how to get a shared service working, so I guess I am doing something wrong. I'm fairly new to building with Angular 2 coming from an AngularJS background and this is the first thing that has me truly stuck.
Thanks
This seems to be a recurring problem in understanding Angular's dependency injection.
The basic issue is in how you are configuring the providers of your service.
The short version:
Always configure your providers at the NgModule level UNLESS you want a separate instance for a specific component. Only then do you add it to the providers array of the component that you want the separate instance of.
The long version:
Angular's new dependency injection system allows for you to have multiple instances of services if you so which (which is in contrast to AngularJS i.e. Angular 1 which ONLY allowed singletons). If you configure the provider for your service at the NgModule level, you'll get a singleton of your service that is shared by all components/services etc. But, if you configure a component to also have a provider, then that component (and all its subcomponents) will get a different instance of the service that they can all share. This option allows for some powerful options if you so require.
That's the basic model. It, is of course, not quite so simple, but that basic rule of configuring your providers at the NgModule level by default unless you explicitly want a different instance for a specific component will carry you far.
And when you want to dive deeper, check out the official Angular docs
Also note that lazy loading complicates this basic rule as well, so again, check the docs.
EDIT:
So for your specific situation,
#Component({
providers: [SharedDataService] <--- remove this line from both of your components, and add that line to your NgModule configuration instead
})
Add it in #NgModule.providers array of your AppModule:
if you add it in #Component.providers array then you are limiting the scope of SharedDataService instance to that component and its children.
in other words each component has its own injector which means that headerComponentwill make its own instance of SharedDataServiceand loginComponent will make its own instance.
My case is that I forget to configure my imports to add HttpClientModule in #NgModules, it works.
I'm trying to use Flowtype in an AngularJS (1.5) project but it complains about the $inject annotation. What is the correct way to handle this?
Flow version 0.30.0
Example code
navigation/links-controller.js
export default class LinksController {
constructor(navigationService) {
this.availableLinks = navigationService.availableLinks;
}
}
LinksController.$inject = ['NavigationService'];
navigation/index.js
...
import NavigationService from './navigation-service';
import LinksController from './links-controller';
export default angular.module('app.links', [uirouter])
.config(routing)
.service('NavigationService', NavigationService)
.controller('LinksController', LinksController)
.name;
Example flowtype output
LinksController.$inject = ['NavigationService'];
^^^^^^^ property `$inject`. Property not found
The reason for this is that by creating a class you are defining its interface in Flow.
When you are assigning $inject to the class you are effectively adding a new property that was not defined in the class interface and that is a type error in Flow.
You have two options for making this type check in Flow:
Adding a static property type definition:
class LinksController {
static $inject: Array<string>;
constructor() {...}
}
LinksController.$inject = ['NavigationService'];
Adding a static property:
class LinksController {
static $inject = ['NavigationService'];
constructor() {...}
}
With the second option you are going to need to enable esproposal.class_static_fields=enable within the [options] section of your .flowconfig
Because this is a proposal not yet added to the JavaScript standard and not available in any browsers, you will also need to compile it with something like Babel (you'll need either the stage-2 preset or the transform-class-properties plugin).
If you are using external dependency injection then you might want to link the functions like below.
angular
.module('app',[])
.controller('LinkCtrl', LinkCtrl);
LinkCtrl.$inject = ['NavigationService'];
Can you please share your full snipper if you have exactly done like above?
I have a typescript class representing a model and I would like instances to communicate with an API via angular's Http service.
But the constructor of the model needs arguments when creating instances. For example something super simple:
class SomeModel{
constructor(public id:number, public name:string, ){
}
I would like to inject the Http service so it is available to my instances, but it seems like the canonical way to do this commandeers the constructor with:
constructor(http:Http)
I've been digging through the Injector docs, but it's a little sparse and I haven't found anything that works. Is there a way to get a reference to a service like Http from the DI system without using the constructor pattern?
I managed to solve the same problem using angular 4. First you create new injector that uses component injector. It knows about your SomeModel class and passes modelParams as instance of SomeModelParameters class. Then you use this newly created injector to create class instance.
#Injectable()
class SomeModel {
constructor(http: Http, someModelParamters: SomeModelParameters) { }
}
export class MyComponent {
constructor(injector: Injector) {
const modelParams = new SomeModelParameters();
const injectorWithModelParams = ReflectiveInjector.resolveAndCreate(
[
SomeModel,
{ provide: SomeModelParameters, useValue: modelParams }
],
injector);
this.someModel = injectorWithModelParams.resolveAndInstantiate([SomeModel]);
}
}
update
HTTP_PROVIDERS is long gone.
HttpClientModule is the current replacement.
original
If you inject a class that has constructor parameters the #Injectable annotation needs to be added.
#Injectable()
class SomeModel{
// constructor(public id:number, public name:string, ){
// }
constructor(http:Http) {}
}
For this to work HTTP_PROVIDERS needs to be added to bootstrap(AppComponent, [HTTP_PROVIDERS]);
See also Angular2 beta - bootstrapping HTTP_PROVIDERS - "Unexpected Token <"
If you need to pass other arguments from your component, youcoud pass them using a function instead.
Another way is to create the instance manually and request Http from the injector.
export class MyComponent {
constructor(injector: Injector) {
this.someModel = new SomeModel(Injector.resolveAndCreate(Http), 1, 2);
}
}