saglamcem
saglamcem

Reputation: 687

How to pass async data to child components in an object (Angular6)

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>

Upvotes: 9

Views: 14804

Answers (5)

Amit Chigadani
Amit Chigadani

Reputation: 29775

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

Upvotes: 4

Yerkon
Yerkon

Reputation: 4798

ngOnChanges(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

Upvotes: 5

saglamcem
saglamcem

Reputation: 687

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();
      });
}

Upvotes: 0

siva636
siva636

Reputation: 16451

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.

Upvotes: 1

user6299088
user6299088

Reputation:

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

Upvotes: 0

Related Questions