Multiple leveles nested components in Angular - javascript

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.

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 module/component - display on own as well as inside another module/component?

Let's say I have ModuleA with a router entry of:
path: 'modulea',
loadChildren: './modulea/modulea.module#ModuleA'
ComponentA (ComponentA's selector is component-a)
ModuleB with a router entry of:
path: 'moduleb',
loadChildren: './moduleb/moduleb.module#ModuleB'
ComponentB (ComponentB's selector is component-b)
ComponentA's template has <div class="component-a">...</div> and if I go to mysite.com/modulea I see everything I expect to see.
However, I cannot get it to display in ComponentB's template like so:
<div class="component-b">
Do some stuff
<div class="component-a"></div>
Do more stuff
</div>
Which kind of makes sense to me because at that point I feel like I am basically overriding ComponentA's template (even if it's failing for some other reason).
Nor can I get it to display ComponentA inside ComponentB by switching <div class="component-a"></div> with <component-a></component-a> to it.
The closest I have found to what I am trying to do is, Angular 2 component inside other component. This looks promising, but when I try to add ComponentA as a directive to ComponentB and use <component-a></component-a>, it throws an error that " 'component-a' is not a known element..."
TL;DR:
I want my module/component to display on its own page in my application, as well as be displayed inside another module/component elsewhere in the application.
You should be exporting componentA from your moduleA, so that you can safely use it in componentB, by importing ModuleA in ModuleB.
This will resolve your error
" 'component-a' is not a known element..."
moduleA.module.ts:
#NgModule({
declarations:[ComponentA],
exports:[ComponentA]
})
export class ModuleA{
}
moduleB.module.ts:
#NgModule({
declarations:[ComponentB],
imports:[ModuleA]
})
export class ModuleB{
}
componentB.component.ts
<div class="component-b">
Do some stuff
<component-a></component-a>
Do more stuff
</div>

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

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

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.

Using Angular 2 Components inside Material <md-tab-group>

I'm starting an Angular 2/Material Design project and have run into a problem when trying to use components with <md-tab-group>. I've seen the general structure examples like the following:
<md-tab-group>
<md-tab label="Gallery">
//gallery content here
</md-tab>
<md-tab label="Settings">
//setting content here
</md-tab>
</md-tab-group>
However, I'd like to modularize the functionality of each tab and it's contents into separate Angular 2 components like this:
app.component.html
<md-tab-group>
<app-gallery></app-gallery>
<app-settings></app-settings>
</md-tab-group>
The structure I have at the moment follows the basic Angular 2 component conventions, for example:
gallery.component.ts
import { Component, OnInit } from '#angular/core';
import {Http} from '#angular/http';
#Component({
selector: 'app-gallery',
templateUrl: './gallery.component.html',
styleUrls: ['./gallery.component.css']
})
export class GalleryComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}
I then use the <md-tab> element inside each component's template like so.
<md-tab label="gallery">
<p>Gallery here...</p>
</md-tab>
Finally, I've made sure everything is imported and declared properly.
app.module.ts
import { BrowserModule } from '#angular/platform-browser';
import { NgModule } from '#angular/core';
import { FormsModule } from '#angular/forms';
import { HttpModule } from '#angular/http';
import { MaterialModule } from '#angular/material';
import { AppComponent } from './core/app.component';
import 'hammerjs';
import { GalleryComponent } from './gallery/gallery.component';
#NgModule({
declarations: [
AppComponent,
GalleryComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
MaterialModule.forRoot()
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Is this possible? At the moment I'm suspecting maybe it's not possible to extract the <md-tab> element from the <md-tab-group>. To me it seems cumbersome to include all tabs and their contents in one single html file, not to mention defeating the purpose of Components. If it is possible to do I'd be grateful for any guidance on how to set it up correctly. Thanks in advance!
The components you put into tabs should not depend on the tabs. Therefore, they should not be wrapped in md-tab in their templates. Material tabs are designed as a container you can put anywhere with almost any content you desire. It serves the purpose of single responsibility, not defeats it.
EDIT
As requested, a sample of how it might look like. I assume there are three components: GaleryComponent (gallery with tabs), PicturesGridComponent (displays a grid of pictures) and PictureComponent (displays formatted picture). Templates are simplified to show the structure of nesting components.
GaleryComponent template:
<md-tab-group>
<md-tab label="cats">
<app-pictures-grid [pictures]="catPictures"></app-pictures-grid>
</md-tab>
<md-tab label="dogs">
<app-pictures-grid [pictures]="dogPictures"></app-pictures-grid>
</md-tab>
</md-tab-group>
PictureComponent template:
<app-picture *ngFor="let picture of pictures" [picture]=picture>
If tabs are an enumerable object, you can always try to create them with ngFor (I did not test it).

Categories

Resources