Mutating a value in vue when the key didn't previously exist does not update the view - vue.js

I have a table and a select box for each row. I want the check box to model a value in the data that doesn't actually exist, yet.
<tr v-for="item in someData">
<input type="checkbox" v-model="item.selected"></td>
<input type="checkbox" v-model="item.name"></td>
<tr>
My data when loaded from the DB looks like this:
someData: [
{'name': 'john'},
{'name': 'kate'},
{'name': 'aaron'},
]
When the user presses a Select All button it should update the selected key even if it doesn't exist (well thats the idea)
toggleSelect: function () {
this.someData.forEach(element => {
element.selected = !element.selected;
});
}
However the checkboxes don't react even though the values have been updated. To make this work I need to get the data and add the key/value manually prior to loading it into view and rendering
getDatabaseData: function () {
// some code omitted
response['data'].forEach(element => {
element["selected"] = false;
});
app.someData = response['data']
}
Am I doing it correctly? Am I right in thinking Vue won't be reactive to values that didn't exist prior to rendering?

Try this idea,
in vue component.
<input type="checkbox" v-model="selectAll"> Select All
<tr v-for="item in someData" :key="item.name">
<td>
<input type="checkbox" v-model="selected" :value="item.name">
</td>
{{ item.name }}
</tr>
script:
data() {
return {
selectAll: false,
selected: [],
someData: [{ name: "john" }, { name: "kate" }, { name: "aaron" }]
};
},
watch: {
selectAll(value) {
// validate if value is true
if (value) {
this.someData.forEach(item => {
// push unique value
if(this.items.indexOf(item.name) === -1) {
this.selected.push(item.name);
}
});
} else {
// Unselect all
this.selected = [];
}
}
}
You have a selected variable where the selected Items are located. selectAll variable to select all items and push to selected variable.

You should be using Vue.set to update the value of the selected property on your objects in order to be reactive, like this:
import Vue from 'vue';
...
toggleSelect: function () {
this.someData.forEach(element => {
Vue.set(element, 'selected', !element.selected);
});
}

Related

When data is changed within the watch function, dom does not update

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>

Why are checkboxes not reset by v-model?

This is what i have:
Template
<div
v-for="(filter, index) in filtersList"
:key="index"
class="option-block"
>
<label
v-for="value in filter.values"
:key="value.id"
class="option-block__container"
>
{{ value.title }}
<input
type="checkbox"
v-model="filtersValues[filter.name]"
:value="value.value"
>
<span class="option-block__checkmark"></span>
</label>
</div>
And the part of my vue code:
data() {
return {
filtersList: {},
filtersValues: {}
}
},
beforeMount() {
this.loadInitData();
this.initFilters();
},
methods: {
loadInitData() {
const data = JSON.parse(this.$el.getAttribute('data-data'));
this.filtersList = data.filters;
},
initFilters() {
for (let i in this.filtersList) {
if (!this.filtersList.hasOwnProperty(i)) {
continue;
}
this.filtersValues[this.filtersList[i].name] = [];
}
}
}
It works, but when i call initFilters() method again (for reseting) checkboxes are still selected, and i don't know why.
The way you are assigning new, empty arrays to filterValues is not reactive.
If you change your initFilters to assign an entire new value to filterValues, you don't need to worry about using Vue.set(). For example
initFilters() {
this.filtersValues = this.filtersList.reduce((vals, { name }) => ({
...vals,
[ name ]: []
}), {})
}
Demo ~ https://jsfiddle.net/cjx09zwt/
Where did filter.values come from in line 2 of template?
Anyways vue would not be able to track the changes you are making (judging from the visible code)
There are some caveats to vue 2's reactivity. Check here for more info.
TLDR; you will need to declare anything you want to be made reactive in the component's data option upfront.
HTH

Dynamic prop binding from parent to child

