I have a codepen at https://codepen.io/james-hudson3010/pen/QWgarbr which demonstrates the problem.
I am trying to create and use a custom checkbox component based on using v-btn and v-icon.
When I click on the checkbox, I get the input event and the data changes, but the look of the checkbox does not change. I am certain it is because the method I call to obtain the value is not "reactive". Therefore, upon a value change, there is not a rerender.
I need to call a method to obtain the value because obtaining the value can be complex and based upon which box was clicked. There is an unknown number of boxes.
However, there is some fundamental concept I a missing here, but I am not sure what it is.
What do I need to change in my code for this to work?
(no, I do not want to use v-checkbox...in part, this is for my own edification for how to create such things.)
Javascript
Vue.component('mybox', {
props: ['value'],
data() {
return {
selected: null,
}
},
template: `
<v-btn v-if="value" #click="clicked()" icon dense>
<v-icon color="#f9a602">check_box</v-icon>
</v-btn>
<v-btn v-else #click="clicked()" icon dense>
<v-icon color="#f9a602">check_box_outline_blank</v-icon>
</v-btn>
`,
methods: {
clicked() {
console.log( "clicked", this.selected, this.value );
this.selected = !this.value;
console.log( "clicked", this.selected, this.value );
this.$emit( "input", this.selected );
}
},
watch: {
selected() {
console.log("selected:", this.selected);
// this.$emit("input", this.selected);
},
}
});
new Vue({
el: '#app',
vuetify: new Vuetify(),
data: {
boxvalues: [true, false],
},
methods: {
boxValueChanged( index ) {
console.log( "boxValueChanged", index, this.boxvalues[ index ] );
this.boxvalues[ index ] = !this.boxvalues[ index ];
console.log( "boxValueChanged", index, this.boxvalues[ index ] );
},
boxValue( index ) {
return this.boxvalues[ index ];
}
},
})
HTML
<div id="app">
<mybox v-for="(value, index ) in boxvalues"
:value="boxValue( index )"
#input="boxValueChanged( index )"
:key="index"
:id="index"/>
</div>
You're right on track, the culprit is the loss of reactivity in this line:
this.boxvalues[index] = !this.boxvalues[ index ];
From the docs:
Vue cannot detect the following changes to an array:
When you directly set an item with the index, e.g.
vm.items[indexOfItem] = newValue
When you modify the length of the
array, e.g. vm.items.length = newLength
There is a simple solution provided by Vue for this use cases: Vue.set() (or this.$set()) method:
this.$set(this.boxvalues, index, !this.boxvalues[index]);
You can read more on this subject here
To add to what Igor said, you can also rewrite the whole array to trigger the reactivity.
ex:
boxValueChanged( index ) {
const boxvalues = [...this.boxvalues]
boxvalues[index] = !boxvalues[index]
this.boxvalues = boxvalues
},
Related
I am trying to display several cards based on Firestore docs, I allow the user to select the card and take some actions with that data. But when a firestore document's data changes, and you already have selected a card, it will rearrange the cards and the 'selected card' will now be different.
1.) select card
2.) firestore data changes(updated live)
3.) the card you selected is now replaced with a new card (new index)
If you change the middle doc's rank to -6, the top card will move down, and be the new selected card
online: [{rank: 1, id: 3030303}, {rank: 2, id: 2020202}, {rank: 3, id: 1010101}]
I looked at a this stackoverflow question but the answer is not working/outdated (as well as others) Manipulating v-list-item-group model with object instead of list index
<template>
<div>
<v-container fluid>
<v-list>
<v-list-item-group v-model="listSelection">
<v-list-item v-for="(item, index) in online" :key="index" :value="item.id">
{{item}}
</v-list-item>
</v-list-item-group>
</v-list>
</v-container>
</div>
</template>
<script>
import db from "#/components/fbInit";
export default {
props: {
value: {}
},
data() {
return {
online: [],
selected: []
}
},
firestore: {
online: db.collection('item').orderBy('rank', 'asc')
},
methods: {
changeSelected(index) {
this.selected = index
this.$emit('change', this.selected);
},
},
computed: {
items: function() {
return this.online
},
listSelection: {
// get: function() { // commented out b/c this line breaks the page
// return this.value.id
// },
set: function(newVal) {
this.$emit('input', this.items.find(item => item.id === newVal));
}
}
},
}
</script>
I would like the selection to be bound to the firestore doc.id (i.e. unchanging doc name versus the always changing doc data)
Any help is greatly appreciated
Every input in search i update the items prop but the v-autocomplete become empty
although the data in my component changed
i tried to add the no-filter prop it didnt help i guess something with the reactivity destroyed
i allso tried with computed property as an items but still same result
Every input in search i update the items prop but the v-autocomplete become empty
although the data in my component changed
i tried to add the no-filter prop it didnt help i guess something with the reactivity destroyed
i allso tried with computed property as an items but still same result
<script>
import ProductCartCard from "~/components/cart/ProductCartCard";
export default {
name: "search-app",
components: {
ProductCartCard
},
props: {
items: {
type: Array,
default: () => []
}
},
data() {
return {
loading: false,
filteredItems: [],
search: null,
select: null
};
},
watch: {
search(val) {
if (!val || val.length == 0) {
this.filteredItems.splice(0, this.filteredItems.length);
return;
} else {
val !== this.select && this.querySelections(val);
}
}
},
methods: {
querySelections(v) {
this.loading = true;
// Simulated ajax query
setTimeout(() => {
this.filteredItems.splice(
0,
this.filteredItems.length,
...this.items.filter(i => {
return (i.externalName || "").toLowerCase().includes((v || "").toLowerCase());
})
);
this.loading = false;
}, 500);
}
}
};
</script>
<template>
<div class="search-app-container">
<v-autocomplete
v-model="select"
:loading="loading"
:items="filteredItems"
:search-input.sync="search"
cache-items
flat
hide-no-data
hide-details
label="searchProduct"
prepend-icon="mdi-database-search"
solo-inverted
>
<template v-slot:item="data">
<ProductCartCard :regularProduct="data" />
</template>
</v-autocomplete>
</div>
</template>
One of the caveat of the v-autocomplete as described in the documentation:
When using objects for the items prop, you must associate item-text and item-value with existing properties on your objects. These values are defaulted to text and value and can be changed.
That may fix your issue
I have a frozen list of non-frozen data, the intent being that the container is not reactive but the elements are, so that an update to one of the N things does not trigger dependency checks against the N things.
I have a computed property that returns a sorted version of this list. But Vue sees the reactive objects contained within the frozen list, and any change to an element results in triggering the sorted computed prop. (The goal is to only trigger it when some data about the sort changes, like direction, or major index, etc.)
The general concept is:
{
template: someTemplate,
data() {
return {
list: Object.freeze([
Vue.observable(foo),
Vue.observable(bar),
Vue.observable(baz),
Vue.observable(qux)
])
}
},
computed: {
sorted() {
return [...this.list].sort(someOrdering);
}
}
}
Is there a Vue idiom for this, or something I'm missing?
...any change to an element results in triggering the sorted computed prop
I have to disagree with that general statement. Look at the example below. If the list is sorted by name, clicking "Change age" does not trigger recompute and vice versa. So recompute is triggered only if property used during previous sort is changed
const app = new Vue({
el: "#app",
data() {
return {
list: Object.freeze([
Vue.observable({ name: "Foo", age: 22}),
Vue.observable({ name: "Bar", age: 26}),
Vue.observable({ name: "Baz", age: 32}),
Vue.observable({ name: "Qux", age: 52})
]),
sortBy: 'name',
counter: 0
}
},
computed: {
sorted() {
console.log(`Sort executed ${this.counter++} times`)
return [...this.list].sort((a,b) => {
return a[this.sortBy] < b[this.sortBy] ? -1 : (a[this.sortBy] > b[this.sortBy] ? 1 : 0)
});
}
}
})
Vue.config.productionTip = false;
Vue.config.devtools = false;
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.min.js"></script>
<div id="app">
<div>Sorted by: {{ sortBy }}</div>
<hr>
<button #click="list[0].name += 'o' ">Change name</button>
<button #click="list[0].age += 1 ">Change age</button>
<hr>
<button #click="sortBy = 'name'">Sort by name</button>
<button #click="sortBy = 'age'">Sort by age</button>
<hr>
<div v-for="item in sorted" :key="item.name">{{ item.name }} ({{item.age}})</div>
</div>
Why name 2 not changed and not reactive? What wrong?
How can I make it reactive so that when the properties of the object change, the DOM also changes?
When I delete Name 2 nothing happens
<template>
<div>
<div v-for="(item, index) in items" v-bind:key="item.id">
<p>{{item.name}}</p>
</div>
<button v-on:click="deleteItem">
Delete Name 2
</button>
</div>
</template>
<script>
export default {
data:function(){
return {
items:[
{
id: 1,
name: "Name 1"
},
{
id: 2,
name: "Name 2"
}
]
}
},
methods: {
deleteItem: function(){
this.items[1] = [];
console.log(this.items);
alert("DELETED");
}
},
created: function(){
let self = this;
setTimeout(function(){
self.items[1] = [];
}, 2000);
}
};
</script>
the reactivity in vue (2) is a little bit tricky, this link explain you how to solve this issue
https://v2.vuejs.org/v2/guide/reactivity.html#For-Arrays
Modify your delete item function. Don't set it to an empty array. Filter the array like this:
Your HTML Markup :
<button #click="deleteItem(2)">
Delete Name 2
</button>
Send the id of the item that you want to delete to the deleteItem() as an argument.
deleteItem: function(itemId){
let filtered = this.items.filter((each) => {
return each.id !== itemId;
})
this.items = filtered; //Finally mutate the items[] in data
}
You are actually assigning an empty array to item with index 1, instead of removing it.
If you want to remove the element with index 1 simply use splice() and Vue will automatically react to it:
this.items.splice(1, 1); // The first 1 is the index
Or, alternatively use Vue.delete(), which is originally to remove properties from object, but can also remove items from arrays:
Vue.delete(this.items, 1); // 1 is the index
More info: https://v2.vuejs.org/v2/api/#Vue-delete
I've created basic jsfiddle here.
var child = Vue.component('my-child', {
template:
'<div> '+
'<div>message: <input v-model="mytodoText"></div> <div>todo text: {{todoText}}</div>'+
'<button #click="remove">remove</button>' +
'</div>',
props:['todoText'],
data: function(){
return {
mytodoText: this.todoText
}
},
methods: {
remove: function(){
this.$emit('completed', this.mytodoText);
}
}
});
var root = Vue.component('my-component', {
template: '<div><my-child v-for="(todo, index) in mytodos" v-bind:index="index" v-bind:todoText="todo" v-on:completed="completed"></my-child></div>',
props:['todos'],
data: function(){
return {
mytodos: this.todos
}
},
methods: {
completed: function(todo){
console.log(todo);
var index = this.mytodos.indexOf(todo, 0);
if (index > -1) {
this.mytodos.splice(index, 1);
}
}
}
});
new Vue({
el: "#app",
render: function (h) { return h(root, {
props: {todos: ['text 1', 'text 2', 'text 3']}
});
}
});
Code is simple: root component receives array of todos (strings), iterates them using child component and pass some values through the props
Click on the top "remove" button and you will see the result - I'm expecting to have
message: text 2 todo text: text 2
but instead having:
message: text 1 todo text: text 2
From my understanding vue should remove the first element and leave the rest. But actually I have some kind of "mix".
Can you please explain why does it happen and how it works?
This is due to the fact that Vue try to "reuse" DOM elements in order to minimize DOM mutations. See: https://v2.vuejs.org/v2/guide/list.html#key
You need to assign a unique key to each child component:
v-bind:key="Math.random()"
where in real-world the key would be for example an ID extracted from a database.