Our application layout features a toolbar and the actual contents area. Since the toolbar has a somewhat complex DOM structure we have decided to create a component out of it.
We could reuse this component on each page, but the toolbar position is actually not next to the contents (some other components are in between). So we would rather place the toolbar once in the parent page component and just declare the toolbar contents on each page as necessary.
<app>
<toolbar>
<ng-container #toolbar></ng-container>
</toolbar>
<some unrelated component>...</some unrelated component>
<content>
<ng-template #tbcontent>
<button>Click me!</button>
</ng-template>
<p>Actual page content!</p>
</content>
</app>
Thus in the example above we would take the contents in #tbcontent and place them within #toolbar, effectively rendering something like this:
<app>
<toolbar>
<button>Click me!</button>
</toolbar>
<some unrelated component>...</some unrelated component>
<content>
<p>Actual page content!</p>
</content>
</app>
We have tried querying both the #toolbar and the #tbcontent nodes from the root AppComponent using #ViewChild (code has been simplified for the sake of clarity):
#Component({
/* ... */
})
export class AppComponent implements OnInit, AfterViewInit {
#ViewChild("tbcontent") tbcontent: TemplateRef<any>;
#ViewChild("toolbar", {read: ViewContainerRef}) toolbar: ViewContainerRef;
/* ... */
ngAfterViewInit() {
console.log('ngAfterViewInit');
console.log(this.tbcontent);
console.log(this.toolbar);
this.toolbar.createEmbeddedView(this.tbcontent);
}
}
But both references are undefined. Is there a way to properly access the elements? Is this actually a bad practice, and maybe there is a better way to achieve the same result?
Update: #martin-nuc 's kindly suggested using *ngTemplateOutlet, which seems to do the trick but only when the node that is copied lives within the same component. Here's a JSFiddle showing this:
https://jsfiddle.net/carlosafonso/k3oq56oq/3/
Thank you.
When you define template it's not inserted into DOM. You need to use some directive to insert it to DOM.
Also ng-container is basically just a placeholder for some content but it's not rendered into DOM.
I think what you are trying to do is
<toolbar>
<ng-container *ngTemplateOutlet="tbcontent"></ng-container>
</toolbar>
EDIT: The questions was how to display template from child component in a parent component. I basically passed template as output from the child component. Here is how to achieve it.
Parent component:
#Component({
selector: 'app',
template: `
<div class="container">
<toolbar>
<ng-container *ngTemplateOutlet="toolbarContent"></ng-container>
</toolbar>
<hr/>
<content (toolbarLoaded)="displayToolbar($event)"></content>
</div>
`,
})
class AppComponent {
constructor() {
}
displayToolbar($event) {
this.toolbarContent = $event;
}
}
Child component with toolbar template:
#Component({
selector: 'content',
template: `
<ng-template #tb>
<button type="button" class="btn btn-sm btn-primary">Content-specific action</button>
</ng-template>
<p>These are the page contents.</p>
`
})
class ContentComponent {
#Output() toolbarLoaded = new EventEmitter<any>();
#ViewChild('tb') tb;
ngAfterViewInit() {
// arrow function dont work in jsfiddle somehow :(
let that = this;
// we need timeout to avoid Expression had changed after it was checked (https://github.com/angular/angular/issues/6005)
setTimeout(() => {
that.toolbarLoaded.emit(that.tb);
}, 0);
}
}
Jsfiddle: https://jsfiddle.net/fdv1tesr/3/
Related
I am making a modal for our library, and I am not sure how to do this, but I would like to use viewRef.createComponent to create a component (A), then I would like to create another component (B) inside of component A and have component A use <ng-content> to get the the items from component B.
Basically Component A is a template for the modal and Component B is what to fill the modal with.
So, first I have sections modal-header and modal-content. These are the two directives I would like to grab from component B.
Note: The last code block in this post is where the issue is located (see the comment).
Stackblitz
#Directive({selector: 'modal-header'})
export class ModalHeader {}
#Directive({selector: 'modal-content'})
export class ModalContent {}
Component A's template looks like this:
<div>
<h4 *ngIf="modalHeader" mat-dialog-title>
<ng-content select="modal-header, [modal-header]"></ng-content>
</h4>
<mat-dialog-content *ngIf="modalContent">
<ng-content select="modal-content, [modal-content]"></ng-content>
</mat-dialog-content>
</div>
Component B's template looks like this:
<modal-header>Hello World</modal-header>
<modal-content>This is some cool stuff</modal-content>
Then in a service I am using MatDialog to open the modal with the above html which works:
#Injectable({ providedIn: 'root' })
export class ModalService {
constructor(private readonly dialog: MatDialog) {}
create<T>(component: Type<T>) {
const dialogRef = this.dialog.open(ModalPopupComponent);
dialogRef.componentInstance.componentType = component;
}
}
That service then opens this component which will then create component A which works, then I want to inject component B into const main.
#Component({ template: `<div #outlet></div>` })
export class ModalPopupComponent {
#ViewChild('outlet', { static: true, read: ViewContainerRef }) outlet!: ViewContainerRef;
componentType!: Type<any>;
ngAfterContentInit() {
const main = this.outlet.createComponent<ModalComponent>(ModalComponent);
// How do I inject `this.componentType` into `main`?
}
}
In createComponent, there is a projectableNodes option, can I pass the component through that option?
I tried doing this as well, but this just appends component B to the end of component A.
const main = this.outlet.createComponent<ModalComponent>(ModalComponent);
const ref = main.injector.get(ViewContainerRef);
ref.createComponent(this.componentType);
I was able to get this to work by using projectableNodes in the create component and passing an array of html elements.
Create the component that we want to get the nodes from.
Get the elements from the newly created component.
Pass the elements to the template element which will replace instances of <ng-content>; where the first <ng-content> matches the first item in the array, the second <ng-content> matches the second item, and so on.
The class would then look something like this:
#Component({ template: `<div #outlet></div>` })
export class ModalPopupComponent {
#ViewChild('outlet', { static: true, read: ViewContainerRef }) outlet!: ViewContainerRef;
componentType!: Type<any>;
ngAfterContentInit() {
const user = this.outlet.createComponent(this.componentType);
const el = user.main.location.nativeElement;
const header = el.querySelectorAll('modal-header');
const content = el.querySelectorAll('modal-content');
this.outlet.createComponent<ModalComponent>(ModalComponent, {
projectableNodes: [header, content]
});
}
}
The projectable nodes would then replace the <ng-content> in this template:
Note: This doesn't seem to work if the content is wrapped within an *ngIf.
Note: Placing the <ng-content> in an <ng-template> might work and then use ngTemplateOutlet to show it (untested).
<div>
<h4 mat-dialog-title>
<!-- The first item in the array -->
<ng-content select="modal-header, [modal-header]"></ng-content>
</h4>
<mat-dialog-content>
<!-- The second item in the array -->
<ng-content select="modal-content, [modal-content]"></ng-content>
</mat-dialog-content>
</div>
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'
});
}
}
Hope someone can enlighten me.
Problem
I need to get a reference to a directive placed inside an inner component.
I'm using #ViewChild targeting Directive class, with {static:true}since it doesnt have to wait for state changes and use it later on lifecicle when the user clicks a button.
#ViewChild(DirectiveClass, {static:true}) childRef : DirectiveClass;
Expected
To have directive reference in childRef instance variable when the event happens.
Actual
childRef is undefined
I did research for similar problems and all seemed to be because ref was inside a *ngIf and should be {static: false}; or because the ref was intended to be used before it was fetched(before ngAfterViewInit hook). This is not the case, this case is fair simplier and yet cant get whats wrong! :/
Reproduction
Situation
so, I got two components and a directive. Lets call them ParentComponent and SonComponent. Directive is applied in son component, so lets call it SonDirective.
Basically this is the structure.
<app-parent>
<app-son> </app-son> //<- Directive inside
</app-parent>
Code
//parent.component.ts
#Component({
selector: 'app-parent',
template: `
<div>
<h2> parent </h2>
<app-son></app-son>
</div>
`,
})
export class AppParentComponent implements AfterViewInit{
#ViewChild(AppSonDirective,{static: true}) childRef : AppSonDirective;
constructor() {}
ngAfterViewInit() {
console.log("childRef:",this.childRef)
}
}
// son.component.ts
#Component({
selector: 'app-son',
template: `
<div appSonDirective>
<p> son <p>
</div>
`,
})
export class AppSonComponent {
constructor() {}
}
//son.directive.ts
#Directive({
selector: '[appSonDirective]'
})
export class AppSonDirective {
constructor(){}
}
Note: if i move the view child ref to the son component it can be accessed. The issue seems to be at parent component (?)
Anyway... here is the reproducion code(it has some logs to know whats going on)
Any thoughts will be helpfull.
Thanks in advance! :)
Problem is that #ViewChild only works around the component DOM but don't have access to the DOM of its children components, that would break encapsulation between components. In this case appSonDirective is declared in AppSonComponent DOM but because you're trying to access it from AppParentComponent it returns undefined because AppParentComponent can't access AppSonComponent DOM. It could work if you had something like this:
<app-parent>
<app-son appSonDirective></app-son>
</app-parent>
One solution is to expose the directive as a property of you child component. Something like:
#Component({
selector: 'app-son',
template: `
<div appSonDirective>
<p> son <p>
</div>
`,
})
export class AppSonComponent {
#ViewChild(AppSonDirective,{static: true}) childRef : AppSonDirective;
constructor() {}
}
and then in AppParentComponent
#Component({
selector: 'app-parent',
template: `
<div>
<h2> parent </h2>
<app-son></app-son>
</div>
`,
})
export class AppParentComponent implements AfterViewInit{
#ViewChild(AppSonComponent,{static: true}) son : AppSonComponent;
constructor() {}
ngAfterViewInit() {
console.log("childRef:",this.son.childRef)
}
}
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