Skip to content
Advertisement

Why must I create my VueJS application before using ChartJS?

I have this very simple page, that works properly:

<!DOCTYPE html>
<html lang="en">

<head>
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.0/dist/chart.js"></script>
</head>

<body>
    <main id="vue-app">
        <p>{{ name }}</p>
        <canvas id="chart-flow-rate"></canvas>
    </main>
</body>

<script>    
    // Start VueJS
    const Application = {
        data() {
            return {
                name: "My Chart"
            };
        }
    }
    vm = Vue.createApp(Application).mount('#vue-app');

    // Use ChartJS
    const myChart = new Chart('chart-flow-rate', {
        type: 'bar',
        data: {
            labels: ['4', '2'],
            datasets: [{
                data: [4, 2],
            }]
        }
    });
</script>

However, if I invert the blocs of JS to use ChartJS first and then to create the VueJS application, the page no longer works: the chart is not shown.

Why?

I have noticed that I can change my HTML’s body a little in order to be able to use ChartJS before VueJS:

<body>
    <main>
        <p id="vue-app">{{ name }}</p>
        <canvas id="chart-flow-rate"></canvas>
    </main>
</body>

Again: why?

Thanks a lot! 🙂

Advertisement

Answer

That is because VueJS uses virtual DOM and it updates the actual DOM in batches: what you’re providing your app is an inline template, which VueJS will parse, interpolate, and rewrite it to the DOM. Therefore if you initialise ChartJS first, it will lose the reference to the DOM element (it is either gone because of a race condition with VueJS, or it is quickly overwritten by Vue once it computes virtual DOM and outputs it to the actual DOM).

The reason why changing your markup worked is because now the element that ChartJS uses to mount and render the chart is no longer erased or manipulated by VueJS, as it is no longer part of the Vue app but resides outside of it in regular DOM.

In fact, the most parsimonious solution is to simply instantiate ChartJS in the mounted hook of your VueJS app, and after waiting for the DOM to be ready, i.e.:

mounted: async function() {
    // Wait for the DOM to be rendered after next tick
    await this.$nextTick();

    // Use ChartJS with the element that now exists in the DOM
    const myChart = new Chart('chart-flow-rate', {
        type: 'bar',
        data: {
            labels: ['4', '2'],
            datasets: [{
                data: [4, 2],
            }]
        }
    });
}

In fact, I would go a step further and implore you to avoid using DOM query methods and take advantage of the ref attribute and the $refs instance property. In your template, update your markup to use refs:

<canvas ref="chart"></canvas>

Since ChartJS accepts an element as the first argument, you can simply access the element using this.$refs.canvas:

const myChart = new Chart(this.$refs.chart, { ... })

See proof-of-concept below:

// Start VueJS
const Application = {
  data() {
    return {
      name: "My Chart"
    };
  },
  mounted: async function() {
    await this.$nextTick();

    // Use ChartJS
    const myChart = new Chart(this.$refs.chart, {
      type: 'bar',
      data: {
        labels: ['4', '2'],
        datasets: [{
          data: [4, 2],
        }]
      }
    });
  }
}
vm = Vue.createApp(Application).mount('#vue-app');
<script src="https://unpkg.com/vue@next"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.0/dist/chart.js"></script>
<main id="vue-app">
  <p>{{ name }}</p>
  <canvas ref="chart"></canvas>
</main>
User contributions licensed under: CC BY-SA
1 People found this is helpful
Advertisement