Is it possible to somehow dependency inject components in Angular? I would like to be able to do something similar to what you can do with services e.g.:
my.module.ts:
providers: [
{
provide: MyService,
useClass: CustomService
}
]
I have tried to use *ngIf="condition" in a wrapper component, but it will then complain about services not being provided for the components I do not wish to use.
It is fully possible if you have parent-child relationship between the component and injecting component.
so if you have the structure like this
#Component( {
selector:"app-parent",
template:" <app-child> </app-child>"
} )
export class ParentComp { ...}
you could inject parent-component inside the child component via dependency injection
#Component({
selector:"app-child",
template:"I am child"
})
export class ChildComponent{
constructor(private parentComp:ParentComponent){
}
}
Angular DI will now that you are asking for parent component that child component lives in and will inject it for you.
If you want to inject component not parent-child relationship like, so for example you want to inject the sidenav component into the some table component that lives outside the sidenav, it is hardly achiavable (not recommended also), but possible. if you want to do that, you should probably create shared service, that will share the states between these components.
Sure, you can provide any value (const, function, class) for the particular injection token. You can find some examples with components providing when we are going to make ControlValueAccessor
#Component({
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true
}
]
})
export class CustomInputComponent {...}
You can create your own injection token and provide any stuff you want and components also.
/* In tokens.ts */
const MY_TOKEN_NAME = new InjectionToken<MyAmazingComponent>('MY_TOKEN_NAME')
/* In module */
providers: [
{ provide: MY_TOKEN_NAME, useClass: MyAmazingComponent }
]
Related
I have an Angular component for a modal such that the content of the modal can be passed in like so:
SomeComponent.html:
<my-modal-component>
<div> all my content for the modal of SomeComponent goes here!</div>
</my-modal-component>
And my-modal-component handles setting up things such as the modal styling, close button, etc.
To extend some options to all of my modals and the components making use of these modals, I have created an abstract class called my-abstract-component, which is not actually a component, but a class that has a similar setup and functions that each component should have (error handling, setup/tear-down, etc.)
My problem is that I cannot get the required non-service information from SomeComponent as a modal into MyAbstractComponent.
SomeComponent.ts, the component with all the content that will go into the modal.
#Component({ ... })
export class SomeComponent extends MyModalComponent{
public constructor(
#Inject(LoggerService) logger: LoggerService, // these services are injected as normal
#Inject(NgbActiveModal) activeModal: NgbActiveModal,
) {
super(activeModal, logger, Components.MyModalComponent);
}
...
}
MyModalComponent.ts, the modal component that takes in content but also has its own content. It contains some shared modal functionality like closing the modal. It extends MyAbstractComponent since it is also a component.
#Component({...}) // this is a real component with a view and module, so it's tagged as such
export class MyModalComponent extends MyAbstractComponent{
public constructor(
protected activeModal: NgbActiveModal,
protected logger: LoggerService,
component: IComponent, // THIS IS MY PROBLEM
) {
super(logger, component);
}
...
}
MyModalModule.ts
#NgModule({
declarations: [ MyModalComponent],
imports: [
CommonModule,
NgbModule
],
providers: [
LoggerService
// is it possible to somehow put the IComponent here?
],
exports: [ MyModalComponent, ],
entryComponents: [ MyModalComponent, ]
})
export class MyModalModule{ }
MyAbstractComponent.ts, the abstract component for all components, includes common functions like logging errors, setup, tear-down, etc.
/* Acts like a component, but is not flagged as a real component because it doesn't have a view or module*/
export abstract class MyAbstractComponent implements OnInit, OnDestroy {
public constructor(
protected logger: LoggerService,
protected component: IComponent, // content passed in the constructor because it's not real dependency injection of services
) {
this.logger= logger;
this.component = component;
}
...
}
The information I'm trying to pass is information used for logging, here is an example of Components.MyModalComponent:
export namespace Components {
export const MyModalComponent: IComponent = {
userFriendlyName: "My Modal Component",
...
};
}
As you can see, I need to get some component information from the top level component class (SomeComponent) down multiple levels into MyAbstractComponent`. If I only had to go through one level, I can pass the component info via the super constructor. However, since this component info needs to go through a second layer of a component where only services can be part of the injection in the component, I receive this error regarding the DI:
ERROR in Can't resolve all parameters for MyModalComponent in MyModalComponent.ts: ([object Object], [object Object], ?).
Where the ? is the component info that is not a service.
I could pass in the required info in different ways (via #Input or with a getter/setter), but in a perfect world everything would be ready in the constructor. I feel confident someone else has faced and solved this issue before. Any thoughts?
I have a module in Angular that is structured likes this:
moduleName
componentA
componentB
Now componentA and componentB are very similar, as they share some attributes and methods, e.g.:
protected available: boolean = true;
As I don't want to repeat myself, I've created a base class, that stores all this:
export abstract class BaseComponent {
protected available: boolean = true;
}
And both controllers inherit from that class:
import { BaseComponent } from '../base.component';
export class ComponentA extends BaseComponent implements OnInit {
constructor() {
super();
}
ngOnInit() {
console.log(this.available);
}
}
This works just fine. However, when I research this soultion a lot of people are saying:
Don't use inheritance, use composition in this case.
Alright, but how can I use composition instead? And is the gain really that big over the current solution?
Thanks a lot for your time.
For composing objects in angular you need to have a reference to that object inside of your class, which shares data and functionality. To do that you need to use Angular services, and inject them to your class, and there should be 1 instance of service per component.
Create a new service by running ng g s my-service, remove providedIn: 'root' from your service annotation (We want to provide instance per component)
Add public available: boolean = true; to the service
provide the service through the components, in #Component configs on your components
inject the service in your both component constructors, constructor(private myService:MyService)
Now you have a composition that keeps data and functionality
#Component({
selector: 'app',
templateUrl: './app.my-component.html',
styleUrls: ['./app.my-component.css'],
providers: [MyService]
})
export class MyComponent {
constructor(private myService: MyService) {
}
}
If you create same components with big part same logic. you can use inheritance for example controlSelectComponent and controlInputComponent stackblitz example
For composition you need to create service and provide it to both components. But you dont keep component state in service becose all service are singletone. And when one component change state another component crash.
You also can provide service to each component in providers section
#Component({
selector: 'app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [MyService]
})
export class AppComponent {
constructor(private myService: MyService) {
}
}
But in case with saving state in service is not the best solution
Conclusion
Use services and composition for share helper methods between components.
Use abstract class and inheritance for components with same logic and state changes.
I would also recommend to read about Composition over Inheritance. The syntax(InversifyJs) is very similar that Angular uses. Please see this blog
I created a library Angular-Slickgrid which is a wrapper of a jQuery data grid library and is Open Source. It all work nicely when there's only 1 grid (component) on the page but when I want to create 2 of these components (same selector) on the same page, I start to get lot of weird behaviors. The behavior I can see is that some of 1st functions affects the 2nd grid. I can deal with the Services singleton, but in my case it's really the properties of the component that get override by the last created component, why is that? I thought each Angular components were totally independent (apart from the Services), so what am I doing wrong?
I use ng-packagr to create my lib and the ngModule of the component is the following
#NgModule({
imports: [
CommonModule,
TranslateModule
],
declarations: [
AngularSlickgridComponent,
SlickPaginationComponent
],
exports: [
AngularSlickgridComponent,
SlickPaginationComponent
],
entryComponents: [AngularSlickgridComponent]
})
export class AngularSlickgridModule {
static forRoot(config: GridOption = {}) {
return {
ngModule: AngularSlickgridModule,
providers: [
{provide: 'config', useValue: config},
CollectionService,
ControlAndPluginService,
ExportService,
FilterService,
GraphqlService,
GridEventService,
GridExtraService,
GridOdataService,
GridStateService,
GroupingAndColspanService,
OdataService,
ResizerService,
SharedService,
SortService
]
};
}
}
The component class starts with
#Injectable()
#Component({
selector: 'angular-slickgrid',
templateUrl: './angular-slickgrid.component.html',
providers: [ResizerService]
})
export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnInit {
Then in my App, I call the external grid module like this
imports: [
AppRoutingRoutingModule,
BrowserModule,
HttpClientModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: (createTranslateLoader),
deps: [HttpClient]
}
}),
AngularSlickgridModule.forRoot({
// add any Global Grid Options/Config you might wantApp
enableAutoResize: true
})
],
Then I can create 2 grids in my View like this
<angular-slickgrid gridId="grid1"
[columnDefinitions]="columnDefinitions"
[gridOptions]="gridOptions"
gridHeight="200"
gridWidth="800"
[dataset]="dataset">
</angular-slickgrid>
<hr>
<angular-slickgrid gridId="grid2"
[columnDefinitions]="columnDefinitions2"
[gridOptions]="gridOptions2"
gridHeight="200"
gridWidth="800"
[dataset]="dataset2">
</angular-slickgrid>
After spending a lot of time debugging, I did find out that the 1st component completely override the properties of the 2nd component. If I destroy the 2nd component, it doesn't fix the issue. For example, I click on a column to sort it on both grid, when I click on "clearSort()" from the 1st grid, it actually clears the sort of the 2nd grid!? I also found that properties of only the last created grid remains, if I click on "clearSort()" from 1st or 2nd grid, it will clear it in the 2nd always.
I know how to deal with Services Singleton, but my issue is really the properties of the class that are somehow shared by the 2 components... or to put it in another perspective, 1st component class properties get overridden by 2nd component properties
Is there something that I'm missing to make these 2 components completely independent? I have been searching and trying for couple of hours already, is that even possible or is that normal behavior?
EDIT
If you want to see all the code, everything is available from GitHub, you can see the 2 grids code (which is currently on a separate branch):
View
Component
App Module
Library Component
Library Module
EDIT 2
After all these hours, I found out that it was related to Services Singleton. Answered my own question down below. Hopefully this will help someone else facing similar issues.
See below for the behavior, watch the data but also the blue sort icons, it all happens on the 2nd grid while I do the action on 1st grid
Both component instances, even of the same component class, should have their own scope. Their variables are encapsulated and unique if they aren't declared as static.
Are you sure that dataset and dataset2 do not share the same reference? Avoid following, even for tests:
private dataset = [data1, data2];
private dataset2 = dataset;
That would enforce the described weired behaviour if you input dataset and dataset2 to two different components.
You are wrapping a jquery plugin which itself is plain javascript. Maybe the wrapped javascript is revoking angulars component scoping?
Are you sure that component instances do not share data by services mistakenly?
Wow I found the issue and I did not expect what I found to be the issue... My library had no providers in it, and so all Services were acting as Singleton. Because of that, any Services function call were using the internal variables (grid, gridOptions, dataView) of the last created grid. So the only thing that I had to do, in order to fix this, was to provide all Services into the providers array.
BEFORE
#Injectable()
#Component({
selector: 'angular-slickgrid',
templateUrl: './angular-slickgrid.component.html',
providers: [ResizerService]
})
export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnInit {
AFTER
#Injectable()
#Component({
selector: 'angular-slickgrid',
templateUrl: './angular-slickgrid.component.html',
providers: [
ControlAndPluginService,
ExportService,
FilterService,
GraphqlService,
GridEventService,
GridExtraService,
GridStateService,
GroupingAndColspanService,
ResizerService,
SortService
]
})
export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnInit {
Oh my... so many hours wasted for such simple thing
In my main app.ts I've declared a global provider :
providers: [{provide: Dependency, useValue: createDependency('AppModule provider')}]
(Where createDependency is just a function that returns a class which has a getName() method.)
I also have a components :
<my-app-component-3>Hello from 3</my-app-component-3>
Code :
#Component({
selector: 'my-app-component-3',
template: `
<div>Component3:
<ng-content></ng-content>
: <span [innerHTML]="dependency?.getName()"></span>
</div>
`,
})
export class Component3 {
constructor(#Host() #Optional() public dependency: Dependency) {}
}
The result is:
Component3: Hello from 3 :
But I expect the result to be :
Component3: Hello from 3 :AppModule provider
Because basically the app structure is :
<my-app>
<my-app-component-3>
</my-app-component-3>
</my-app>
Question:
Why doesn't #Host() match the parent provider ?
(which is : providers: [{provide: Dependency, useValue: createDependency('AppModule provider')}])
To my knowledge - the injector should seek for a Dependency in this manner :
So why doesn't it find it ?
PLUNKER
Notice
I already know that if I remove #host - it does reach the top. My question is why adding #host - is not reaching the top - despite the fact thatmy-component3 is under my-app !!
Check out A curios case of the #Host decorator and Element Injectors in Angular for in-depth explanation of how #Host decorator works and where Element Injectors come into this picture.
In order for it to work you should define dependencies in the in the parent component and using viewProviders:
#Component({
selector: 'my-app',
viewProviders: [{provide: Dependency, useValue: createDependency('AppModule provider')}],
...
export class MyApp {}
Here is what the comments inside metadata.ts say:
Specifies that an injector should retrieve a dependency from any
injector until reaching the host element of the current component.
So basically it says that a host element injector and all injectors above are not used when resolving a dependency. So if your MyApp component has the following template:
<my-app-component-3></my-app-component-3>
and the resulting components tree look like this:
<my-app>
<my-app-component-3></my-app-component-3>
</my-app>
neither MyApp component's injector nor App module injectors are used to resolve dependency for the my-app-component-3.
However, there's the following interesting code in the ProviderElementContext._getDependency that performs one additional check:
// check #Host restriction
if (!result) {
if (!dep.isHost || this.viewContext.component.isHost ||
this.viewContext.component.type.reference === tokenReference(dep.token !) ||
// this line
this.viewContext.viewProviders.get(tokenReference(dep.token !)) != null) { <------
result = dep;
} else {
result = dep.isOptional ? result = {isValue: true, value: null} : null;
}
}
which basically checks if the provider is defined in the viewProviders and resolves it if found. That's why viewProviders work.
So, here is the lookup tree:
Usage
This decorator is mostly used for directives to resolve providers from the parent injector within the current component view. Even the unit test is written only to test directives. Here is a real example from the forms module how it's decorator is used.
Consider this template for the A component:
<form name="b">
<input NgModel>
</form>
NgModel directive wants to resolve a provider supplied by the form directive. But if the provider is not available, there's no need to go outside of a current component A.
So NgModel is defined like this:
export class NgModel {
constructor(#Optional() #Host() parent: ControlContainer...)
While form directive is defined like this:
#Directive({
selector: '[formGroup]',
providers: [{ provide: ControlContainer, useExisting: FormGroupDirective }],
...
})
export class NgForm
Also, a directive can inject dependencies defined by its hosting component if they are defined with viewProviders. For example, if MyApp component is defined like this:
#Component({
selector: 'my-app',
viewProviders: [Dependency],
template: `<div provider-dir></div>`
})
export class AppComponent {}
the Dependency will be resolved.
I wonder if the #Optional() is injecting null. I believe that one might be the culprit.
Edit
So from your plunker I can’t seem to find an actual host for the component 3. Something like
<parent-component>
<component-3><component-3/>
<parent-component/>
On my understanding here it seems what it’s looking for.
just remove #Host() decorator from your Component 3 constructor:
Component({
selector: 'my-app-component-3',
template: `
<div>Component3:
<ng-content></ng-content>
: <span [innerHTML]="dependency?.getName()"></span></div>
`,
})
export class Component3 {
constructor(#Optional() public dependency: Dependency) {}
}
Angular will take the provider from the AppModule.
straight from Angular's docs on dependency injection and the Host decorator: https://angular.io/guide/dependency-injection-in-action#qualify-dependency-lookup-with-optional-and-host
The #Host decorator stops the upward search at the host component.
The host component is typically the component requesting the dependency.
with the #Host decorator, you're telling it to only check the host component for a provider, and you're making it optional, so it's just seeing there's no provider and quitting.
In practice, the use case for the Host decorator is extremely narrow, and really only ever makes sense if you're projecting content.
I want to provide a set of generic components, so they will be not aware of services that provides dependencies. Dependencies such components are promises.
In other words I want to keep for instance data access out of the scope of those generic components. Any dependencies, especially data to render and component configuration should be provided to components by the context that are declaring the component.
This is easy when I declare component in view as a DOM tag e.g.:
<generic-component data="getSomeData()" configuration="componentConfig"></generic-component>
But how I can handle that when component is invoked directly be the route?
I've read very similar issue but answer for the question definitely did not satisfy me. Accepted answer advice to put dependencies into component, but that means losing generic manner of component.
In Angular 1 approach to do so was by using resolve property of route declaration. What is equivalent of Angular's 1 resolve in Angular 2?
Please refer to the mentioned question's example cause it's very accurate.
Angular 2 in RC 4 introduced resolve property of Route.
This property is object with properties which implements Resolve interface.
Each resolver must be #Injectable and has method resolve which return Observable|Promise|any.
When You inject ActivatedRoute as route into component You can access each resolved property from route.snapshod.data['someResolveKey'].
Example from angular.io doc:
class Backend {
fetchTeam(id: string) {
return 'someTeam';
}
}
#Injectable()
class TeamResolver implements Resolve<Team> {
constructor(private backend: Backend) {}
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<any>|Promise<any>|any {
return this.backend.fetchTeam(route.params.id);
}
}
#NgModule({
imports: [
RouterModule.forRoot([
{
path: 'team/:id',
component: TeamCmp,
resolve: {
team: TeamResolver
}
}
])
],
providers: [TeamResolver]
})
class AppModule {}
Or You can also provide a function with the same signature instead of the class.
#NgModule({
imports: [
RouterModule.forRoot([
{
path: 'team/:id',
component: TeamCmp,
resolve: {
team: 'teamResolver'
}
}
])
],
providers: [
{
provide: 'teamResolver',
useValue: (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => 'team'
}
]
})
class AppModule {}
And You can get data in component:
export class SomeComponent implements OnInit {
resource : string;
team : string;
constructor(private route: ActivatedRoute) {
}
ngOnInit() {
this.team = this.route.snapshot.data['team'];
// UPDATE: ngOnInit will be fired once,
// even If you use same component for different routes.
// If You want to rebind data when You change route
// You should not use snapshot but subscribe on
// this.route.data or this.route.params eg.:
this.route.params.subscribe((params: Params) => this.resource = params['resource']);
this.route.data.subscribe((data: any) => this.team = data['team']);
}
}
Hope it helps,
happy hacking!
I experience exactly the same issue.
Route's dedicated components which will holds generics components could be solutions. But this is not elegant, it is rather bypass then solution.