So I'm having a headache since yesterday on this. I'd like to have some kind of two way binding of data. I want for my array data(): exploredFields be able to update the values of the children components. But the update is called from the child.
Here is my parent component.
<div>
<button #click="resetMinefield(rows, mineCount)">RESET</button>
</div>
<div class="minefield">
<ul class="column" v-for="col in columns" :key="col">
<li class="row" v-for="row in rows" :key="row">
<Field
:field="mineFields[(row + (col - 1) * rows) - 1].toString()"
:id="(row + (col - 1) * rows) - 1"
:e="exploredFields[(row + (col - 1) * rows) - 1]" // This doesn't update the child !!!
#update="exploreField"
/>
</li>
</ul>
</div>
...
<script>
import Field from "#/components/Field";
export default {
name: "Minesweeper",
components: {
Field,
},
created() {
this.resetMinefield(this.rows, this.mineCount);
},
data() {
return {
rows: 7,
columns: 7,
mineCount: 5,
mineFields: [],
exploredFields: [],
}
},
methods: {
exploreField(index) {
this.exploredFields[index] = true;
},
resetMinefield(fieldSize, mineCount) {
// Updates this.mineFields
// Passes data properly to the child
}
},
}
</script>
And here is a child component. On #click it updates self data: explored and parents data: exploredFields. But the dynamic binding for props: e does not work.
<template>
<div :class="classObject" #click="changeState">
{{ field }}
</div>
</template>
<script>
export default {
name: "Field",
data() {
return {
explored: false,
}
},
props: {
id: {
type: Number,
default: -1,
},
field: {
type: String,
default: 'X',
},
e: {
type: Boolean,
default: false,
}
},
methods: {
changeState() {
this.$emit("update", this.id);
this.explored = true;
}
},
computed: {
classObject: function() {
// Stuff here
}
},
}
</script>
I also tried to do dynamic binding for data :explored instead of props :e, but no effect there too. It seams that it doesn't want to update because I'm calling the update from the child. And even I can see the data changing, it is not passed back to child dynamically
Looks like a common Vue change detection caveat.
Vue cannot detect the following changes to an array:
When you directly set an item with the index, e.g. vm.items[indexOfItem] = newValue
This...
this.exploredFields[index] = true
won't be reactive. Try this instead
this.$set(this.exploredFields, index, true)
What you probably need is a watcher inside your child component, on the prop "e"
watch: {
e(newValue){
this.explored = newValue
}
}
Everything else looks fine to me, once you click on the field you are emitting an event and youre listening to that in the parent.

changing a single value using v-model / full table is redrawn

