Search code examples
javascripthtmlcssweb-componentamcharts

How to load libraries before am4core is called in amCharts?


I have a requirement where I have to create a custom widget using Amcharts. But I am facing problem that before the libraries are loaded am4core function is called.

HTML Code

<com-sap-sample-helloworld5></com-sap-sample-helloworld5>

Js code


(function () {
  const amchartscorejs = "https://www.amcharts.com/lib/4/core.js";
  const amchartschartsjs = "https://www.amcharts.com/lib/4/charts.js";
  const amchartsanimatedjs = "https://www.amcharts.com/lib/4/themes/animated.js";
  const vennchartjs = "https://cdn.amcharts.com/lib/4/plugins/venn.js";
  async function LoadLibs() {
    console.log("LoadLibs");
    try {
      await loadScript(amchartscorejs);
      await loadScript(amchartschartsjs);
      await loadScript(amchartsanimatedjs);
      await loadScript(vennchartjs);
    } catch (e) {
      alert(e);
    } finally {
      that._firstConnection = 1;
    }
  }
  LoadLibs();
  function loadScript(src) {
    console.log("LoadScript");
    return new Promise(function (resolve, reject) {
      let script = document.createElement("script");
      script.src = src;
      script.onload = () => {
        console.log("Load: " + src);
        resolve(script);
      };
      script.onerror = () => reject(new Error(`Script load error for ${src}`));
      document.head.appendChild(script);
    });
  }
  let template = document.createElement("template");
  template.innerHTML = `<div id="chartdiv" width="100%" height="500px"></div>`;
  customElements.define(
    "com-sap-sample-helloworld5",
    class HelloWorld extends HTMLElement {
      constructor() {
        super();
        let shadowRoot = this.attachShadow({
          mode: "open",
        });
        shadowRoot.appendChild(template.content.cloneNode(true));
        this._firstConnection = false;
        this.addEventListener("click", (event) => {
          var event = new Event("onClick");
          this.dispatchEvent(event);
        });
      }
      //Fired when the widget is added to the html DOM of the page
      connectedCallback() {
        this._firstConnection = true;
        this.redraw();
      }
      //Fired when the widget is removed from the html DOM of the page (e.g. by hide)
      disconnectedCallback() {}
      //When the custom widget is updated, the Custom Widget SDK framework executes this function first
      onCustomWidgetBeforeUpdate(oChangedProperties) {}
      //When the custom widget is updated, the Custom Widget SDK framework executes this function after the update
      onCustomWidgetAfterUpdate(oChangedProperties) {
        if (this._firstConnection) {
          this.redraw();
        }
      }
      //When the custom widget is removed from the canvas or the analytic application is closed
      onCustomWidgetDestroy() {}
      //When the custom widget is resized on the canvas, the Custom Widget SDK framework executes the following JavaScript function call on the custom widget
      // Commented out by default
      /*
    onCustomWidgetResize(width, height){
    
    }
    */
      get chartType() {
        return this.chartTypeValue;
      }
      set chartType(value) {
        this.chartTypeValue = value;
      }

      redraw() {
        console.log("redraw function");
        // Themes begin
        am4core.useTheme(am4themes_animated);
        // Themes end
        var data = [
          { name: "A", value: 10 },
          {
            name: "B",
            value: 10,
          },
          {
            name: "C",
            value: 10,
          },
          {
            name: "X",
            value: 2,
            sets: ["A", "B"],
          },
          {
            name: "Y",
            value: 2,
            sets: ["A", "C"],
          },
          {
            name: "Z",
            value: 2,
            sets: ["B", "C"],
          },
          {
            name: "Q",
            value: 1,
            sets: ["A", "B", "C"],
          },
        ];

        var chart = am4core.create("chartdiv", am4plugins_venn.VennDiagram);
        var series = chart.series.push(new am4plugins_venn.VennSeries());
        series.dataFields.category = "name";
        series.dataFields.value = "value";
        series.dataFields.intersections = "sets";
        series.data = data;
        chart.legend = new am4charts.Legend();
        chart.legend.marginTop = 40;
      }
    }
  );
})();


Please tell what changes should I do so that first amCharts libraries are loaded and then redraw() function is called.

You can also check the logs in jsfiddle to understand what problem I am facing.

Thanks in advance.


Solution

  • I started with Promise.all, but ended up on an Amcharts Issue that Amcharts needs to be loaded in sequence.

    ES7 awaits will do the job

    You were halfway there, but stopped the async part and continued with sync loadLibs, it looks like you then struggled trying to flag the element when loading was done.

    Here libraries load in sequence, with a generic loadScripts function,
    ready for multiple custom elements depending on AmCharts, script will only load once:

    (function() {
      function log() {
        let args = [...arguments];
        //console.log(`%c ${args.shift()} `, "background:lightgreen", ...args); //Chrome!
        document.body.append(args.join` `,document.createElement('BR'));
      }
      log("start IIFE script");
      async function loadScripts() {
        const load = (src) => new Promise((resolve, reject) => {
          const script = document.createElement('script');
          script.src = `https://www.amcharts.com/lib/4/${src}.js`;
          if (document.querySelector(`[src="${script.src}"]`)) resolve();
          log("load", script.src)
          script.onload = resolve;
          //script.onerror = () => reject(new Error(`Script load error for ${src}`));
          document.head.append(script)
        });
        await load("core"); // must be loaded first
        await load("charts");
        await load("themes/animated");
        await load("plugins/venn");
        return " / return (not even required)";
      }
      customElements.define('my-element', class extends HTMLElement {
        connectedCallback() {
          this.attachShadow({mode: "open"}).innerHTML = `<div>Executing:</div>`;
          log('connectedCallback');
          loadScripts().then(result => {
            log('done loading', result);
          });
        }
      });
    
    })();
    <my-element></my-element>

    You could move everything inside the (now async marked) connectedCallback

      async connectedCallback() {
        const load = (src) => new Promise((resolve, reject) => {
          let script = document.createElement('script');
          script.src = `https://www.amcharts.com/lib/4/${src}.js`;
          //if (document.querySelector(`[src="${script.src}"]`)) resolve();
          script.onload = resolve;
          this.append(script); // fine, doesn't really matter where SCRIPT is injected
        });
        await load("core");
        await load("charts");
        await load("themes/animated");
        await load("plugins/venn");
        // Loaded all
      }
    
    

    If the connectedCallback runs multiple times because you move DOM nodes around you need that check for already loaded script