How to watch when object has a new property - vue.js

Simple question : is it possible to watch when an object as a new property ?
In the JSFiddle I only initialize the property "show" of the first item in the list".
When I click on "Toggle" for first item it works.
But for the others items (that get "show" property dynamically) and for myObject (that get "test2" property dynamically) it is refresh only when you toggle the first item of the list !
In my case I want that the toggle works immediately.
Someone has an explanation ?
new Vue({
el: "#app",
data: function() {
return {
myObject: { test: 'test' },
list: [{
value: 1,
show: true
}, {
value: 2
}, {
value: 3
}, {
value: 4
}, {
value: 5
}]
}
},
methods: {
toggleItem: function( item ) {
if( !item.show ) item.show = true;
else item.show = !item.show;
console.log(item.show)
this.myObject.test2 = 'test2';
}
},
watch: {
list: function( newValue ) {
console.log('NEW VALUE: ', newValue );
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.2.5/vue.js"></script>
<div id="app">
{{ myObject }}
<div v-for="item in list">
<span v-if="item.show">
{{ item.value }}
</span>
<button #click="toggleItem(item)" type="button">Toggle !</button>
</div>
</div>

To help Vue pick up new properties, you have to use Vue.set() / this.$set to add them. Otherwise, Vue can't detect the addition and the new property will not be reactive.
if( !item.show ) this.$set(item, 'show', true)
https://v2.vuejs.org/v2/api/#Vue-set
Generally, We strongly advise to set all properties beforehand in data()

Related

Vue sorting a frozen list of non-frozen data

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>

Marking a Vue Todo List Item as done

This may have been answered before, but I have been unable to find an answer that works in this specific situation.
I'm new to Vue and trying to build a Todo list in which I can click on a list item when it is complete, changing or adding a class that will change the style of the item.
I guess I don't fully understand how the scopes work together between the main Vue and a component. The code I have right now does absolutely nothing. I have tried moving methods between the main and component, but it always gives me some error.
I guess I'm just looking for some guidance as to how this should be done.
Vue.component('todo-item', {
props: ['todo'],
template: '<li>{{ todo.id + 1 }}. {{ todo.text }}</li>'
})
var app = new Vue({
el: '#app',
data: {
isDone: false,
todos: [
{ id: 0, text: 'This is an item to do today' },
{ id: 1, text: 'This is an item to do today' },
{ id: 2, text: 'This is an item to do today' },
{ id: 3, text: 'This is an item to do today' },
{ id: 4, text: 'This is an item to do today' },
{ id: 5, text: 'This is an item to do today' }
]
},
methods: {
markDone: function(todo) {
console.log(todo)
this.isDone = true
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<div class="content">
<ul class="flex">
<todo-item
v-for="todo in todos"
:todo="todo"
:key="todo.id"
#click="markDone"
:class="{'done': isDone}"
></todo-item>
</ul>
</div>
</div>
Thanks for the help, guys.
You were getting so close! You simply had your :class="{'done': isDone}" #click="markDone" in the wrong place!
The important thing to remember with components is that each one has to have their own data. In your case, you were binding all todo's to your root Vue instance's done variable. You want to instead bind each one to their own done variable in their own data.
The way you do this is by creating a function version of data that returns individual data for each component. It would look like this:
data () {
return {
isDone: false,
}
},
And then you move the :class="{'done': isDone} from the todo to the li internal to it:
<li :class="{'done': isDone}">{{ todo.id + 1 }}. {{ todo.text }}</li>
Now we have the 'done' class depending on an individual data piece for each individual todo element. All we need to do now is be able to mark it as complete. So we also want each todo component to have it's own method to do so. Add a methods: object to your todo component and move your markDone method there:
methods: {
markDone() {
this.isDone = true;
},
}
Now move the #click="markDone" to the li as well:
<li :class="{'done': isDone}" #click="markDone">{{ todo.id + 1 }}. {{ todo.text }}</li>
And there you go! Now you should be able to create as many todo's as you want, and mark them all complete!
Bonus:
Consider changing your function to toggleDone() { this.isDone = !this.isDone; }, that way you can toggle them back and forth between done and not done!
Full code below :)
Vue.component('todo-item', {
props: ['todo'],
template: `<li :class="{'done': isDone}" #click="toggleDone">{{ todo.id + 1 }}. {{ todo.text }}</li>`,
data () {
return {
isDone: false,
}
},
methods: {
toggleDone() {
this.isDone = !this.isDone;
},
}
})
var app = new Vue({
el: '#app',
data: {
isDone: false,
todos: [
{ id: 0, text: 'This is an item to do today' },
{ id: 1, text: 'This is an item to do today' },
{ id: 2, text: 'This is an item to do today' },
{ id: 3, text: 'This is an item to do today' },
{ id: 4, text: 'This is an item to do today' },
{ id: 5, text: 'This is an item to do today' }
]
},
methods: {
markDone: function(todo) {
console.log(todo)
this.isDone = true
}
}
})
.done {
text-decoration: line-through;
}
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<div class="content">
<ul class="flex">
<todo-item
v-for="todo in todos"
:todo="todo"
:key="todo.id"
></todo-item>
</ul>
</div>
</div>
First of all, your current implementation will affect all items in the list when you mark one item as done because you are associating a single isDone property to all the items and when that property becomes true, it will be applied to all the items in your list.
So to fix that, you need to find a way to associate done to each item. And because your item is an object, you just assign a new property done dynamically and set the value to true which means it is marked as done. It will be very confusing to just explain it, so I included a full example using your existing code.
See this JS Fiddle: https://jsfiddle.net/eywraw8t/205021/
In your code, which are handling with the click event is the <li> element, but your are trying to handle it in the root of your component, there're a few ways to solve this
Use native modifier
<todo-item
v-for="todo in todos"
:todo="todo"
:key="todo.id"
#click.native="markDone"
:class="{'done': isDone}"
>
</todo-item>
You can find more info here
https://v2.vuejs.org/v2/guide/migration.html#Listening-for-Native-Events-on-Components-with-v-on-changed
Emit from component the click event
Vue.component('todo-item', {
props: ['todo'],
template: '<li #click="click()">{{ todo.id + 1 }}. {{ todo.text }}</li>',
methods: {
click () {
this.$emit('click')
}
}
})
By the way, in your current code once you click in one todo all the todos will be "marked as done" as you are using just one variable for all of them.

Why does the change in this property not trigger a watch in Vuejs?

I need to figure out why a property that is changed is not triggering a watch in Vue.
I have 2 components Parent and Child.
Child has as 1 Prop (item) and item has 4 properties: id, text, isImportant, isCool.
The Parent has 2 lists that are populated using two computed properties which return arrays, one where the items's "isImportant" == TRUE and the other where "isImportant" is FALSE.
In the Child, isImportant and isCool are both bound to input[type=checkbox] elements.
I have a watch (deep) set to respond to changes in the item prop of the Child.
Changing "isCool" triggers the watch while "isImportant" does not.
Changing isImportant does update the collection and the property is updated but it does not trigger the "watch".
It seems to be related to the computed property but not sure why?
https://jsfiddle.net/dclaysmith/y54b0mrq/
Vue.component('todo', {
props: {
item: Object
},
template: `
<label>
{{ item.text }}
<input type="checkbox"
v-model="item.isImportant">
Is Important?
<input type="checkbox"
v-model="item.isTicked">
Is Cool?
</label>`,
watch: {
item: {
handler: function (a, b) {
alert('Changed!')
},
deep: true
},
'item.isImportant': function (a, b) {
alert('Changed!')
}
},
})
new Vue({
el: '#app',
template: `
<div id="app">
<h2>Important:</h2>
<ol>
<li v-for="item in important">
<todo :item="item" :key="item.id"></todo>
</li>
</ol>
<br>
<h2>Not Important:</h2>
<ol>
<li v-for="item in notImportant">
<todo :item="item" :key="item.id"></todo>
</li>
</ol>
</div>
`,
data: {
todos: [
{ id: 1, text: "Learn JavaScript", isImportant: false, isTicked: false },
{ id: 2, text: "Learn Vue", isImportant: true, isTicked: false },
{ id: 3, text: "Play around in JSFiddle", isImportant: true, isTicked: false },
{ id: 4, text: "Build something awesome", isImportant: true, isTicked: false }
]
},
computed: {
important: function () {
return this.todos.filter(function(todo) {
return (todo.isImportant == true);
});
},
notImportant: function () {
return this.todos.filter(function(todo) {
return (todo.isImportant != true);
});
}
}
})
The reason that the change to isImportant isn't caught by the component, is that when you change isImportant, the component is removed, because the todo-item is moved from one list to the other.
If you have just one list of all todo's (<li v-for="item in todos">), both listeners trigger just fine.

Vue.js - Watcher for property with variable keypath

Is it possible, in Vue v2, to define a watcher using a keypath containing a variable?
For example, depending on the currentKey, I want to watch either the changes in obj.A or obj.B:
data() {
return {
currentKey: 'A',
obj: { A: { 'a': '' }, B: { 'b' :'' },
}
},
watch: {
'obj[currentKey]'(newItem, oldItem) {}
}
You can make a computed property which returns this.obj[this.currentKey] and then set a watcher on that.
But, if you want to watch changes to the properties of the dynamic object, you'll also need to set the deep property of the watcher to true.
Here's a simple example:
new Vue({
el: '#app',
data () {
return {
currentKey: 'A',
obj: {A: {value: ''}, B: {value:''} },
}
},
computed: {
selected() {
return this.obj[this.currentKey];
}
},
watch: {
selected: {
deep: true,
handler(object) {
console.log('selected object value', object.value);
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.9/vue.min.js"></script>
<div id="app">
Selected object: {{ selected }}
<select v-model="currentKey">
<option v-for="i in ['A', 'B']" :key="i" :value="i">{{ i }}</option>
</select>
<br><br>
Text for selected object: <input v-model="selected.value">
</div>

How can I make a reusable "CheckAll" checkbox solution in Vue2

I am trying to create a reusable "Check All" solution for displaying a list of objects retrieved from an API.
I really like the get/set methods of computed properties that I use in this example here, https://codepen.io/anon/pen/aLeLOZ but I find that rewriting the same function over and over again and maintaining a seperate checkbox state list is tedious.
index.html
<div id="app">
<input type="checkbox" v-model="selectAll1"> Check All
<div v-for="person in list1">
<input type="checkbox" v-model="checkbox" :value="person.id">
<span>{{ person.name }}</span>
</div>
<hr/>
<input type="checkbox" v-model="selectAll2"> Check All
<div v-for="person in list2">
<input type="checkbox" v-model="checkbox2" :value="person.id">
<span>{{ person.name }}</span>
</div>
</div>
main.js
new Vue({
el: '#app',
data () {
return {
list1: [
{ id: 1, name: 'Jenna1'},
{ id: 2, name: 'Jenna2'},
{ id: 3, name: 'Jenna3'},
{ id: 4, name: 'Jenna4'}
],
list2: [
{ id: 1, name: 'Mary1'},
{ id: 2, name: 'Mary2'},
{ id: 3, name: 'Mary3'},
{ id: 4, name: 'Mary4'}
],
checkbox: [],
checkbox2: []
}
},
computed: {
selectAll1: {
get: function () {
return this.list1 ? this.checkbox.length === this.list1.length : false
},
set: function (value) {
let selected = []
if (value) {
this.list1.forEach(function (bf) {
selected.push(bf.id)
})
}
this.checkbox = selected
}
},
selectAll2: {
get: function () {
return this.list2 ? this.checkbox2.length === this.list2.length : false
},
set: function (value) {
let selected = []
if (value) {
this.list2.forEach(function (bf) {
selected.push(bf.id)
})
}
this.checkbox2 = selected
}
},
}
});
How can I make a resuable selectAll() function that will work in this example that can be included as often as needed?
Is it possible to make a class that can maintain the check box state for each list and still function as a computed property to make use of the v-model directive?
It's not the same at all, but a method based solution would be
methods: {
selectUs: function(){
if (this.checkbox.length <= this.list1.length) {
this.checkbox = Array.from(Array(this.list1.length + 1).keys())
} else {
this.checkbox = []
}
}
}
with #click="selectUs" instead of v-model="selectAll1"
(you could keep the get part of your computed properties to keep track of whether all are selected, and then use if (selectAll1) { etc } in the method)