This question already has answers here:
#ViewChild in *ngIf
(19 answers)
Closed 3 years ago.
I would like to make a loading When requesting data from ngrx+ api http request
I have a state that contain loading boolean, and another with raw data.
I set loading to false when data arrive.
the problem viewChild don't find reference because data arrive before loading set to false.
I get error
ERROR TypeError: Cannot read property 'nativeElement' of undefined
Here's the template
<div *ngIf="isLoading; else kpi"><mat-spinner></mat-spinner></div>
<ng-template #kpi>
<div class="chartWrapper">
<div class="scrollArea">
<div class="chartAreaWrapper">
<canvas #lineChart id="lineChart" width="1200" height="400"></canvas>
</div>
</div>
<canvas #stickyAxis id="chartAxis" height="400" width="0"></canvas>
</div>
</ng-template>
in component
export class BranchKpisComponent implements OnChanges, OnInit, OnDestroy {
#ViewChild('lineChart', { static: true }) private chartRef;
I'm using ngrx store
I have the selectors
selectLoadings$ = this.store.pipe(select(selectLoadings));
selectedDataByBranch$ = this.store.pipe(
select(selectBranchDirections, {
branchId: this.branchId,
location: 'directionsByBranch',
dir: 0
})
inside ngOnchange() I subscribe to loading and data observable ( separately )
this.selectLoadings$.subscribe(
loading => (this.isLoading = loading.directionsByBranch[this.branchId])
);
this.selectedDataByBranch$
.pipe(filter(data => Object.keys(data).length > 0))
.subscribe(selectedDataByBranch => {
this.trainsDatasets = this.getDatasets(selectedDataByBranch);
this.context = this.chartRef.nativeElement; ### Error undefined chartRef
Inside reducer when I get data I set loading to false
case ESchedulesActions.GetAllSchedulesByDirectionSuccess: {
return {
...state,
directionsByBranch: {
...state.directionsByBranch,
[action.payload[1]]: action.payload[0]
},
loadings: {
...state.loadings,
directionsByBranch: {
...state.loadings.directionsByBranch,
[action.payload[1]]: false
}
}
};
}
You could use [hidden] instead of *ngIf. It will still have the element you want to view in the DOM. Would need another one instead of the else which is the opposite.
<div [hidden]="!isLoading"><mat-spinner></mat-spinner></div>
<ng-template [hidden]="isLoading">
<div class="chartWrapper">
<div class="scrollArea">
<div class="chartAreaWrapper">
<canvas #lineChart id="lineChart" width="1200" height="400"></canvas>
</div>
</div>
<canvas #stickyAxis id="chartAxis" height="400" width="0"></canvas>
</div>
</ng-template>
In terms of performance there won't be much difference if at all due to the size of the code.
viewChild should be implemented in this way. From the above, I think you implemented viewChild in wrong way.
import { Component,
ViewChild,
AfterViewInit,
ElementRef } from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit {
#ViewChild('someInput') someInput: ElementRef;
ngAfterViewInit() {
this.someInput.nativeElement.value = "Anchovies! 🍕🍕";
}
}
so your updated code block will be
export class BranchKpisComponent implements OnChanges, OnInit, OnDestroy {
#ViewChild('lineChart', { static: true }) chartRef:ElementRef;
Dont forget to import ElementRef
Related
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)
I have a web page which has some charts, right now I'm trying to create a loader which is very simple:
<div *ngIf="loading === true; else elseBlock" class="container">
<div class="grid-pulse la-3x">
</div>
</div>
<ng-template #elseBlock>
<ng-content></ng-content>
</ng-template>
import { Component, Input, ChangeDetectionStrategy } from '#angular/core';
#Component({
selector: 'app-loader',
templateUrl: './loader.component.html',
styleUrls: ['./loader.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class LoaderComponent {
#Input() loading = true;
}
which only swap between a component and a loader while the flag is true and the component when it is false.
now I implemented some of the charts like this:
<app-loader [loading]="loading">
<canvas
baseChart
#canvas
[labels]="chartLabels"
[datasets]="chartData"
[options]="chartOptions"
[chartType]="chartType">
</canvas>
</app-loader>
import { Component, Input, ChangeDetectionStrategy } from '#angular/core';
#Component({
selector: 'my-component',
// ... the other stuff, scss, html
})
export class MyComponent implements OnInit {
loading = true;
// more
// data
// ....
ngOnInit(){
this.httpService.get('/something').subscribe(data => {
//do something with the data
this.loading = false; //set loader to false.
});
}
}
which work perfectly, my loading variable depends on a http request that I do, and then I change the variable to false and therefore my chart appears.
the problem is that when I change my variable to false, it starts to mount the component, but I already stopped showing the loader.... therefore I see a blank space while the chart is appearing.
so the question is:
how can I check if the chart was already mounted and created? because I don't want to show that white space (I'll show a fake loader on top while the component starts rendering, any Ideas on how to accomplish?)
I read that someway to accomplish this is with AfterViewInit but can I access that information with a component that is not created by me (in this case )
The only logical reason i can think of - You are changing the loading variable to true, before the data is actually processed and passed into the array, if you are using a forEach loop consider using await or a simple for loop.
I am working on an generate dynamic template using angular 6. I have an API that return strings like below:
<button type="button" (click)="openAlert()">click me</button>
and html
<div [innerHtml]="myTemplate | safeHtml">
</div>
function is bellow:
openAlert() {
alert('hello');
}
You cannot bind angular events directly to innerHTML.
Still if you need to attach the event listeners you need to to do it after the html content is loaded.
Once the content is set to the variable, ngAfterViewInit Angular life cycle event will be triggered. Here you need to attach the required event listeners.
Checkout the working example below.
component.html
<button (click)="addTemplate()">Get Template</button>
<div [innerHTML]="myTemplate | safeHtml"></div>
component.ts
import { Component, ElementRef } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
name = 'Angular';
myTemplate = '';
constructor(private elementRef:ElementRef){
}
openAlert() {
alert('hello');
}
addTemplate(){
this.myTemplate = '<button type="button" id="my-button" (click)="openAlert()">click mee</buttn>';
}
ngAfterViewChecked (){
if(this.elementRef.nativeElement.querySelector('#my-button')){
this.elementRef.nativeElement.querySelector('#my-button').addEventListener('click', this.openAlert.bind(this));
}
}
}
safe-html.pipe.ts
import { Pipe, PipeTransform } from '#angular/core';
import { DomSanitizer } from '#angular/platform-browser';
#Pipe({
name: 'safeHtml'
})
export class SafeHtmlPipe implements PipeTransform {
constructor(private sanitized: DomSanitizer) {}
transform(value) {
return this.sanitized.bypassSecurityTrustHtml(value);
}
}
this should work too:
component.html
<div #template></div>
component.ts
#ViewChild('template') myTemplate: ElementRef;
addTemplate(){
this.myTemplate.nativeElement.innerHTML = '<button type="button">click me</button>';
this.myTemplate.nativeElement.addEventListener('click', this.openAlert);
}
Basically this will not work. When you write code in Angular, it is transpiled by webpack and converted to javascript that is executed in the browser.
However, now you are injecting Angular code dynamically and not building it. The event detection (click) would not work natively and the function openAlert is also not in the global scope where it is injected. You will have to consider a different approach and generate content using <ng-template> based on response from the API.
I'm trying to dynamically add a URL into an iframe src on a click event but I'm getting this error
Error: Uncaught (in promise): Error: Cannot match any routes. URL Segment: 'SafeValue%20must%20use%20%5Bproperty%5D'
Ive used domSanitizer to make the URL safe to insert it in the iframe
HTML
<div class="cards-wrapper">
<div class="prepackaged__card" *ngFor="let video of videos">
<img class="prepackaged__card-header" src="{{video.thumbnail}}">
<div class="prepackaged__card-body">
<div class="category">{{video.subname}}</div>
<h2 class="title">{{video.name}}
</h2>
<button (click)="sendUrl(video.videoData)">PLAY VIDEO</button>
</div>
</div>
</div>
<div class="video-player-modal">
<div class="video-player-modal_header">
</div>
<div class="video-player-modal_video">
<iframe class="video-player-modal_video_player" src="" frameborder="0" allowfullscreen=""></iframe>
</div>
</div>
app.component.ts
import { Component, OnInit } from '#angular/core';
import { DashboardServiceProxy, UserVideoDto } from '#shared/service-proxies/service-proxies';
import { DomSanitizer, SafeResourceUrl, SafeUrl } from '#angular/platform-browser';
declare var jQuery: any;
const $ = jQuery;
#Component({
selector: 'app-video',
templateUrl: './video.component.html',
styleUrls: ['./video.component.scss'],
providers: [
DashboardServiceProxy
]
})
export class VideoComponent implements OnInit {
videos: UserVideoDto[] = [];
trustedDashboardUrl: SafeUrl;
constructor(
private _dashboardService: DashboardServiceProxy,
private sanitizer: DomSanitizer
) {
}
ngOnInit() {
this.getVideos();
}
getVideos() {
this._dashboardService.getAllUserVideos().subscribe((result) => {
this.videos = result;
console.log(this.videos);
});
}
sendUrl(playerUrl) {
this.trustedDashboardUrl = this.sanitizer.bypassSecurityTrustResourceUrl(playerUrl);
$('.video-player-modal_video_player').attr('src', this.trustedDashboardUrl);
}
}
any ideas on what is happening?
What I would recommend is using property binding instead of using jQuery for dynamically populating the attributes. It goes as follows:
Set the src attribute to a component member variable which is initialised to empty string:
[src]="iframeURL"
In your component file set iframeURL:
iframeURL = '';
Modify your click event handler as follows:
sendUrl(playerUrl) {
// this.iframeURL = playerUrl // try it first, if it doesn't work use the sanitized URL
this.trustedDashboardUrl = this.sanitizer.bypassSecurityTrustResourceUrl(playerUrl);
this.iframeURL = this.trustedDashboardUrl;
}
Hope it works! Kindly share in case it doesn't.
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