Reputation: 1321
I have angular class that represents shape. I want to be able to instantiate multiple instances of that class using constructor.
The constructor takes multiple arguments representing properties of that shape.
constructor(public center: Point, public radius: number, fillColor: string,
fillOpacity: number, strokeColor: string, strokeOpacity: number, zIndex: number)
Inside my class I want to use service that provides capability to draw shapes on the map. Is it possible to inject that service into my class and still use constructor the standard way.
So I want to do something like below and have Angular automatically resolve injected dependency.
constructor(public center: GeoPoint, public radius: number,
fillColor: string, fillOpacity: number, strokeColor: string, strokeOpacity: number,
zIndex: number, @Inject(DrawingService) drawingService: DrawingService)
Upvotes: 32
Views: 28415
Reputation: 989
There is a post from vojtajina on GitHub that provides a nice approach to this problem. This answer is just a link, but it is really better to read this in its context since there are other interesting information there:
https://github.com/angular/di.js/issues/22#issuecomment-36773343
Upvotes: 0
Reputation: 1321
I've managed to resolve my problem.
Angular 2 - 4 provides reflective injector that allows to inject dependencies outside of constructor parameters.
All I had to do was to import Reflective injector from @angular/core
.
import {ReflectiveInjector} from '@angular/core';
And then:
let injector = ReflectiveInjector.resolveAndCreate([DrawingService]);
this.drawingApi = injector.get(DrawingService);
The class doesn't even have to be decorated with the @Injectable
decorator.
The only problem is that I have to provide all dependencies for DrawingService and all the nested dependencies, so that is hard to maintain.
EDIT:
Angular 5
import { Injector } from "@angular/core";
const injector = Injector.create([
{ provide: DrawingService }
]);
this.drawingApi = injector.get(DrawingService);
Angular 6
import { Injector } from "@angular/core";
const injector = Injector.create({
providers: [
{ provide: DrawingService },
]
});
this.drawingApi = injector.get(DrawingService);
Upvotes: 46
Reputation: 3141
As of Angular 5.x:
import { Injector } from "@angular/core";
export class Model {
static api: Api;
constructor(data: any) {
// check the api ref not exist
// We don't want to initiate a new object every time
if (!Model.api){
//try inject my api service which use the HttpClient
const injector: any = Injector.create([{ provide: Api, useClass: Api, deps: [] }]);
Model.api = injector.get(Api);
}
// .....
}
}
Upvotes: 6
Reputation: 5423
Here are two other possible ways to achieve the desired or very similar result.
You have one or more factory service(s) that is/are in charge of instantiating your objects.
This means that it can provide the required deps further on to objects, and doesn't require that you pass them on yourself.
For example, say you have entities as a class hierarchy:
abstract class Entity { }
class SomeEntity extends Entity {
...
}
You can then have an EntityManager that is a service and can construct entities:
@Injectable() // is a normal service, so DI is standard
class EntityManager {
constructor(public http: Http) { } // you can inject any services now
create<E extends Entity>(entityType: { new(): E; }): E {
const entity = new entityType(); // create a new object of that type
entity.manager = this; // set itself on the object so that that object can access the injected services like http - one can also just pass the services not the manager itself
return entity;
}
}
You can also have construction parameters if you want to (but they will not have any type information because create
needs to work with all types of entities):
class SomeEntity extends Entity {
constructor(param1, param1) { ... }
}
// in EntityManager
create<E extends Entity>(entityType: { new(): E; }, ...params): E {
const entity = new entityType(...params);
...
}
Your entities can now declare the manager:
abstract class Entity {
manager: EntityManager;
}
And your entities can use it to do whatever:
class SomeEntity extends Entity {
doSomething() {
this.manager.http.request('...');
}
}
Now every time you need to create an entity/object you use this manager. The EntityManager
needs to be injected itself but the entities are free objects. But all angular code will start from a controller or service or such so it will be possible to inject the manager.
// service, controller, pipe, or any other angular-world code
constructor(private entityManager: EntityManager) {
this.entity = entityManager.create(SomeEntity);
}
This approach can also be adapted to arbitrary objects. You don't need a class hierarchy, but with typescript this works better. It also makes sense to have some base class for your objects as you can re-use code this old fashion way as well, especially in a domain/object oriented approach.
PROS: This approach is safer because it still resides on the full DI hierarchy and there should be less unwanted side-effects.
CONS: The downside is that you can never use new
ever again, nor can you obtain access to these services in arbitrary code. You always need to rely on the DI and on your factory / factories.
You create a service dedicated to obtaining (via DI) the services you need in objects.
This part is very similar to the first approach, just that this service is not a factory. Instead, it passes on the injected services into an object that is defined outside this class in a different file (explanation after). For example:
...
import { externalServices } from './external-services';
@Injectable()
export class ExternalServicesService {
constructor(http: Http, router: Router, someService: SomeService, ...) {
externalServices.http = http;
externalServices.router = router;
externalServices.someService = someService;
}
}
The object that will hold the services is defined in its own file as such:
export const externalServices: {
http,
router,
someService
} = { } as any;
Note that the services are not using any type information (this is a drawback but necessary).
Then, you must make sure that ExternalServicesService
gets injected once. Best place is to use the main app component:
export class AppComponent {
constructor(..., externalServicesService: ExternalServicesService) {
Finally, now you can use the services in any arbitrary object at any point after the main app component has been instantiated.
import { externalServices } from '../common/externalServices' // or wherever is defined
export class SomeObject() {
doSomething() {
externalServices.http().request(...) // note this will be called after ng2 app is ready for sure
}
}
Note you'll not be able to call any of these services in the class code or in objects not instantiated after the app is instantiated. But in a typical app, this should never be needed.
Now, a few explanations about this weird setup:
Why use an object externalServices
in a separate file instead of the same file or simply saving the services on the class itself (as static attributes) and why are the services untyped?
The reason is that when you're bulding the code e.g. via angular-cli / webpack with --prod
mode, it's very likely to get cyclic dependencies that cannot be resolved correctly and you'll get ugly errors that are difficult to find -
I've already been through this :).
An error of the form
Cannot read property 'prototype' of undefined
seen only when running with --prod
flag will hint to the fact that dependencies are not resolved correctly.
It's much better to make sure that ExternalServicesService
only depends on externalServices
to pass the service instances, and the application only injects ExternalServicesService
once (e.g. in your main AppComponent) then all arbitrary code / objects will only use externalServices
to obtain the services.
Thus any such code will only need to import the externalServices
which has no further deps (because the services are also not typed). If they were to import ExternalServicesService
it would have imported everything else and would not have been able to resolve the bi-directional deps statically. And this becomes a major problem in ng2/webpack when bundling for prod.
The same would happen if we were to use types for the services, because that will require imports
.
PROS: This approach is easier to use once the setup has been made, and you are free to use new
. Basically any code file can import the externalServices
and have instant access to those services you want to expose this way.
CONS: The downside is the hackish setup and the possible issues caused by cyclic deps. It is also more sensitive, as you cannot be sure externalServices
has those services right away. They will only be defined once the ng2 app starts and the ExternalServicesService
is first injected. A drawback is also that you no longer have type information on those services.
PS: I'm not sure why this topic is not more popular.
For example, being a fan of domain-oriented design, having powerful entities (e.g. with methods directed at REST calls or interacting with other services) is important and this limitation always made it difficult.
We had to overcome this limitation both in angularjs and now again in Angular2+ as it seems still not to be addressed in the library.
Upvotes: 20
Reputation: 202326
In fact, you can't. The class must be decorated with @Injectable
to let Angular2 inject things. The@Inject
decorator is "only" there to specify additional metadata about what to inject.
In your case, the class is managed by you since most of its constructor parameters don't correspond to dependencies and are provided when you explicitly instantiate the class.
Upvotes: -3