licer93
licer93

Reputation: 351

ngAfterViewInit called too early

I am using am4chart and I got an error in my console : html container not found.

The line which is causing this error is this line of my angular component :
var chart = am4core.create("graphdiv", am4charts.XYChart);

stacktrace in console :

function createChild(htmlElement, classType) {
   var htmlContainer = $dom.getElement(htmlElement);
   if (htmlContainer) {
      ...
   }
   else {
      system.log("html container not found");
      throw new Error("html container not found");
   }
}

So basically what I understand is that this line : var chart = am4core.create("graphdiv", am4charts.XYChart); is executed BEFORE the graphdiv element exists in the DOM, because it didn't found the graphdiv HTML element.

So what I did is that I copy paste my code which creates the graph into the ngAfterViewInit angular function, to make sure that the script which creates my am4chart graph is executed AFTER to make sure that DOM exists before running the script

My afterViewInit function :

ngAfterViewInit(): void {
    console.log("CREATE GRAPH AFTER VIEW INIT")
    this.createGraph();
}

But what I see is that the console.log("CREATE GRAPH AFTER VIEW INIT") is in fact executed BEFORE that the DOM is fully rendered, so what I did doesn't slve my error html container not found

createGraph() :

createGraph() {

    console.log("CREATE GRAAAAPH");

    // Create chart instance
    am4core.ready(function() {

      var chart = am4core.create("graphdiv", am4charts.XYChart);
      chart.numberFormatter.numberFormat = this.currencyBefore + "#,####,###.##" + this.currencyAfter;

      chart.data = [];
      var cumul = 0;

      for (var i = 0; i < 37; i++) {
        if (i == 0) {
          chart.data.push({ "month": "", "units": 0 });
        } 
        else if (i == 1) {
          cumul = cumul + this.AppComponent.activeDetailedResults.prices.upfrontYear1 + this.AppComponent.activeDetailedResults.prices.monthlyPrice;
          chart.data.push({ "month": "M" + i, "units": cumul });
        } 
        else if (i == 13) {
          cumul = cumul + this.AppComponent.activeDetailedResults.prices.upfrontYear2 + this.AppComponent.activeDetailedResults.prices.monthlyPrice;
          chart.data.push({ "month": "M" + i, "units": cumul });
        } 
        else if (i == 25) {
          cumul = cumul + this.AppComponent.activeDetailedResults.prices.upfrontYear3 + this.AppComponent.activeDetailedResults.prices.monthlyPrice;
          chart.data.push({ "month": "M" + i, "units": cumul });
        } 
        else {
          cumul = cumul + this.AppComponent.activeDetailedResults.prices.monthlyPrice;
          chart.data.push({ "month": "M" + i, "units": cumul });
        }
      }

      // Create axes
      let categoryAxis = chart.xAxes.push(new am4charts.CategoryAxis());
      categoryAxis.dataFields.category = "month";
      categoryAxis.renderer.labels.template.disabled = true
      categoryAxis.title.text = "Time (36 months)";

      let valueAxis = chart.yAxes.push(new am4charts.ValueAxis());
      valueAxis.renderer.labels.template.disabled = false;

      // Create series
      var series2 = chart.series.push(new am4charts.LineSeries());
      series2.name = "Units";
      series2.stroke = am4core.color("#551A8B");
      series2.strokeWidth = 3;
      series2.dataFields.valueY = "units";
      series2.dataFields.categoryX = "month";
      series2.fillOpacity = 0.5;
    });
  }

template :

<mat-card class="startcard">
   <p class="mat-title">Cumulative expenses: </p>
   <mat-card-content style="text-align: center;">
      <div fxLayout="row" fxLayoutAlign="space-around center">
         <div id="graphdiv"></div>
      </div>
   </mat-card-content>
</mat-card>

Upvotes: 0

Views: 1938

Answers (2)

Poul Kruijt
Poul Kruijt

Reputation: 71941

So the issue is that the element is not visible at component creation, because you are waiting for API calls. You can make the ViewChild a setter:

If this is your template:

<mat-card class="startcard">
   <p class="mat-title">Cumulative expenses: </p>
   <mat-card-content style="text-align: center;">
      <div fxLayout="row" fxLayoutAlign="space-around center">
         <div #graphdiv></div>
      </div>
   </mat-card-content>
</mat-card>

You can make your component the following:

private _div?: HTMLElement;

@ViewChild('graphdiv')
set graphDiv(div: ElementRef<HTMLElement>) {
  if (div && div.nativeElement && this._div !== div.nativeElement) {
    this._div = div.nativeElement;
    this.createGraph();
  }
}

createGraph() {
  am4core.ready(() => {
    const chart = am4core.create(this._div, am4charts.XYChart);
  });
}

This way you don't have to do it in the ngAfterViewInit hook or after the API call. It will be done automatically

Upvotes: 1

Anton Marinenko
Anton Marinenko

Reputation: 2992

You can try to move your code to ngAfterViewChecked method. As another try is to use setTimeout with 0 time and your code inside.

Upvotes: 1

Related Questions