Why must I create my VueJS application before using ChartJS? - javascript

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! :)

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>

Related

Cannot read properties of null (reading 'transition'). What does it mean?

I am created graphs with dynamic data from axios by chartjs library.
But when I refresh browser graphs at first nothing available, like this:
If I change dimensions for browser window - all graphs appears:
Error from concole:
Uncaught TypeError: Cannot read properties of null (reading 'transition')
at Chart.transition (Chart.js?473e:9865:1)
at Chart.draw (Chart.js?473e:9827:1)
at Chart.render (Chart.js?473e:9799:1)
at Object.callback (Chart.js?473e:2208:1)
at Object.advance (Chart.js?473e:3544:1)
at Object.startDigest (Chart.js?473e:3517:1)
at eval (Chart.js?473e:3506:1)
I don't have idea, why it can be so, and what to do.
It's look like firstly browser tried build graphs (but data didn't received yet).
During all calculation approximately 580ms.
This MainChart.vue for build charts:
<template>
<div>
<canvas id="main-chart" height="400"></canvas>
</div>
</template>
<script>
import Chart from 'chart.js'
export default {
name: 'MainChart',
props: ['chartData'],
mounted() {
const ctx = document.getElementById('main-chart');
new Chart(ctx, this.chartData);
}
}
</script>
This common page Analytics-test.vue.
I skipped all methods.
<template>
<div> Прибыль/посещаемость <div class="small">
<MainChart :chart-data="datacollection" />
</div>
</div>
</template>
<script>
import MainChart from '../MainChart.vue'
export default {
components: { MainChart },
data: () => ({
flagStartDate: false,
chartData: null,
labels: [],
datasets: {},
draftData: null,
data: [],
datacollection: { type: 'line', },
clubsId: ['5c3c5e12ba86198828baa4a7', '5c3c5e20ba86198828baa4c5', '60353d6edbb58a135bf41856', '61e9995d4ec0f29dc8447f81', '61e999fc4ec0f29dc844835e'],
}),
methods: {
...some a lot of code..},
async mounted() {
await this.loadIncomings(this.clubsId),
await this.loadAvgIncomings(this.clubsId),
await this.loadAvgPayments(this.clubsId),
await this.loadAvgSchedule(this.clubsId),
await this.loadTrainings(this.clubsId)
},
</script>
There are 19 functions in methods, and I decided don't input here.
This error seems similar to the one described in this GitHub issue: https://github.com/chartjs/Chart.js/issues/5149 It sounds like it could be caused due to the fact that the chart is immediately loaded with data, but there are a couple of solutions in the comments on that issue.

maximum calstack size exceeded on vuex commit

I have this error here
I could limit the error to this line here
mounted() {
this.$nextTick(() => {
let ctx = this.$refs.canvas.getContext('2d')
let { chartType, dataOptions } = this.module
this.chart = new Chart(ctx, {
type: chartType,
data: dataOptions,
options: minimizeOptions,
})
})
},
The error comes from dataOptions. If i set data to {} everything works ok, but obviously my chart has no data then.
this.module is an prop that is being passed to my component. The component itself gets rendered in an v-for loop
<module
v-for="mod in modules"
:module="mod"
:key="mod._id.toString()"
/>
I am using here Chart.js.
I cannot find the reason for this call stack exceed error.
Maybe somebody had similar problems?
I also need to mention that this error happens when i want to toggle an global component that is placed in an layout layout
dataOptions:
{
datasets: [
{
backgroundColor: "#34495e",
borderColor: "bdc3c7",
data: [0],
label: "My first dataset"
}
],
labels: ["Start"]
}
I actually does not need reactivity on this prop, so i decided to freeze it up.
let { chartType, dataOptions } = Object.freeze(this.module);
Reactivity is gone an the error aswell. This fixed my problem.
Maybe somebody has an solution for reactive data

Using Vue.js directives within component template

I'm new to Vue.js and trying to create a component that connects to one object within some global-scope data and displays differently based on the specifics of each object. I think I'm misunderstanding how the directives v-if and v-on work within component templates. (Apologies if this should actually be two different questions, but my guess is that the root of my misunderstanding is the same for both issues).
Below is a minimal working example. My goal is to have each member entry only display the Disable button if the associated member is active, and enable changing their status via the button. (I also want to keep the members data at the global scope, since in the actual tool there will be additional logic happening outside of the app itself).
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<member-display
v-for="member in members"
v-bind:member="member"
></member-display>
</div>
<script>
var members = [
{name: "Alex", status: "On"},
{name: "Bo", status: "On"},
{name: "Charley", status: "Off"}
]
Vue.component('member-display', {
props: ['member'],
computed: {
active: function() {
// Placeholder for something more complicated
return this.member.status == "On";}
},
methods: {
changeStatus: function() {
this.member.status = 'Off';
}
},
// WHERE MY BEST-GUESS FOR THE ISSUE IS:
template: `
<div>
{{member.name}} ({{member.status}})
<button v-if:active v-on:changeStatus>Disable</button>
</div>
`
});
var app = new Vue({
el: "#app",
data: {
members: members
}
})
</script>
</body>
</html>
Thanks for your help!
The code v-if and the v-on for the button just have the wrong syntax. The line should look like this:
<button v-if="active" v-on:click=changeStatus>Disable</button>

How to pass data into Vue instance so that subsequent updates are reflected?

I'm trying to dynamically update a Vue JS prop after the view has been loaded and the custom has been initialised. I'm building a custom Vue plugin and am using props to pass options, one of which is a object which I need to dynamically update the value passed after the component has loaded, e.g:
<div id="app">
<script>
var seedData = {
percent: 50,
name: 'Smith'
}
setInterval(() => {
seedData = {
percent: Math.random(),
name: 'Smith'
}
}, 1000)
</script>
<offers :parent-data="seedData"></offers>
</div>
Vue.component('offers', {
template: '<h1>Parent Data: {{ parentData.percent }}</h1>',
props: {
parentData: {
default: () => ({
percent: 0,
name: 'John'
}),
type: Object
},
}
});
// create a new Vue instance and mount it to our div element above with the id of app
var vm = new Vue({
el: '#app'
});
This will load the initial name/values from offersData, however, the new values on the setInterval doesn't get passed through.
I've tried adding a watcher inside of my custom Vue plugin that gets loaded through <offers> but this doesn't seem to work either:
watch: {
parentData: function (newVal) {
this.parentData = newVal
}
}
UPDATE
The following is my implementation:
Code Pen -> https://codepen.io/sts-ryan-holton/pen/VwYNzdZ
There are multiple problems with your code
Everything inside <div id="app"> is treated by the Vue as a template and compiled - see the docs
If neither render function nor template option is present, the in-DOM HTML of the mounting DOM element will be extracted as the template. In this case, Runtime + Compiler build of Vue should be used.
Including <script> tag there is wrong. Just try to include vue.js (debug build) instead of vue.min.js (minified production build of Vue) and you will see bunch of errors (btw its always good idea to use debug build for development as it gives you lots of useful errors and warnings)
The fact that it "somehow works" in prod build (ie the initial values are shown on the page) doesn't mean it's supported...
So the inside of <div id="app"> is template for Vue. As I said before in the comments, all data referenced by template must be in the context of Vue instance. You cannot pass some global variable into props. So moving <script> outside of <div id="app"> won't help
[Vue warn]: Property or method "seedData" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initialising the property.
What you can do is to pass seedData object into root Vue instance like this:
var vm = new Vue({
el: '#app',
data: {
seedData: seedData
}
});
Now the errors are gone but data changes is not reflected still. Reason for that is not Vue specific. Its simple JavaScript. Object are passed by reference in JS. Look at this code:
var a = { name: 'John' }
var b = a
a = { name: 'Smith' }
// b is still pointing to "John" object
To workaround it, don't replace whole object. Just mutate it's properties (beware of Vue reactivity caveats)
setInterval(function() {
seedData.name = 'John';
seedData.percent = Math.random();
}, 1000)
Whole solution:
Vue.component('offers', {
template: '<h1>{{ parentData.name }}: {{ parentData.percent }}</h1>',
props: {
parentData: {
default: () => ({
percent: 0,
name: 'John'
}),
type: Object
},
}
});
// create a new Vue instance and mount it to our div element above with the id of app
var vm = new Vue({
el: '#app',
data: {
seedData: seedData
}
});
<script>
var seedData = {
percent: 60,
name: 'Smith'
}
setInterval(function() {
seedData.name = 'John';
seedData.percent = Math.random();
}, 1000)
</script>
<div id="app">
<offers :parent-data="seedData"></offers>
</div>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.js"></script>

Is it possible in Vue JS to make several vue components reuse existing data structure with different data?

I have two vue.js components, that are based on two same data structures, to that can be assigned different values, received from the server.
I want to autogenerate components (may be in "for" cycle) according to the data, received from server. This autogenerated components should have the same data structure but different values.
For example, now I have implemented dummy approach:
I defined in template two vue.js components
They are based on two different data structures
Data structures are updated separately according to the server response.
How I want to be:
I have something like "generator" for n vue.js components according to the number of series I have received from the server
This components have the same vue data structures
Data structures are updated centrally according to the server response.
My current dummy solution:
https://jsfiddle.net/t2j80bpn/7/
It sounds like your "generator" is just a computed. Use map to transform your data_from_server into an array of objects for your graph. Or visualize_data could do the transformation into a real data item.
Vue.use(HighchartsVue.default)
let data_from_server = [
[1, 2, 3, 1, 8, 1, 9, 2],
[11, 2, 6, 1, 9, 10, 1, 2]
]
var app = new Vue({
el: "#app",
data() {
return {
dfs: []
}
},
computed: {
chartOptions() {
return this.dfs.map((d, i) => ({
id: i,
chart: {
type: 'spline'
},
title: `Chart ${i}`,
series: [ {data: d}]
}))
}
},
methods: {
visualise_data: function(event) {
this.dfs = data_from_server;
}
}
});
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<script src="https://code.highcharts.com/highcharts.js"></script>
<script type="text/javascript" src="https://cdn.rawgit.com/highcharts/highcharts-vue/1ce7e656/dist/script-tag/highcharts-vue.min.js"></script>
<div id="app">
<button v-on:click="visualise_data">Change series</button>
<highcharts v-for="opt in chartOptions" :options="opt" :key="opt.id"></highcharts>
</div>

Categories

Resources