I am not getting my components to display when they are within an ng-container. I have an array of items that I am looping over where each item has a boolean value of whether or not the user can see the item based on their access. Since I cannot place two structural directives on an element I decided to use an ng-container for the ngFor and then place the ngIf on the component.
This is the primary side nav component which displays a list of side nav items.
#Component({
selector: 'side-nav',
template: '<ng-content select="side-nav-item"></ng-content>'
})
export class SideNav { }
This is an example side nav item (for this purpose it does nothing except display an h2):
#Component({
selector: 'side-nav-item',
template: '<h2>Side Nav Item</h2>'
})
export class SideNavItem { }
This utilizes the two above components
#Component({
selector: 'main-nav',
template: `
<side-nav>
<ng-container *ngFor="let item of sideNavItems">
<side-nav-item *ngIf="item.hasAccess"></side-nav-item>
</ng-container>
</side-nav>
<side-nav>
<side-nav-item *ngFor="let item of sideNavItems"></side-nav-item>
</side-nav>
`
})
export class MainNav {
sideNavItems: ISidNavItem[] = [
{
hasAccess: true, uri: '/dashboard'
}
]
}
In this StackBlitz I would like to see two components printed, but I am only getting the last one because it is not inside of an ng-container. How can I get this working using this ngFor and ngIf?
You can use ngProjectAs in your MainNav template.
#Component({
selector: 'main-nav',
template: `
<side-nav>
<ng-container *ngFor="let item of sideNavItems" ngProjectAs="side-nav-item">
<side-nav-item *ngIf="item.hasAccess"></side-nav-item>
</ng-container>
</side-nav>
<side-nav>
<side-nav-item *ngFor="let item of sideNavItems"></side-nav-item>
</side-nav>
`
})
export class MainNav {
sideNavItems: ISidNavItem[] = [
{
hasAccess: true, uri: '/dashboard'
}
]
}
This way the select of the ng-content will read the ng-container as a side-nav-item.
Related
currently I trying to project a third component in a child component which is projected inside ngFor loop (inside child), but in parent whenever I change or set some property in the projected content using index of query list (ViewChildren('#thirdComponent')) in parent all the child's projected content shows same change. Is there any proper way of doing this.
Is it due to duplicating of select property binding at the place of content projection in child component.Child's projection is done inside a accordion with one or many panels opened at a time.
#Component({
selector: "my-app",
template: `
<child-comp #child>
<ng-container selected>
<some-other-comp #someOtherComp></some-other-comp>
</ng-container>
</child-comp>
`,
styleUrls: ["./app.component.css"]
})
export class AppComponent implements AfterViewInit {
h = 0;
i = 1;
j = 2;
k = 3;
#ViewChildren("someOtherComp") otherCompList: QueryList<SomeOtherComponent>;
ngAfterViewInit(): void {
this.otherCompList.toArray()[this.h].prop = this.h;
// below will result in undefined due to QueryList of size 1
// this.otherCompList.toArray()[this.i].prop = this.i;
// this.otherCompList.toArray()[this.j].prop = this.j;
// this.otherCompList.toArray()[this.k].prop = this.k;
}
}
#Component({
selector: "child-comp",
template: `
<div *ngFor="let value of [1, 2, 3]; let i = index">
<!-- if ngIf is removed than only the last projection is diplayed -->
<div *ngIf="i === 0">
<ng-content select="[selected]"> </ng-content>
</div>
</div>
`,
styleUrls: ["./app.component.css"]
})
export class ChildComponent {}
#Component({
selector: "some-other-comp",
template: `
<p>{{ prop }}</p>
`,
styleUrls: ["./app.component.css"]
})
export class SomeOtherComponent {
prop: any;
}
Stackblitz
Utilizing *ngTemplateOutlet and let-variables
We can pass along a template into our child-component, and utilize the #Input() decorator in conjunction with *ngTemplateOutlet to directly access the property from the HTML template in the parent.
Example
First, I've defined an array in my parent component which I want to use as the basis for my loop in my outer-child component.
Parent Component
#Component({
selector: 'parent',
templateUrl: 'parent.component.html',
styleUrls: ['parent.component.scss']
})
export class ParentComponent implements OnInit {
dataItems: { title: string, description: string }[] = [{
title: 'First Element',
description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eveniet, nihil!'
}...] // remaining items truncated for brevity.
constructor() {
}
ngOnInit(): void {
}
}
This parent component then has a child component, which takes an input of the entire list of items
<child [items]="dataItems"></child>
Child-Component (fist level)
#Component({
selector: 'child',
templateUrl: 'child.component.html',
styleUrls: ['child.component.scss']
})
export class ChildComponent implements OnInit {
#Input() items!: any[];
constructor() {
}
ngOnInit(): void {
}
}
<ng-container *ngFor="let childItem of items">
<projected [item]="childItem">
<ng-template let-item>
<h4>{{item.title}}</h4>
<p>{{item.description}}</p>
</ng-template>
</projected>
</ng-container>
Projected component (sub-child)
#Component({
selector: 'projected',
templateUrl: 'projected.component.html',
styleUrls: ['projected.component.scss']
})
export class ProjectedComponent implements OnInit {
#Input() item: any;
#ContentChild(TemplateRef) templateOutlet!: TemplateRef<any>
constructor() {
}
ngOnInit(): void {
}
}
<ng-container *ngTemplateOutlet="templateOutlet; context: {$implicit: item}"></ng-container>
<ng-content></ng-content>
How does it work
The Parent Component isn't strictly necessary in this relationship, as we aren't projecting content directly from the parent into the ProjectedComponent, I simply chose to define a list of items here to keep a hierarchy similar to your question.
The Child Component
The child component does two things:
Defines a *ngFor loop to loop thru some collection of elements.
Defines a template for how these elements should be utilized in the ProjectedComponent's template.
In the ProjectedComponent we utilize the #ContentChild decorator to select the TemplateRef which we expect to be given via <ng-content>
This template is then put into a container using the *ngTemplateOutlet which also allows us to create a data-binding context to a local variable.
the context: {$implicit: item} tells Angular that any let-* variable defined on the template without any explicit binding should bind to the item property in our component.
Thus, we are able to reference this property in the template at the parent-component level.
Edit
Technically, the context binding is not necessary if you want to define the template directly inside of the child component, as you have a direct reference to the *ngFor template, however it becomes necessary if you want to lift the template out to the ParentComponent level to make the solution more reusable.
You are correct the reason for the bug (changing just the last element) is because when rendered you have multiple elements with the same select value.
A possible solution is to use template reference to pass the desired child component from the top level to the place where you want it to be projected.
Here is a working StackBlitz
import {
AfterViewInit,
Component,
Input,
QueryList,
ViewChildren
} from "#angular/core";
#Component({
selector: "my-app",
template: `
<child-comp #child [templateRef]="templateRef"> </child-comp>
<ng-template #templateRef>
<some-other-comp #someOtherComp></some-other-comp>
</ng-template>
`,
styleUrls: ["./app.component.css"]
})
export class AppComponent implements AfterViewInit {
h = 0;
i = 1;
j = 2;
k = 3;
#ViewChildren("someOtherComp") otherCompList: QueryList<SomeOtherComponent>;
ngAfterViewInit(): void {
this.otherCompList.toArray()[this.h].prop = this.h;
this.otherCompList.toArray()[this.i].prop = this.i;
this.otherCompList.toArray()[this.j].prop = this.j;
this.otherCompList.toArray()[this.k].prop = this.k;
}
}
#Component({
selector: "child-comp",
template: `
<div *ngFor="let value of [1, 2, 3, 4]; let i = index">
<!-- if ngIf is removed than only the last projection is diplayed -->
<ng-container *ngTemplateOutlet="templateRef"></ng-container>
</div>
`,
styleUrls: ["./app.component.css"]
})
export class ChildComponent {
#Input() templateRef;
}
#Component({
selector: "some-other-comp",
template: `
<p>{{ prop }}</p>
`,
styleUrls: ["./app.component.css"]
})
export class SomeOtherComponent {
prop: any;
}
THE PROBLEM
So I have two Angular components, a parent and a child. The parent passes a custom template to the child component, which then hydrates the template with its own data using ngTemplateOutlet.
This works well for the most part. Unfortunately, I run into issues when trying to access the DOM elements of this parent template from the child.
If I try to access <div #container></div> from the default child template using #ViewChild('container',{static: false}), it gets the element without issue. When I do the same using the custom template passed in by app.component, I get the error "cannot read property 'nativeElement' of undefined".
What else do I have to do to access the DOM of my template?
Here's a Stackblitz
App.Component (Parent)
import { Component } from "#angular/core";
#Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {}
<child [customTemplate]="parentTemplate"></child>
<ng-template #parentTemplate let-context="context">
<div #container>HELLO FROM CONTAINER</div>
<button (click)="context.toggleShow()">Toggle Display</button>
<div *ngIf="context.canShow">Container contains the text: {{context.getContainerText()}}</div>
</ng-template>
child.component (Child)
import {
Component,
ElementRef,
Input,
TemplateRef,
ViewChild
} from "#angular/core";
#Component({
selector: "child",
templateUrl: "./child.component.html",
styleUrls: ["./child.component.css"]
})
export class ChildComponent {
#Input() public customTemplate!: TemplateRef<HTMLElement>;
#ViewChild("container", { static: false })
public readonly containerRef!: ElementRef;
templateContext = { context: this };
canShow: boolean = false;
toggleShow() {
this.canShow = !this.canShow;
}
getContainerText() {
return this.containerRef.nativeElement.textContent;
}
}
<ng-container *ngTemplateOutlet="customTemplate || defaultTemplate; context: templateContext">
</ng-container>
<ng-template #defaultTemplate>
<div #container>GOODBYE FROM CONTAINER</div>
<button (click)="toggleShow()">Toggle Display</button>
<div *ngIf="canShow">Container contains the text: {{getContainerText()}}</div>
</ng-template>
MY QUESTION
How do I use #ViewChild to access this div from an outside template that updates with any changes in the DOM? (Note: Removing the *ngIf is NOT an option for this project)
What's causing this? Are there any lifecycle methods that I can use to remedy this issue?
MY HUNCH
I'm guessing that ViewChild is being called BEFORE the DOM updates with its new template and I need to setup a listener for DOM changes. I tried this and failed so I'd really appreciate some wisdom on how best to proceed. Thanks in advance :)
EDIT:
This solution needs to properly display <div #container></div> regardless of whether you're passing in a custom template or using the default one.
ViewChild doesn't seem to pick up a rendered template - probably because it's not part of the components template initially. It's not a timing or lifecycle issue, it's just never available as a ViewChild
An approach that does work is to pass in the template as content to the child component, and access it using ContentChildren. You subscribe to the ContentChildren QueryList for changes, which will update when the DOM element becomes rendered
You can then access the nativeElement (the div). If you wanted you could add listeners here to the DOM element, and trigger cd.detectChanges afterwards, but that would be a bit unusual. It would probably be better to handle DOM changes in the parent element, and pass the required values down to the child using regular #Input on the child
#Component({
selector: "my-app",
template: `
<child>
<ng-template #parentTemplate let-context="context">
<div #container>Parent Template</div>
</ng-template>
</child>
`,
styleUrls: ["./app.component.css"]
})
export class AppComponent {}
#Component({
selector: "child",
template: `
<ng-container *ngTemplateOutlet="customTemplate"> </ng-container>
`,
styleUrls: ["./child.component.css"]
})
export class ChildComponent implements AfterContentInit {
#ContentChild("parentTemplate")
customTemplate: TemplateRef<any>;
#ContentChildren("container")
containerList: QueryList<HTMLElement>;
ngAfterContentInit() {
this.containerList.changes.subscribe(list => {
console.log(list.first.nativeElement.innerText);
// prints 'Parent Template'
});
}
}
In Angular I want to project (Transclude) a nativeElement to another component without using innerHTML.
Currently I'm trying to do this using ngComponentOutlet using a child component that has an ng-content.
I want to load the component and send html data to it all rendered so I dont need to use innerHTML to compile it to HTML.
Reason for this is, innerHTML looses the bindings.
This is my attempt:
My Parent Component:
import { Component, ContentChildren, QueryList } from '#angular/core';
import { TableRowComponent } from '../table-row/table-row.component';
import { TableCellComponent } from '../table-cell/table-cell.component';
#Component({
selector: 'ls-table-body',
template: `
<ng-container>
<tr ls-table-row *ngFor="let row of rows">
<ng-container *ngFor="let cell of cells">
<ng-container *ngComponentOutlet="tableCellComponent; content: [[cell.elem.nativeElement]]"></ng-container>
</ng-container>
</tr>
</ng-container>
`,
styleUrls: ['./table-body.component.scss']
})
export class TableBodyComponent {
#ContentChildren(TableRowComponent) rows: QueryList<TableRowComponent>;
#ContentChildren(TableCellComponent) cells: QueryList<TableCellComponent>;
public tableCellComponentComponent = TableCellComponent;
}
My Child Component called TableCellComponent:
import { Component, HostBinding, Input, ContentChildren, ViewEncapsulation, QueryList } from '#angular/core';
#Component({
selector: '[ls-table-cell]',
template: `
<ng-content></ng-content>
`,
styleUrls: ['./table-cell.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class TableCellComponent {
#HostBinding() #Input('class') classList = 'ls-table-cell';
#ContentChildren(TableCellComponent) cells: QueryList<TableCellComponent>;
#Input() span: number;
#Input() data: any;
}
Outcome I'm looking for:
I'm looking to compile the HTML - as I cannot use innerHTML due to the bindings getting removed - so I've optted for the ngComponentOutlet solution using ng-content - as I believe that compiles my HTML I pass through using cell.elem.nativeElement.
I'm also open to alternatives
I know there are a few questions similar to this one but they aren't quite the same. I'm building a nested list and I want to display a custom html content in each grandchild along side common html. When I add the to ListComponent outside of the loop works, but if I pass it inside the loop to the inner child, it doesn't work like the example bellow. The html I pass in the in the code bellow isn't shown. I'm probably trying to solve this the wrong way but I couldn't get it to work any way I tried. Any of you guys know how to make this work?
Thanks!
export class Model {
title: string;
children?: Model[] = [];
}
#Component({
selector: 'list',
template: `
<ul>
<li *ngFor="let item of items">
<list-item [item]="item">
<div main-content>
<ng-content select="[main-content]"></ng-content>
</div>
</list-item>
<list [items]="item.children"></list>
</li>
</ul>
`
})
export class List {
#Input() items: Model[];
}
#Component({
selector: 'list-item',
template: `
<h1>{{ item.title }}</h1>
<div class="content">
<ng-content select="[main-content]"></ng-content>
</div>
`
})
export class ListItem {
#Input() item: Model;
}
#Component({
selector: 'app-main',
template: `
<list [items]="items">
<div main-content>
<h1>Test</h1>
</div>
</list>
`
})
export class AppMainComponent {
}
After much testing and going further through the duplicate question that was mentioned and its inner-links, it doesn't quite solve my issue, because the template I'm not trying to duplicate the content as it is in plain. I'm trying to inject into another component with other common html inside it, so if I just iterate through I'll just replicate the the template that I'm passing but I'll lose all other html that is inside.
I know it's a little too late but I've recently encountered a similar problem and an answer here would have helped me.
From what I understood you wish to define the template for your list items in the component in which you are using the list.
To do that you need to use ng-template in both the child component(the list) and also in the grandchild(the item).
The grandchild should be something like this:
#Component({
selector: 'app-item',
template:`
<ng-container *ngTemplateOutlet="itemTemplate; context: {$implicit: data}">
</ng-container>
`
})
export class ItemComponent {
#Input() public data;
#ContentChild('itemTemplate') public itemTemplate;
}
And the child:
#Component({
selector: 'app-list',
template:`
<app-item [data]="dataItem" *ngFor="let dataItem of data">
<ng-template #itemTemplate let-item>
<ng-container *ngTemplateOutlet="itemTemplate1; context: {$implicit: item}">
</ng-container>
</ng-template>
</app-item>
`
})
export class ListComponent {
#Input() public data;
#ContentChild('itemTemplate') public itemTemplate1;
}
And the component that uses the list:
<app-list [data]="data">
<ng-template #itemTemplate let-item>
<h1>--{{ item?.value}}--</h1>
</ng-template>
</app-list>
In the item you use an ng-template received as content and pass it the input as context. Inside this template you can define how the item will look inside of your list and though the context you have access to the data of that item. Because you can't just skip a level, you must also expect an ng-template in the list. Inside the list you define the ng-template for the item and inside that ng-template you are just using the ng-template received from above, through an ng-container. And finally at the highest level you can simply define the template for the item.
How can I access the "content" of a component from within the component class itself?
I would like to do something like this:
<upper>my text to transform to upper case</upper>
How can I get the content or the upper tag within my component like I would use #Input for attributes?
#Component({
selector: 'upper',
template: `<ng-content></ng-content>`
})
export class UpperComponent {
#Input
content: String;
}
PS: I know I could use pipes for the upper case transformation, this is only an example, I don't want to create an upper component, just know how to access the component's content from with the component class.
If you want to get a reference to a component of the transcluded content, you can use:
#Component({
selector: 'upper',
template: `<ng-content></ng-content>`
})
export class UpperComponent {
#ContentChild(SomeComponent) content: SomeComponent;
}
If you wrap <ng-content> then you can access access to the transcluded content like
#Component({
selector: 'upper',
template: `
<div #contentWrapper>
<ng-content></ng-content>
</div>`
})
export class UpperComponent {
#ViewChild('contentWrapper') content: ElementRef;
ngAfterViewInit() {
console.debug(this.content.nativeElement);
}
}
You need to leverage the #ContentChild decorator for this.
#Component({
selector: 'upper',
template: `<ng-content></ng-content>`
})
export class UpperComponent {
#Input
content: String;
#ContentChild(...)
element: any;
}
Edit
I investigated a bit more your issue and it's not possible to use #ContentChild here since you don't have a root inner DOM element.
You need to leverage the DOM directly. Here is a working solution:
#Component({
selector: 'upper',
template: `<ng-content></ng-content>`
})
export class UpperComponent {
constructor(private elt:ElementRef, private renderer:Renderer) {
}
ngAfterViewInit() {
var textNode = this.elt.nativeElement.childNodes[0];
var textInput = textNode.nodeValue;
this.renderer.setText(textNode, textInput.toUpperCase());
}
}
See this plunkr for more details: https://plnkr.co/edit/KBxWOnyvLovboGWDGfat?p=preview
https://angular.io/api/core/ContentChildren
class SomeDir implements AfterContentInit {
#ContentChildren(ChildDirective) contentChildren : QueryList<ChildDirective>;
ngAfterContentInit() {
// contentChildren is set
}
}
Note that if you do console.log(contentChildren), it will only work on ngAfterContentInit or a later event.
Good morning,
I've been doing a little research on this topic because something similar has happened to me in my project. What I have discovered is that there are two decorators that can help you for this solution: ViewChildren and ContentChildren. The difference mainly according to what I have found in the network is that ViewChildren accesses the interior of the component and ContentChildren accesses the DOM or the component itself.
To access the upper element from the ng-content you must change the upper element leaving it like this:
<upper #upper>my text to transform to upper case</upper>
And then simply to access the interior (In the component that has the ng-content):
#ViewChildren('upper') upper: QueryList<ElementRef>
And to access the component in general (In the component that has the ng-content):
#ContentChildren('upper') upper: QueryList<ElementRef>
Where did you get the information from: https://netbasal.com/understanding-viewchildren-contentchildren-and-querylist-in-angular-896b0c689f6e
You can also use TemplateRef, which was what worked me in the end because I was using a service to initialize my component.
In your-component.component.html
<!-- Modal Component & Content -->
<ng-template #modal>
... modal content here ...
</ng-template>
In your-component.component.ts
#ViewChild('modal') modalContentRef!: TemplateRef<any>;
... pass your modalContentRef into your child ...
... For me here, the service was the middleman - [Not shown] ...
In modal.component.html
<div class="modal-container">
<!-- Content will be rendered here in the ng-container -->
<ng-container *ngTemplateOutlet="modalContentRef">
</ng-container>
</div>
In modal.component.ts
#Input() modalContentRef: TemplateRef<any> | null = null;
This how I ended up solving the problem with a TemplateRef, since ng-content could not be used since a service was creating the modal