I am trying to use reactive data mixin for vue-chartjs
The mounted function to set the initial data is working and I can see the chart correctly using the API response:
fetchSessionTrends() {
axios.get(endpoint)
.then(({data}) => {
this.sessions = data.map(session => session.sessions);
this.sessionLabels = data.map(label => label.date);
this.loaded = true;
});
},
The data:
data() {
return {
endpoint: 'public/api/sessions',
sessions: [],
sessionLabels: [],
loaded: false,
daysFilter: 14
};
},
I am display the chart with a text field to provide the reactive portion - expectation is that it calls the endpoint again and receives new response
<div class="col-md-2 session-filter">
<div class="input-group">
<input type="text" class="form-control" placeholder="days..." v-model="daysFilter">
<span class="input-group-btn">
<button class="btn btn-secondary" type="button" #click="refreshSessions">Go</button>
</span>
</div>
</div>
<line-chart v-if="loaded" :chart-data="sessions" :chart-labels="sessionLabels"></line-chart>
To test the reactive part however, for now I am simply changing the data arrays directly to see how it works:
refreshSessions() {
this.sessions = [1, 2, 3];
this.sessionlabels = ["june", "july", "august"];
},
Right, so this is giving me the errors
[Vue warn]: Error in callback for watcher "chartData": "TypeError: Cannot read property 'map' of undefined" found in ....
TypeError: Cannot read property 'map' of undefined
LineChart.js is as described in the docs, abbreviated here for space
import { Line, mixins } from 'vue-chartjs';
const { reactiveProp } = mixins
extends: Line,
mixins: [reactiveProp],
props: {
chartData: {
type: Array,
required: true
},
chartLabels: {
type: Array,
required: true
}
},
mounted() {
this.renderChart({
labels: this.chartLabels,
datasets: [
{
label: 'sessions',
data: this.chartData
}
]
}, this.options)
}
So, chart is initially working fine but I can't seem to get the reactive part working.
This will not work. Because the reactiveMixins assume that chartData is the whole chartjs dataset object. With the dataset array, with the labels etc.
But you are splitting it up, this way the reactiveMixins can't work.
Because your chartData is only the pure data of one dataset.
To solve it, you can do two things:
Pass in the whole dataset object
Add own watchers.
I guess the most simple method would be to add two watchers to watch chartData and chartLabel and on a change call this.$data._chart.update()
Related
Below is the data in a component
data: function () {
return {
sltAreaStyle: {
paddingTop: "3%",
},
checkedTypes: ["krw", "btc", "usdt"],
};
},
Below is watch function of checkedTypes data
watch: {
checkedTypes: {
handler: function (newVal, oldVal) {
if (newVal.length < 1) {
alert("Choose one or more.");
var last = oldVal[0];
this.$data.checkedTypes = [last];
}
},
},
},
Below is my html template
<div class="ckbxArea">
<input type="checkbox" value="krw" v-model="checkedTypes">KRW</input>
<input type="checkbox" value="btc" v-model="checkedTypes">BTC</input>
<input type="checkbox" value="usdt" v-model="checkedTypes">USDT</input>
</div>
I want to change the last value to checkedTypes data when all the check boxes are unchecked.
If the first checkbox was finally unchecked, the checkedTypes would be 'krw' like checkedTypes = ['krw'] The checkedTypes data is ['krw'], but all checkbox tags are unchecked. That is, dom has not been updated. I don't think I understand Vue's life cycle well. I think this problem is related to the life cycle of v-model and components, but I don't know what the problem is. Please explain why this problem occurs and tell me how to solve it.
Well this is more about Vue rendering mechanisms for v-modeleld input controls.
Check this:
Only one last checkbox is checked so model value is ['krw']
Uncheck last checkbox
Watcher is executed - new model value is [] BUT the watcher immediately sets it to same value as before ... ['krw']
Vue re renders the template (see the message in the console) BUT as the v-model value is same as during last render, it does not update the checkbox
Simple solution to situations like this is to postpone the update to next rendering cycle using nextTick
this.$nextTick(() => {
this.checkedTypes = [last];
})
new Vue({
el: "#app",
data: function () {
return {
checkedTypes: ["krw", "btc", "usdt"],
};
},
updated() {
console.log("Component updated")
},
watch: {
checkedTypes: {
handler: function (newVal, oldVal) {
if (newVal.length < 1) {
alert("Choose one or more.");
//console.log("Choose one or more.");
var last = oldVal[0];
// this.checkedTypes = [last];
this.$nextTick(() => {
this.checkedTypes = [last];
})
}
},
},
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.14/vue.js"></script>
<div id="app">
<input type="checkbox" value="krw" v-model="checkedTypes"/> KRW
<input type="checkbox" value="btc" v-model="checkedTypes"/> BTC
<input type="checkbox" value="usdt" v-model="checkedTypes"/> USDT
<pre>{{ checkedTypes }}</pre>
</div>
I'm trying to pass an array of object to a childComponent as prop but when I add an object in it, it doesn't render. (Note: I'm working on vuejs 2.6)
I suppose it has a link with the "monitoring" of the items of the array and not the array itself? Stuff is that if I do not pass the prop and use the default value instead, it's working perfectly. I think I'm missing something here. Could someone help me ?
By curiosity is this kind of behavior still stand with vue3js ?
As you can see below:
App.vue:
<template>
<div id="app">
<Card
v-for="user in users"
:key="user.userId"
:userId="user.userId"
:username="getUsernameFromUserId(user.userId)"
:links="getUserLinksFromUserId(user.userId)"
/>
</div>
</template>
<script>
import Card from "./components/Card.vue";
export default {
name: "App",
components: {
Card,
},
data: function () {
return {
users: [
{ userId: 1, name: "Bob" },
{ userId: 2, name: "Alice" },
{ userId: 3, name: "Eliot" },
],
links: [
{ userId: 1, link: "hello->world" },
{ userId: 1, link: "world->!" },
{ userId: 3, link: "hello->back" },
{ userId: 4, link: "hello->you" },
],
};
},
methods: {
getUsernameFromUserId: function (userId) {
return this.users.filter((obj) => obj.userId == userId)?.[0]?.name ?? "Not found";
},
getUserLinksFromUserId: function (userId) {
return this.links.filter((obj) => obj.userId == userId);
},
},
};
</script>
Card.vue
<template>
<div class="card">
<h1>{{ username }}</h1>
<button #click="addLink">Add One link</button><br><br>
<span v-if="links.length == 0">No links</span>
<div class="links">
<Link v-for="link in links" :key="links.indexOf(link)" :link="link"></Link>
</div>
</div>
</template>
<script>
import Link from '../components/Link'
export default {
components:{Link},
props: {
userId: Number,
username: String,
links: { type: Array, default: () => [], required: false },
},
methods:{
addLink: function(){
this.links.push({
userId: this.userId,
link: 'newlink->cool'
});
}
}
}
</script>
Link.vue
<template>
<div>
<span>UserId: {{ this.link.userId }} Link: {{ this.link.link }</span>
</div>
</template>
<script>
export default {
props: {
link: { type: Object, default: () => [], required: false },
},
};
</script>
This is a bad way to work with props
Note: do not focus on Dev Tools too much as it can be "buggy" at times - especially if you use Vue in a wrong way. Focus on your app output
Your Card.vue component is modifying (push) a prop, which is not recommended but it sort of works if the prop is object/Array and you do not replace it, just modify it's content (as you do)
But in your case, the values passed to props are actually generated by a method! The getUserLinksFromUserId method is generating a new array every time it is called, and this array is NOT reactive. So by pushing to it, your component will not re-render and what is worse, parent's links array is not changed at all! (on top of that - if App.vue ever re-renders, it will generate new arrays, pass it to pros and your modified arrys will be forgoten)
So intead of modifying links prop in Card.vue, just emit an event and do the modification in App.vue
I have to create a dynamic form in vue2. I want to save the values of the dynamic fields in an named object so that I can pass them along on submit.
The following code is working fine except I get an error in the console when I change the input value the first time (value will be propagated correctly though):
[TypeError: Cannot read property '_withTask' of undefined]
Here is how I define the props:
props: {
fields: {
type: Object,
default: {startWord: 'abc'}
},
},
And this is how I populate the model from the input field:
v-model="fields[field.id]"
Here is the entire code:
<template>
<div>
<!-- Render dynamic form -->
<div v-for="(field, key) in actionStore.currentAction.manifest.input.fields">
<!-- Text -->
<template v-if="field.type == 'string'">
<label>
<span>{{key}} {{field.label}}</span>
<input type="text" v-bind:placeholder="field.placeholder"
v-model="fields[field.id]"/>
</label>
</template>
<!-- Footer -->
<footer class="buttons">
<button uxp-variant="cta" v-on:click="done">Done</button>
</footer>
</div>
</template>
<script>
const Vue = require("vue").default;
const {Bus, Notifications} = require('../../Bus.js');
module.exports = {
props: {
fields: {
type: Object,
default: {startWord: 'abc'}
},
},
computed: {
actionStore() {
return this.$store.state.action;
},
},
methods: {
done() {
console.log('fields', this.fields);
Bus.$emit(Notifications.ACTION_INPUT_DONE, {input: this.fields});
}
},
}
</script>
So again, everything is working just fine (showing initial value in input, propagating the new values to the model etc.). But I get this '_withTask' error when I first enter a new character (literally only on the first keystroke). After that initial error it doesn't pop up again.
-- Appendix --
This is what the manifest/fields look like:
manifest.input = {
fields: [
{ id: 'startWord', type: 'string', label: 'Start word', placeholder: 'Enter start word here...' },
{ id: 'startWordDummy', type: 'string', label: 'Start word dummy', placeholder: 'Enter start word here...' },
{ id: 'wordCount', type: 'integer', label: 'Word count' },
{ id: 'clean', type: 'checkbox', label: 'Clean up before' },
]
}
-- Update --
I just discovered that if I set the dynamic field values initially with static values I don't get the error for those fields set this way:
created() {
this.fields.startWord = 'abc1';
},
But this is not an option since it will be a dynamic list of fields. So what is the best way to handle scenarios like this?
From documentation: Due to the limitations of modern JavaScript (and the abandonment of Object.observe), Vue cannot detect property addition or deletion. Since Vue performs the getter/setter conversion process during instance initialization, a property must be present in the data object in order for Vue to convert it and make it reactive.
As I understand it's bad idea create keys of your object by v-model.
What would I do in HTML:
<input type="text"
:placeholder="field.placeholder"
#input="inputHandler(event, field.id)" />
Then in JS:
methods: {
// ...
inputHandler({ target }, field) {
this.fields[field] = target.value;
}
},
I'm very new to js frontend frameworks, and I'm trying to learn some vue.js.
In particular I'm trying to render an array of Note objects that have an id, description and date attributes.
I'm trying to do all of this in a component :)
My ul element looks like
<ul class="list-group">
<li
is="note-item"
v-for="note in notesList"
v-bind:title="note.description"
:key="note.id"
></li>
</ul>
And my Vue code looks like:
Some notes:
I run on page load vue.updateNoteList which calls vue.loadFirstNote.
Vue.component('note-item', {
template: '\
<li>\
{{ note.description }}\
<button v-on:click="$emit(\'remove\')">X</button>\
</li>\
',
props: ['notesList']
});
const vm = new Vue({
el: '#main-content',
data: {
input: '',
notesList: [{ }]
},
methods: {
updateNoteList: function (callback) {
const that = this;
Note.all((notes) => {
that.notesList = notes;
return callback();
});
},
loadFirstNote: function () {
if (this.notesList.length > 0) {
this.note = this.notesList[0];
}
}
});
I've been trying to get this working all day, and I'm getting nowhere. I'm getting the following console errors. Any help would be appreciated.
vue.common.js?e881:519 [Vue warn]: Property or method "note" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
vue.common.js?e881:519 [Vue warn]: Error when rendering component <note-item>:
vue.common.js?e881:2961 Uncaught TypeError: Cannot read property 'description' of undefined
I can see two errors in your code
you are trying to use note in your component, but you have not passed it as props in that component, you have notesList which you dont use in view.
You have use \ in $emit, which is not required
Following are these fixes:
HTML:
<ul class="list-group">
<li
is="note-item"
v-for="note in notesList"
v-bind:title="note.description"
:key="note.id"
:note="note"
></li>
</ul>
JS:
Vue.component('note-item', {
template: '\
<li>\
{{ note.description }}\
<button v-on:click="$emit('remove')">X</button>\
</li>\
',
props: ['note']
});
const vm = new Vue({
el: '#main-content',
data: {
input: '',
notesList: [{ }]
},
methods: {
updateNoteList: function (callback) {
const that = this;
Note.all((notes) => {
that.notesList = notes;
return callback();
});
},
loadFirstNote: function () {
if (this.notesList.length > 0) {
this.note = this.notesList[0];
}
}
});
I am fairly new to vue and can't figure out how to add data values within a template. I am trying to build a very basic form builder. If I click on a button it should add another array of data into a components variable. This is working. The I am doing a v-for to add input fields where some of the attributes are apart of the array for that component. I get it so it will add the input but no values are being passed into the input.
I have created a jsfiddle with where I am stuck at. https://jsfiddle.net/a9koj9gv/2/
<div id="app">
<button #click="add_text_input">New Text Input Field</button>
<my-component v-for="comp in components"></my-component>
<pre>{{ $data | json }}</pre>
</div>
new Vue({
el: "#app",
data: function() {
return {
components: [{
name: "first_name",
showname: "First Name",
type: "text",
required: "false",
fee: "0"
}]
}
},
components: {
'my-component': {
template: '<div>{{ showname }}: <input v-bind:name="name" v-bind:type="type"></div>',
props: ['showname', 'type', 'name']
}
},
methods: {
add_text_input: function() {
var array = {
name: "last_name",
showname: "Last Name",
type: "text",
required: "false",
fee: "0"
};
this.components.push(array);
}
}
})
I appreciate any help as I know I am just missing something obvious.
Thanks
Use props to pass data into the component.
Currently you have <my-component v-for="comp in components"></my-component>, which doesn't bind any props to the component.
Instead, do:
<my-component :showname="comp.showname"
:type="comp.type"
:name="comp.name"
v-for="comp in components"
></my-component>
Here is a fork of your fiddle with the change.
while asemahle got it right, here is a boiled down version on how to expose data to the child component. SFC:
async created() {
await this.setTemplate();
},
methods: {
async setTemplate() {
// const templateString = await axios.get..
this.t = {
template: templateString,
props: ['foo'],
}
},
},
data() {
return {
foo: 'bar',
t: null
}
}
};
</script>
<template>
<component :is="t" :foo="foo"></component>
It pulls a template string that is compiled/transpiled into a js-render-function. In this case with Vite with esm to have a client-side compiler available:
resolve: {
alias: {
// https://github.com/vuejs/core/tree/main/packages/vue#with-a-bundler
vue: "vue/dist/vue.esm-bundler.js",
the index.js bundle size increases by few kb, in my case 30kb (which is minimal)
You could now add some what-if-fail and show-while-loading with defineasynccomponent