I am trying to display popover inside container but it does not work,
if I remove the *ngIf it works
where ever there if *ngIf it doesn't render
<div class="container" *ngIf="data" >
<button
class="popover"
data-trigger="hover"
data-toggle="tooltip"
data-content="hello"
data-container="body">
<mat-icon>
info
</mat-icon>
</button>
</div>
//ts file
export class SomeComponent implements OnInit {
//... variables
//... constructor
ngOnInit() {
$('.popover').popover({
boundary: 'viewport',
placement: 'top',
container:'body',
sanitize: true,
appendToBody: true
})
}
}```
This does not work because the div.container is not rendered by angular by the time when ngOnInit() is called by angular. Instead you can use the AfterViewInit Lifecycle hook as shown below.
Pay attention that the paragraph with the ngIf can't be loaded in the ngOnInit but it can be loaded in the ngAfterViewInit.
Stackblitz code: https://stackblitz.com/edit/angular-ngif-lifecycle-hook
component.html
<p id="p1">
Without ngIf
</p>
<p id="p2" *ngIf="data">
With ngIf
</p>
component.ts
import { Component, VERSION, OnInit, AfterViewInit } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit, AfterViewInit{
data = {random: 'text'};
ngOnInit() {
const withoutNgIf = document.getElementById('p1');
const withNgIf = document.getElementById('p2');
console.log('OnInit without ngIf: ', withoutNgIf);
# Output: HTMLParagraphElement
console.log('OnInit with ngIf: ', withNgIf);
# Output: null
}
ngAfterViewInit() {
const withNgIf = document.getElementById('p2');
console.log('AfterViewInit with ngIf: ', withNgIf);
# Output: HTMLParagraphElement
}
}
I hope that helps you to understand the problem.
Tip: I would suggest to use the ViewChild decorator to access the DOM instead of jquery if you are using angular. (Example: https://dev.to/danielpdev/how-to-use-viewchild-decorator-in-angular-9-i0)
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;
}
I am trying to make a popover with angular. Which currently has two components and one directive.
When I click on my button, I get an error saying
ERROR TypeError: Cannot read property 'openPopover' of undefined
How can I get a reference to the parent from the directive and then get the child from that parent?
So, the click path would look like so: [open-popover] / <map-popover> / <map-popover-content>
app.component.html
<mat-popover>
<mat-popover-content>
<p>Hello World</p>
</mat-popover-content>
<button mat-flat-button open-popover color="primary">Add Debt</button>
</mat-popover>
popover.component.ts
#Component({
selector: 'mat-popover',
template: '<ng-content></ng-content>',
styleUrls: ['./popover.component.scss']
})
export class MatPopoverComponent {
#ViewChild(MatPopoverContentComponent)
public content: MatPopoverContentComponent
public open() {
this.content.openPopover()
}
}
content.component.ts
#Component({
selector: 'mat-popover-content',
template: `<ng-template #popoverContent>
<ng-content></ng-content>
</ng-template>`,
styleUrls: ['./content.component.scss']
})
export class MatPopoverContentComponent {
#ViewChild('popoverContent')
public template: TemplateRef<any>
public constructor(public dialog: MatDialog) { }
openPopover(): void {
this.dialog.open(this.template, {
hasBackdrop: false
})
}
}
open.directive.ts
#Directive({
selector: '[open-popover]'
})
export class OpenPopoverDirective {
public constructor(#Host() private popover: MatPopoverComponent) { }
#HostListener('click')
public onClick() {
this.popover.open()
}
}
Replace ContentChild decorator with ViewChild here #ViewChild(MatPopoverContentComponent) and it should work.
Explanation
You should distinguish Light DOM and Shadow DOM:
component
#Component({
selector: 'mat-popover',
template: `<ng-content></ng-content>`, <--- Shadow DOM
styleUrls: ['./popover.component.scss']
})
export class MatPopoverComponent {}
consumer
<mat-popover>
<!-- Light DOM starts -->
<mat-popover-content>
<p>Hello World</p>
</mat-popover-content>
<button mat-flat-button open-popover color="primary">Add Debt</button>
<!-- Light DOM ends-->
</mat-popover>
So in Angular we query elements in Shadow DOM by using ViewChild/ren and elements in Light DOM by using ContentChild/ren
I would like to set the body of <ng-content> while instantiating a component dynamically using ComponentFactoryResolver.
I see that I can get access to input & output using ComponentRef, but not a way to set <ng-content>.
Please note <ng-content> I'm planning on setting can contain simple text/can span dynamically created components
#Component({
selector: 'app-component-to-project',
template: `<ng-content></ng-content>`
})
export class ComponentToProject implements AfterContentInit {
ngAfterContentInit() {
// We will do something important with content here
}
}
#Directive({
selector: 'appProjectionMarker'
})
export class ProjectionMarkerDirective implements OnInit {
constructor(private viewContainerRef: ViewContainerRef, private componentFactoryResolver: ComponentFactoryResolver) {
}
ngOnInit() {
const componentFactory: ComponentFactory<ComponentToProject> = this.componentFactoryResolver.resolveComponentFactory(ComponentToProject);
const componentRef: ComponentRef<ComponentToProject> = this.viewContainerRef.createComponent(componentFactory);
// Question: How to set content before the child's afterContentInit is invoked
}
}
#Component({
selector: 'appTestComponent',
template: `<div appProjectionMarker></div>`
})
export class TestComponent {}
There is the projectableNodes parameter for the vcRef.createComponent method
createComponent<C>(componentFactory: ComponentFactory<C>, index?: number, injector?: Injector, projectableNodes?: any[][], ngModule?: NgModuleRef<any>): ComponentRef<C>;
You can use it to dynamically inject one component into another.
Let's say we have the following component
#Component({
selector: 'card',
template: `
<div class="card__top">
<h2>Creating a angular2 component with ng-content dynamically</h2>
</div>
<div class="card__body">
<ng-content></ng-content>
</div>
<div class="card__bottom">
<ng-content></ng-content>
</div>
`
})
export class CardComponent {}
We want to create it dynamically and insert some controls to its ng-content locations. It could be done like follows:
const bodyFactory = this.cfr.resolveComponentFactory(CardBodyComponent);
const footerFactory = this.cfr.resolveComponentFactory(CardFooterComponent);
let bodyRef = this.vcRef.createComponent(bodyFactory);
let footerRef = this.vcRef.createComponent(footerFactory);
const cardFactory = this.cfr.resolveComponentFactory(CardComponent);
const cardRef = this.vcRef.createComponent(
cardFactory,
0,
undefined,
[
[bodyRef.location.nativeElement],
[footerRef.location.nativeElement]
]
);
Plunker Example
See also
Why is projectableNodes an any[][]?
Pawel Kozlowski - Reactive parenting with Angular 2 - NG-BE 2016
Trying to do child to parent communication with #Output event emitter but is no working
here is the child component
import { Component, OnInit, Output, Input, EventEmitter } from '#angular/core';
#Component({
selector: 'app-emiter',
templateUrl: './emiter.component.html',
styleUrls: ['./emiter.component.css']
})
export class EmiterComponent implements OnInit {
#Output() emitor: EventEmitter<any>
constructor() { this.emitor = new EventEmitter()}
touchHere(){this.emitor.emit('Should Work');
console.log('<><><><>',this.emitor) // this comes empty
}
ngOnInit() {
}
}
this is the html template
<p>
<button (click)=" touchHere()" class="btn btn-success btn-block">touch</button>
</p>
The console.log inside the touchHere it shows nothing
even if I put this inside the parent component it show nothing as well
parent component
import { Component , OnInit} from '#angular/core';
// service I use for other stuff//
import { SenderService } from './sender.service';
// I dont know if I have to import this but did it just in case
import { EmiterComponent } from './emiter/emiter.component'
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'app';
user: any;
touchThis(message: string) {
console.log('Not working: ${message}');
}
constructor(private mySessionService: SenderService) { }
}
and here is the html template
<div>
<app-emiter>(touchHere)='touchThis($event)'</app-emiter>
</div>
Parent component template:
<app-emitor (emitor)='touchThis($event)'></app-emiter>
In parent template #Output should be 'called', not the child method.
Also, see: https://angular.io/guide/component-interaction#parent-listens-for-child-event
Here’s an example of how we write a component that has outputs:
#Component({
selector: 'single-component',
template: `<button (click)="liked()">Like it?</button>`
})
class SingleComponent {
#Output() putRingOnIt: EventEmitter<string>;
constructor() {
this.putRingOnIt = new EventEmitter();
}
liked(): void {
this.putRingOnIt.emit("oh oh oh");
}
}
Notice that we did all three steps: 1. specified outputs, 2. created an EventEmitter that we attached
to the output property putRingOnIt and 3. Emitted an event when liked is called.
If we wanted to use this output in a parent component we could do something like this:
#Component({
selector: 'club',
template: `
<div>
<single-component
(putRingOnIt)="ringWasPlaced($event)"
></single-component>
</div>`
})
class ClubComponent {
ringWasPlaced(message: string) { console.log(`Put your hands up: ${message}`);
} }
// logged -> "Put your hands up: oh oh oh"
Again, notice that:
putRingOnIt comes from the outputs of SingleComponent
ringWasPlaced is a function on the ClubComponent
$event contains the thing that wasemitted, in this case a string
<app-emiter (emitor)="touchThis($event)" ></app-emiter>
By using #Output() you should apply the event you need to emit in the directive of the emitter component.Adding the name of the variable to the the directive and but the emitted over function inside the quotation passing the $event.
touchHere() is the method from which you are binding some value to emit with your EventEmitter. And your EventEmitter is 'emitor'.
So your code will work if you simply do the below:
<app-emiter (emitor)='touchThis($event)'></app-emiter>
I have created dynamic component instances by selecting pre-existing components. For example,
#Component({
selector: 'dynamic-component',
template: `<div #container><ng-content></ng-content></div>`
})
export class DynamicComponent {
#ViewChild('container', {read: ViewContainerRef}) container: ViewContainerRef;
public addComponent(ngItem: Type<WidgetComponent>,selectedPlugin:Plugin): WidgetComponent {
let factory = this.compFactoryResolver.resolveComponentFactory(ngItem);
const ref = this.container.createComponent(factory);
const newItem: WidgetComponent = ref.instance;
newItem.pluginId = Math.random() + '';
newItem.plugin = selectedPlugin;
this._elements.push(newItem);
return newItem;
}
}
My pre-existed components are ChartWidget and PatientWidget which extended the class WidgetComponent that I wanted to add in the container. For example,
#Component({
selector: 'chart-widget',
templateUrl: 'chart-widget.component.html',
providers: [{provide: WidgetComponent, useExisting: forwardRef(() => ChartWidget) }]
})
export class ChartWidget extends WidgetComponent implements OnInit {
constructor(ngEl: ElementRef, renderer: Renderer) {
super(ngEl, renderer);
}
ngOnInit() {}
close(){
console.log('close');
}
refresh(){
console.log('refresh');
}
...
}
chart-widget.compoment.html (using primeng Panel)
<p-panel [style]="{'margin-bottom':'20px'}">
<p-header>
<div class="ui-helper-clearfix">
<span class="ui-panel-title" style="font-size:14px;display:inline-block;margin-top:2px">Chart Widget</span>
<div class="ui-toolbar-group-right">
<button pButton type="button" icon="fa-window-minimize" (click)="minimize()"</button>
<button pButton type="button" icon="fa-refresh" (click)="refresh()"></button>
<button pButton type="button" icon="fa-expand" (click)="expand()" ></button>
<button pButton type="button" (click)="close()" icon="fa-window-close"></button>
</div>
</div>
</p-header>
some data
</p-panel>
data-widget.compoment.html (same as chart-widget using primeng Panel)
#Component({
selector: 'data-widget',
templateUrl: 'data-widget.component.html',
providers: [{provide: WidgetComponent, useExisting: forwardRef(() =>DataWidget) }]
})
export class DataWidget extends WidgetComponent implements OnInit {
constructor(ngEl: ElementRef, renderer: Renderer) {
super(ngEl, renderer);
}
ngOnInit() {}
close(){
console.log('close');
}
refresh(){
console.log('refresh');
}
...
}
WidgetComponent.ts
#Component({
selector: 'widget',
template: '<ng-content></ng-content>'
})
export class WidgetComponent{
}
Now I added the components by selecting a component from the existed components (e.g. chart-widget and data-widget) in the following way and stored the instances into an array.
#Component({
templateUrl: 'main.component.html',
entryComponents: [ChartWidget, DataWidget],
})
export class MainComponent implements OnInit {
private elements: Array<WidgetComponent>=[];
private WidgetClasses = {
'ChartWidget': ChartWidget,
'DataWidget': DataWidget
}
#ViewChild(DynamicComponent) dynamicComponent: DynamicComponent;
addComponent(): void{
let ref= this.dynamicComponent.addComponent(this.WidgetClasses[this.selectedComponent], this.selectedComponent);
this.elements.push(ref);
this.dynamicComponent.resetContainer();
}
}
Now, I am facing problem to render the components using innerHtml in main.component.html. It render the html but I am not able to use button click event or other event on it. I have also tried to render chart using primeng but its also not working.
main.component.html
<dynamic-component [hidden]="true" ></dynamic-component>
<widget *ngFor="let item of elements">
<div [innerHTML]="item._ngEl.nativeElement.innerHTML | sanitizeHtml">
</div>
</widget>
I have also implemented a sanitizeHtml Pipe but its giving still same result. So, as I understand innerHTML is only showing the html data but I can't use any button event as well as the js chart. I have also tried to show the items like this {{item}} under tag. But it display like a text [object object]. So, could anyone give a solution for it? How can I render the components allowing the button events and js chart? Thanks.
EDIT: See my Plunker here https://plnkr.co/edit/lugU2pPsSBd3XhPHiUP1?p=preview
You can see here, it is possible to add chart or data widget dynamically and I am showing it using innerHTML. So, the button events are not working here. If I coding like {{item}} then it shows [object object] text. You can also see in console the component array data. The main Question is, How can I active the button events on it (e.g. if i click close or refresh button then it will call the related functions)?
I would create structural directive like:
view.directive.ts
import { ViewRef, Directive, Input, ViewContainerRef } from '#angular/core';
#Directive({
selector: '[view]'
})
export class ViewDirective {
constructor(private vcRef: ViewContainerRef) {}
#Input()
set view(view: ViewRef) {
this.vcRef.clear();
this.vcRef.insert(view);
}
ngOnDestroy() {
this.vcRef.clear()
}
}
then
app.component.ts
private elements: Array<{ view: ViewRef, component: WidgetComponent}> = [];
...
addComponent(widget: string ): void{
let component = this.dynamicComponent.addComponent(this.WidgetClasses[widget]);
let view: ViewRef = this.dynamicComponent.container.detach(0);
this.elements.push({view,component});
this.dynamicComponent.resetContainer();
}
and
app.component.html
<widget *ngFor="let item of elements">
<ng-container *view="item.view"></ng-container>
</widget>
So i have just moved view from dynamic component container to desired place.
Plunker Example