Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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>
like image 403
saglamcem Avatar asked Jun 21 '18 10:06

saglamcem


People also ask

Which decorator is used to pass the data from parent to child?

Pass data from parent to child component using @Input() decorator, which allows data to pass through templates and child to parent component using @Output() decorator with the help of Event Emitter.

How do you access the parent component property in a child component?

In the parent component, declare the property that you want to receive in the child component, say 'ParentId'. While including the child component inside the parent component, bind the 'ParentId' property to the child component using property binding.


3 Answers

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

like image 180
Amit Chigadani Avatar answered Oct 18 '22 14:10

Amit Chigadani


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.

like image 44
siva636 Avatar answered Oct 18 '22 13:10

siva636


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

like image 41
Yerkon Avatar answered Oct 18 '22 12:10

Yerkon