Angular's `#Host` decorator not reaching the top? - javascript

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.

Related

Dependency inject components in Angular

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 }
]

Angular / TypeScript, when using multiple components, 1st component affect 2nd component

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

Multiple leveles nested components in Angular

I am using Angular 5+ and I want to create 3 levels up nested components.
Here is an example of what I can do.
<my-app>
<first></first>
<second></second>
</myapp>
And here is what I cannot do.
<my-app>
<first><second></second></first>
</myapp>
I have the following code in my app module.
#NgModule({
declarations: [
AboutPage,FirstComponent,SecondComponent
],
imports: [
IonicPageModule.forChild(AboutPage),
],
})
export class AppModule{}
Note here that AppModule is not the root module but it is lazyLoaded Component as well.
you will have to implement the <second></second> component inside of the <first></first>'s components template.
#Component({
selector: 'first',
template: '<second></second>'
})
export class FirstComponent { ... }
your module is correct
MyAppComponent needs to have an <ng-content> element, otherwise it won't display projected content.
Caution: This only works for components that are not the root component. Angular doesn't support projecting content to the root component. See comment below the question to see what causes the confusion.

Angular directive cannot be used?

1) Created a new directive with angularCLI.
import {Directive, ElementRef, OnInit} from '#angular/core';
#Directive({
selector: '[scrollable]'
})
export class ScrollableDirective implements OnInit{
constructor(public el:ElementRef) { }
ngOnInit(){
console.log('its working!')
}
}
2) Angular CLI automatically adds the directive to the app.module declarations
import { ScrollableDirective } from './scrollable/scrollable.directive';
#NgModule({
declarations: [
...
ScrollableDirective
],
3) Try to use the directive as an attribute
<div class="menu-container" *ngIf="menuService.showMenu" [scrollable]>
4) Resulting error
Error: Uncaught (in promise): Error: Template parse errors:
Can't bind to 'scrollable' since it isn't a known property of 'div'.
I have read the official documentation and I seem to be doing all the right things. I cannot understand what I could have missed and why the directive cannot be used.
Try adding the scrollable directive without the [] bindings:
<div class="menu-container" *ngIf="menuService.showMenu" scrollable>
[] would be if you are passing a value to the directive, but you aren't utilizing any #Input values in you directive, so it would not be needed.
The docs use the binding brackets [highlightColor]="'orange'" because it's expecting a string value from the consumer to specify a color. #Input would only be needed if you are needing a value passed to the attribute directive to use in some way.
#Kevin is right that the error is being caused by #Input not being added to the directive configuration, but in this case you don't need it, so avoid the import/export of that decorator.

Angular2: Service with Model - "no provider for model"

What I'm trying to do is create a service that uses a model to show an alert. The alert-model should be necessary nowhere else but in that service but I am not able to make this work. My service:
import {Injectable, Inject} from "angular2/core";
import {AlertModel} from "../models/alert.model";
#Injectable()
export class AlertService {
constructor(#Inject(AlertModel) alertModel: AlertModel) {
}
public alert(){
this.alertModel.message = 'success';
//...
}
}
But I keep getting this error:
Uncaught (in promise): No provider for AlertModel! (UserComponent -> AlertService -> AlertModel)
I'm new to angular and I do not understand this. What am I missing? Thanks in advance!
You need to provide the AlertModel somewhere
bootstrap(AppComponent, [AlertModel])
or in the root component (preferred):
#Component({
selector: 'my-app',
providers: [AlertModel],
...
})
Ensure AlertModel has the #Injectable() decorator and all its constructor parameters are provided as well (if it has any)
#Inject(AlertModel) is redundant if the type of the constructor parameter is already AlertModel. #Inject() is only necessary if the type differs or if AlertModel doesn't have the #Injectable() decorator.
constructor(#Inject(AlertModel) alertModel: AlertModel) {
You have this error since there is no provider for the AlertModel class visible from the UserComponent component (that calls the service). You can define either this class in the providers attribute of the component either when bootstrapping your application.
See the question to know more about how hierarchical injectors works and how to inject things into services:
What's the best way to inject one service into another in angular 2 (Beta)?
Since the AlertModel class seems to be a model class I don't think that you need to inject it. You can simply import the class and instantiate it:
#Injectable()
export class AlertService {
alertModel: AlertModel = new AlertModel();
public alert(){
this.alertModel.message = 'success';
//...
}
}

Categories

Resources