I was building an editable table, which began to crawl to a halt when the number of rows started to run in the 100's. This led me to investigate what was going on.
In the example below, when changing the value in the input, the whole table is redrawn, and the ifFunction() function is trigged 4 times.
Why is this happening? Shouldn't Vue be capable of just redrawing the respective cell? Have I done something wrong with the key-binding?
<template>
<div id="app">
<table border="1" cellpadding="10">
<tr v-for="(row, rowKey) in locations" :key="`row_+${rowKey}`">
<td v-for="(column, columnKey) in row" :key="`row_+${rowKey}+column_+${columnKey}`">
<span v-if="ifFunction()">{{ column }}</span>
</td>
</tr>
</table>
<input v-model="locations[0][1]">
</div>
</template>
<script>
export default {
data() {
return {
locations: [
["1","John"],
["2","Jake"]
], // TODO : locations is not generic enough.
}
},
methods: {
ifFunction() {
console.log('ifFunction');
return true;
},
}
}
</script>
The data property defines reactive elements - if you change one part of it, everything that's depending on that piece of data will be recalculated.
You can use computed properties to "cache" values, and only update those that really need updating.
I rebuilt your component so computed properties can be used throughout: created a cRow and a cCell component ("custom row" and "custom cell") and built back the table from these components. The row and the cell components each have a computed property that "proxies" the prop to the template - thus also caching it.
On first render you see the ifFunction() four times (this is the number of cells you have based on the data property in Vue instance), but if you change the value with the input field, you only see it once (for every update; you may have to click "Full page" to be able to update the value).
Vue.component('cCell', {
props: {
celldata: {
type: String,
required: true
},
isInput: {
type: Boolean,
required: true
},
coords: {
type: Array,
required: true
}
},
data() {
return {
normalCellData: ''
}
},
watch: {
normalCellData: {
handler: function(value) {
this.$emit('cellinput', {
coords: this.coords,
value
})
},
immediate: false
}
},
template: `<td v-if="ifFunction()"><span v-if="!isInput">{{normalCellData}}</span> <input v-else type="text" v-model="normalCellData" /></td>`,
methods: {
ifFunction() {
console.log('ifFunction');
return true;
},
},
mounted() {
this.normalCellData = this.celldata
}
})
Vue.component('cRow', {
props: {
rowdata: {
type: Array,
required: true
},
rownum: {
type: Number,
required: true
}
},
template: `
<tr>
<td
is="c-cell"
v-for="(item, i) in rowdata"
:celldata="item"
:is-input="!!(i % 2)"
:coords="[i, rownum]"
#cellinput="reemit"
></td>
</tr>`,
methods: {
reemit(data) {
this.$emit('cellinput', data)
}
}
})
new Vue({
el: "#app",
data: {
locations: [
["1", "John"],
["2", "Jake"]
], // TODO : locations is not generic enough.
},
methods: {
updateLocations({
coords,
value
}) {
// creating a copy of the locations data attribute
const loc = JSON.parse(JSON.stringify(this.locations))
loc[coords[1]][coords[0]] = value
// changing the whole locations data attribute to preserve
// reactivity
this.locations = loc
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<table border="1" cellpadding="10">
<tbody>
<tr v-for="(row, i) in locations" is="c-row" :rowdata="row" :rownum="i" #cellinput="updateLocations"></tr>
</tbody>
</table>
<!-- <input v-model="locations[0][1]">
<input v-model="locations[1][1]">-->
{{locations}}
</div>

How to solve v-switch vuetify only binds one way?

My v-switch from vuetify is only binding one way.
If i load in my data it switches on or off. so its working if i load data in the v-model of the v-switch.
But if i switch the v-switch, it switches off, but does not change anything.
here is the code:
<v-data-table :headers="datatable.headers" :items="datatable.items" class="elevation-1">
<template v-slot:body="{ items }">
<tr v-for="(item, index) in items" :key="index">
<td>{{item.name}}</td>
<td #click="() => { $router.push(`/settings/${item.name.toLowerCase()}`) }"><v-icon small>edit</v-icon></td>
<td><v-switch v-model="inMenu[item.name.toLowerCase()]" :label="`Switch 1: ${inMenu[item.name.toLowerCase()]}`"></v-switch></td>
</tr>
</template>
</v-data-table>
<script>
export default {
data() {
return {
tabs: [
'Content types'
],
tab: null,
datatable: {
items: [],
headers: [{
text: 'Content types', value: "name"
}]
},
settings: null,
inMenu: {},
}
},
mounted() {
this.$axios.get('/settings.json').then(({data}) => {
this.settings = data
});
this.$axios.get('/tables.json').then(({data}) => {
// set all content_types
data.map(item => {
this.datatable.items.push({
name: item
})
})
// check foreach table if it's in the menu
this.datatable.items.forEach(item => {
this.inMenu[item.name.toLowerCase()] = JSON.parse(this.settings.menu.filter(menuitem => menuitem.key == item.name.toLowerCase())[0].value)
})
})
},
updated() {
console.log(this.inMenu)
}
}
</script>
so i clicked on the first switch and it does not change the state
i tried to have a normal prop in the data function.
i made a switch: null prop and it will react fine to that, but not to my code.
Any idea?
My guess is that your data is not reactive when you write:
// check foreach table if it's in the menu
this.datatable.items.forEach(item => {
this.inMenu[item.name.toLowerCase()] = JSON.parse(this.settings.menu.filter(menuitem => menuitem.key == item.name.toLowerCase())[0].value)
})
You should use the $set method instead and write:
// check foreach table if it's in the menu
this.datatable.items.forEach(item => {
this.$set(this.inMenu, item.name.toLowerCase(), JSON.parse(this.settings.menu.filter(menuitem => menuitem.key == item.name.toLowerCase())[0].value)
}))
See https://v2.vuejs.org/v2/guide/reactivity.html for more information on reactivity
Does this solve your problem?