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>