I'm trying to display data retrieved from a server (using Angular 6, Rxjs and Chartjs), and render a chart using the data. If I use local mock data, everything renders just fine. But if I use get the data from the servers, the necessary data to render the graphs isn't available so the charts render as blank charts.
Summary: A component makes a service call, and prepares an object to pass down to a child component using the response from the service call. However, by the time the response is ready, the object is already sent without the necessary information.
Service code snippet:
getAccountsOfClientId(clientID: string): Observable<Account[]> {
return this.http.get<Account[]>(`${this.BASE_URL}/accounts?client=${clientID}`)
.pipe(
tap(accounts => console.log('fetched client\'s accounts')),
catchError(this.handleError('getAccountsOfClientId', []))
);
}
In client-infoponent.ts (component to make the service call, and prepare and pass the object to child component)
@Input() client; // received from another component, data is filled
constructor(private clientAccountService: ClientAccountService) { }
ngOnInit() {
this.getAccountsOfClientId(this.client.id);
}
ngAfterViewInit() {
this.updateChart(); // render for pie chart
this.updateBarChart(); // render for bar chart
}
getAccountsOfClientId(clientID: string): void {
this.clientAccountService.getAccountsOfClientId(this.client.id)
.subscribe(accounts => this.clientAccounts = accounts);
}
updateBarChart(updatedOption?: any): void {
/* unrelated operations above ... */
// Create new base bar chart object
this.barChart = {};
this.barChart.type = 'bar';
this.setBarChartData();
this.setBarChartOptions('Account', 'Balance');
}
setBarChartData(): void {
// field declarations..
console.log('clientAccounts has length: ' + this.clientAccounts.length); // prints 0
this.clientAccounts.map((account, index) => {
// do stuff
});
dataset = {
label: 'Balance',
data: data,
...
};
datasets.push(dataset);
// since clientAccounts was empty at the time this function ran, the "dataset" object doesn't contain
// the necessary information for the chart to render
this.barChart.data = {
labels: labels,
datasets: datasets
};
}
I'm looking for changes using ngOnChanges (in the child component), however the chart data is NOT updated in the child component after the "clientAccounts" array is filled with the response.
@Input() chart: Chart;
@Input() canvasID: string;
@Input() accountBalanceStatus: string;
ngOnChanges(changes: SimpleChanges) {
if (changes['accountBalanceStatus'] || changes['chart']) {
this.renderChart();
}
}
renderChart(): void {
const element = this.el.nativeElement.querySelector(`#${this.canvasID}`);
if (element) {
const context = element.getContext('2d');
if (this.activeChart !== null) {
this.activeChart.destroy();
}
this.activeChart = new Chart(context, {
type: this.chart.type,
data: this.chart.data,
options: this.chart.options
});
} else {
console.log('*** Not rendering bar chart yet ***');
}
}
Can you point me to how I should continue my research on this?
Sorry for the long question, and thanks!
EDIT: Upon request, the templates are below
Parent (client-info):
<div class='client-info-container'>
<div class='info-container'>
<li>Date of Birth: {{ client.birthday | date: 'dd/MM/yyyy' }}</li>
<li>Name: {{ client.name }}</li>
<li>First Name: {{ client.firstname }}</li>
</div>
<div class='more-button'>
<button (click)='openModal()'>More</button>
</div>
<div class='chart-container'>
<div *ngIf='pieChart && client'>
<app-balance-pie-chart
[chart]='pieChart'
[canvasID]='accountBalancePieChartCanvasID'
(updateChart)='handlePieChartOnClick($event)'>
</app-balance-pie-chart>
</div>
<div class='bar-chart-container'>
<div class='checkbox-container'>
<div *ngFor='let option of cardTypeCheckboxOptions' class='checkbox-item'>
<input
type='checkbox'
name='cardTypeCheckboxOptions'
value='{{option.value}}'
[checked]='option.checked'
[(ngModel)]='option.checked'
(change)="updateCardTypeCheckboxSelection(option, $event)"/>
<p>{{ option.name }} {{ option.checked }}</p>
</div>
</div>
<div *ngIf='barChart && client'>
<!-- *ngIf='client.accounts.length === 0' -->
<div class="warning-text">This client does not have any accounts.</div>
<!-- *ngIf='client.accounts.length > 0' -->
<div>
<app-balance-account-bar-chart
[chart]='barChart'
[canvasID]='accountBarChartCanvasID'
[accountBalanceStatus]='accountBalanceStatus'>
</app-balance-account-bar-chart>
</div>
</div>
</div>
</div>
</div>
Chart:
<div class='bar-chart-canvas-container' *ngIf='chart'>
<canvas id='{{canvasID}}' #{{canvasID}}></canvas>
</div>
I'm trying to display data retrieved from a server (using Angular 6, Rxjs and Chartjs), and render a chart using the data. If I use local mock data, everything renders just fine. But if I use get the data from the servers, the necessary data to render the graphs isn't available so the charts render as blank charts.
Summary: A component makes a service call, and prepares an object to pass down to a child component using the response from the service call. However, by the time the response is ready, the object is already sent without the necessary information.
Service code snippet:
getAccountsOfClientId(clientID: string): Observable<Account[]> {
return this.http.get<Account[]>(`${this.BASE_URL}/accounts?client=${clientID}`)
.pipe(
tap(accounts => console.log('fetched client\'s accounts')),
catchError(this.handleError('getAccountsOfClientId', []))
);
}
In client-info.component.ts (component to make the service call, and prepare and pass the object to child component)
@Input() client; // received from another component, data is filled
constructor(private clientAccountService: ClientAccountService) { }
ngOnInit() {
this.getAccountsOfClientId(this.client.id);
}
ngAfterViewInit() {
this.updateChart(); // render for pie chart
this.updateBarChart(); // render for bar chart
}
getAccountsOfClientId(clientID: string): void {
this.clientAccountService.getAccountsOfClientId(this.client.id)
.subscribe(accounts => this.clientAccounts = accounts);
}
updateBarChart(updatedOption?: any): void {
/* unrelated operations above ... */
// Create new base bar chart object
this.barChart = {};
this.barChart.type = 'bar';
this.setBarChartData();
this.setBarChartOptions('Account', 'Balance');
}
setBarChartData(): void {
// field declarations..
console.log('clientAccounts has length: ' + this.clientAccounts.length); // prints 0
this.clientAccounts.map((account, index) => {
// do stuff
});
dataset = {
label: 'Balance',
data: data,
...
};
datasets.push(dataset);
// since clientAccounts was empty at the time this function ran, the "dataset" object doesn't contain
// the necessary information for the chart to render
this.barChart.data = {
labels: labels,
datasets: datasets
};
}
I'm looking for changes using ngOnChanges (in the child component), however the chart data is NOT updated in the child component after the "clientAccounts" array is filled with the response.
@Input() chart: Chart;
@Input() canvasID: string;
@Input() accountBalanceStatus: string;
ngOnChanges(changes: SimpleChanges) {
if (changes['accountBalanceStatus'] || changes['chart']) {
this.renderChart();
}
}
renderChart(): void {
const element = this.el.nativeElement.querySelector(`#${this.canvasID}`);
if (element) {
const context = element.getContext('2d');
if (this.activeChart !== null) {
this.activeChart.destroy();
}
this.activeChart = new Chart(context, {
type: this.chart.type,
data: this.chart.data,
options: this.chart.options
});
} else {
console.log('*** Not rendering bar chart yet ***');
}
}
Can you point me to how I should continue my research on this?
Sorry for the long question, and thanks!
EDIT: Upon request, the templates are below
Parent (client-info):
<div class='client-info-container'>
<div class='info-container'>
<li>Date of Birth: {{ client.birthday | date: 'dd/MM/yyyy' }}</li>
<li>Name: {{ client.name }}</li>
<li>First Name: {{ client.firstname }}</li>
</div>
<div class='more-button'>
<button (click)='openModal()'>More</button>
</div>
<div class='chart-container'>
<div *ngIf='pieChart && client'>
<app-balance-pie-chart
[chart]='pieChart'
[canvasID]='accountBalancePieChartCanvasID'
(updateChart)='handlePieChartOnClick($event)'>
</app-balance-pie-chart>
</div>
<div class='bar-chart-container'>
<div class='checkbox-container'>
<div *ngFor='let option of cardTypeCheckboxOptions' class='checkbox-item'>
<input
type='checkbox'
name='cardTypeCheckboxOptions'
value='{{option.value}}'
[checked]='option.checked'
[(ngModel)]='option.checked'
(change)="updateCardTypeCheckboxSelection(option, $event)"/>
<p>{{ option.name }} {{ option.checked }}</p>
</div>
</div>
<div *ngIf='barChart && client'>
<!-- *ngIf='client.accounts.length === 0' -->
<div class="warning-text">This client does not have any accounts.</div>
<!-- *ngIf='client.accounts.length > 0' -->
<div>
<app-balance-account-bar-chart
[chart]='barChart'
[canvasID]='accountBarChartCanvasID'
[accountBalanceStatus]='accountBalanceStatus'>
</app-balance-account-bar-chart>
</div>
</div>
</div>
</div>
</div>
Chart:
<div class='bar-chart-canvas-container' *ngIf='chart'>
<canvas id='{{canvasID}}' #{{canvasID}}></canvas>
</div>
Share
Improve this question
edited Jun 21, 2018 at 10:29
saglamcem
asked Jun 21, 2018 at 10:13
saglamcemsaglamcem
6871 gold badge9 silver badges17 bronze badges
6
|
Show 1 more comment
5 Answers
Reset to default 5ngOnChanges(changes: SimpleChanges) { if (changes['accountBalanceStatus'] || changes['chart']) { this.renderChart(); } }
ngOnChanges's argument value is type of SimpleChanges for each Input()
prop:
class SimpleChange {
constructor(previousValue: any, currentValue: any, firstChange: boolean)
previousValue: any
currentValue: any
firstChange: boolean
isFirstChange(): boolean
}
You should check you data by previousValue
, currentValue
.
Something like:
if(changes.accountBalanceStatus.previousValue != changes.accountBalanceStatus.currentValue
|| changes.chart.previousValue != changes.chart.currentValue){
this.renderChart();
}
StackBlitz Demo
I saw that, you are not assigning the data directly to this.barChart
instead you are assigning it as this.barChart.data
, which means you are modifying the property directly, which might not invoke the ngOnChanges
of the child component. This is due to the explanation that you have given in your comments.
I read that it may be because angular change detection checks the differences by looking at the object references
And it will not get to know when the property of object gets changed
The variable that is bound to @Input()
property is this.barChart
and not this.barChart.data
.
Instead of
this.barChart.data = {
labels: labels,
datasets: datasets
};
You try this
this.barChart = {
data : {
labels: labels,
datasets: datasets
}};
here you are directly modifying this.barChart
which should trigger ngOnChanges()
.
EDIT :
You should be invoking this.updateChart();
inside subscribe
block of
this.clientAccountService.getAccountsOfClientId(this.client.id)
.subscribe((accounts) => {
this.clientAccounts = accounts;
this.updateChart();
})
That is why you also have this.clientAccounts.length
as 0
Your component needs to have the data before rendering. You may use resolve, a built in feature that Angular provides to handle use-cases like the ones you described.
Also look here. may be a useful resource in a tutorial form.
You need to interact with child components from parent this you nned to use input binding.
Refer:
https://angular.io/guide/component-interaction#pass-data-from-parent-to-child-with-input-binding
My issue is solved and I'd like to share the solution in case anyone needs it in the future. As Amit Chigadani suggested (in the comments), invoking my chart updating functions in the subscribe block worked.
getAccountsOfClientId(clientID: string): void {
this.clientAccountService.getAccountsOfClientId(this.client.id)
.subscribe(accounts => {
this.clientAccounts = accounts;
this.updateChart();
this.updateBarChart();
});
}
if (changes['accountBalanceStatus'] || changes['chart']) { }
and check if you are getting the right data after the asynchronous call is resolved.ngOnChanges
will usually fire multiple times for each data change. – Amit Chigadani Commented Jun 21, 2018 at 10:35