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.
Related
I created a breadcrumb component, I have a service BreadcrumbService that has a function that reads the url path and converts them to an array of segments (this.breadService.getUrlPathSegments()). When breadcrumbs is loaded or updated, I get the following error:
ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value for 'click-enabled': 'true'. Current value: 'false'.
What is the correct way to handle this? The code works the way I intended, but I need to handle the error message.
What I am trying to do is disable click events on the last item in the breadcrumb list, so when you click on it, none of the events fire. This all works even though I receive the error message.
What I am doing is when the view is checked, update the value of each breadcurmb's clickable state. This is done just like this:
#Component({
selector: 'breadcrumbs',
styleUrls: ['./breadcrumbs.component.scss'],
template: `
<ng-content select="breadcrumb"></ng-content>
`,
encapsulation: ViewEncapsulation.None
})
export class Breadcrumbs implements AfterViewChecked {
#Input() disableLast = true;
#ContentChildren(Breadcrumb, { descendants: false })
breadcrumbs!: QueryList<Breadcrumb>;
ngAfterViewChecked() {
this.enableDisableLast();
}
enableDisableLast() {
if (this.breadcrumbs && this.breadcrumbs.length > 0) {
this.breadcrumbs.forEach(item => { item.clickable = true; });
this.breadcrumbs.last.clickable = !this.disableLast;
}
}
}
Next in the breadcrumb I have a #HostBinding(), that updates the class of the element. Which is done like this:
#Component({
selector: 'breadcrumb',
styleUrls: ['./breadcrumb.component.scss'],
template: `
<button>{{label}}</button>
`
})
export class Breadcrumb {
#HostBinding('class.click-enabled')
get clickEnabled() { return this.clickable; }
}
I then combine the two in the component that I am using them with a forEach to create the child breadcrumbs. I also listen for navigation changes to re-generate the array of breadcrumb segments to keep the breadcrumb display up-to-date with the current path.
#Component({
selector: 'app-root',
templateUrl: `
<breadcrumbs>
<breadcrumb *ngFor="let crumb of breadcrumbs" [label]="crumb.label|titlecase" [routerLink]="crumb.uri"></breadcrumb>
</breadcrumbs>
`,
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
breadcrumbs: BreadcrumbSegment[] = [];
constructor(
private router: Router,
private breadService: BreadcrumbService
) { }
ngOnInit() {
this.router.events.subscribe(val => {
if (val instanceof NavigationEnd) {
// Returns an array formatted as: {label:string; uri:string;}[]
this.breadcrumbs = this.breadService.getUrlPathSegments();
}
});
}
}
I am not sure if this is the optimal solution, but it is working for my needs. When the Breadcrumbs component view is initialized, I set the QueryList to dirty, then pipe a delay before I subscribe to the changes. This stops the error from showing up and runs the change detection.
#Component({...})
export class Breadcrumbs implements AfterViewChecked {
ngAfterViewInit() {
// Set to dirty so the changes emit at least one time.
this.breadcrumbs.setDirty();
this.breadcrumbs.changes.pipe(delay(1)).subscribe(() => {
this.enableDisableLast();
});
}
}
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
I have implemented the angular material slide-toggle that all seems to work except for some reason it isn't binding the value to the relevant variable for some reason?
// other irrevelant imports above..
import {MatDialog, MatDialogRef, MAT_DIALOG_DATA} from '#angular/material';
#Component({
selector: 'app-calendar',
templateUrl: './calendar.component.html',
styleUrls: ['./calendar.component.scss'],
host: {
'(document:click)': 'handleClickEvent($event)',
}
})
export class CalendarComponent implements OnInit {
filteringSchedule: boolean = false;
filteringSent: boolean = false;
filteringFailed: boolean = false;
}
// component
<mat-slide-toggle
class="calendar-filter-types"
[ngModel]="(filteringSchedule)"
[color]=""
[checked]="">
Scheduled : {{ filteringSchedule }}
</mat-slide-toggle>
Once I check or uncheck the toggle I would expect the filteringSchedule value to change to true or false accordingly? However within the component it always stays as false for some unknown reason - can anyone suggest why is this?
I am Angular 4
Just update your html to
[(ngModel)]="filteringSchedule"
This works on Angular8+
[(ngModel)]="filteringSchedule"
And make sure you already imported FormsModule
import {FormsModule} from '#angular/forms';
[ngModel]="(filteringSchedule)"
Change this to:
[(ngModel)]="filteringSchedule"
I am having a bit of a problem with getting the following situation done in Angular.
I am using Angular 4 and this is the situation.
app.component.html contains a wrapper div which I would like to be able to change it's color by adding a class to it.
My problem is that I have different layers of components.
For example, if I wanted to change the class in app.component.html it would have this:
app.component - root
button-wrapper.component - holds the button
button.component - message comes from there.
Normally I could do this:
import { Component } from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'app';
myClass = '';
handleEvent(value) {
console.log(value);
myClass = value;
}
}
and in app.component.html have this:
<div [ngClass]="myClass">
<app-button (outputEvent)="handleEvent($event)"></app-button>
</div>
BUT, my problem is that app-button component is inside button-wrapper component so it looks like this:
<div [ngClass]="myClass">
<button-wrapper></div>
</div>
So where would I put this:
(outputEvent)="handleEvent($event)"
How can I go round this issue?
You can chain events as long as their direct children:
button.component.ts (selector: app-button)
startChainEvent() {
this.outputEvent.emit('className');
}
button-wrapper.component.html
<app-button (outputEvent)="handleEvent($event)"></app-button>
button-wrapper.component.ts (selector: app-button-wrapper)
handleEvent(e) {
this.outputEvent.emit(e);
}
app.component.html
<app-button-wrapper (outputEvent)="handleEvent($event)"></app-button-wrapper>
app.component.ts
handleEvent(e) {
myClass = e;
}
Plnkr:
https://plnkr.co/edit/ka6ewGPo1tgnOjRzYr3R?p=preview
I have a subscription that will change overtime:
ngOnInit(){
this.route.snapshot.data.remote.subscribe(x => {
this.obs$ = x[0];
});
}
I have a template work-post.component.html that showcases these changes:
<h1>{{obs$.title}}</h1>
<p>
{{obs$.paragraph}}
</p>
Question:
When obs$ gets each next/new value, Is it possible to animate the enter and leave animations of these values. Can obs$.title obs$.paragraph bindings have a "crossfade" e.g. fade out old text, fade in the new text on change? if so, how could I implement this animation inside of my component and template:
component:
import { Component, ChangeDetectionStrategy, Input, OnInit } from '#angular/core';
import { Observable } from 'rxjs';
import { ActivatedRoute } from '#angular/router';
#Component({
selector: 'work-post',
templateUrl: 'work-post.component.html',
styleUrls: ['work-post.component.scss'],
})
export class WorkPostComponent implements OnInit{
public obs$;
constructor(private route: ActivatedRoute){}
ngOnInit(){
this.route.snapshot.data.remote.subscribe(x => {
this.obs$ = x[0];
});
}
}
Here's a video of how the UI currently looks.
If I understood well, you want to apply X method (animations) when your obs$ changes value. So, in my opinion, you need a listener.
For this, I would go for DoCheck.
In component:
import { Component, DoCheck, ElementRef, OnInit } from '#angular/core';
export class MyClass implements OnInit, DoCheck {
ngDoCheck() {
if(this.obs$ !== 'whateverValue') {
// Apply an animation because this.obs$ changed
}
}
Documentation: https://angular.io/docs/ts/latest/api/core/index/DoCheck-class.html
Update 1:
I suggest that you store an initial version of this.obs$ in, for example, this.obs_initial.
And inside the logic in the listener you compare both, if this.obs$ is different from its previous value (this.obs_initial) then means that this.obs$ changed and therefore we trigger X animation.