Why does the change in this property not trigger a watch in Vuejs? - vue.js

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.

Related

Vuejs - v-bind.sync on resursive components (hierarchical list)

I have a hierarchical list component where child items have checkboxes. Checkbox actions(check/uncheck) must keep the parent component in sync with the checkbox's changed state. I cannot figure out how to achieve this using v-bind.sync recursively. My code is as below:
Menu.vue
This component holds the hierarchical list. (Only relevant code included)
HierarchicalCheckboxList is the component that displays the hierarchical list
Property 'value' holds the check/uncheck value (true/false)
Property 'children' contains the child list items
How do I define the .sync attribute on HierarchicalCheckboxList and with what parameter?
<template>
<div>
<HierarchicalCheckboxList
v-for="link in links"
#checked="primaryCheckChanged"
:key="link.id"
v-bind="link">
</HierarchicalCheckboxList>
</div>
</template>
<script>
import HierarchicalCheckboxList from 'components/HierarchicalCheckboxList'
data () {
return {
links: [{
id: 1,
title: 'Home',
caption: 'Feeds, Dashboard & more',
icon: 'account_box',
level: 0,
children: [{
id: 2,
title: 'Feeds',
icon: 'feeds',value: true,
level: 1,
children: [{
id: '3',
title: 'Dashboard',
icon: 'settings',
value: true,
level: 1
}]
}]
}]
}
},
methods: {
primaryCheckChanged (d) {
// A child's checked state is propogated till here
console.log(d)
}
}
</script>
HierarchicalCheckboxList.vue
This component calls itself recursively:
<template>
<div>
<div v-if="children != undefined && children.length == 0">
<!--/admin/user/user-->
<q-item clickable v-ripple :inset-level="level" :to="goto">
<q-item-section>
{{title}}
</q-item-section>
</q-item>
</div>
<div v-else>
<div v-if="children != undefined && children.length > 0">
<!-- {{children}} -->
<q-expansion-item
expand-separator
:icon="icon"
:label="title"
:caption="caption"
:header-inset-level="level"
default-closed>
<template v-slot:header>
<q-item-section>
{{ title }}
</q-item-section>
<q-item-section side>
<div class="row items-center">
<q-btn icon="add" dense flat color="secondary"></q-btn>
</div>
</q-item-section>
</template>
<HierarchicalCheckboxList
v-for="child in children"
:key="child.id"
#checked="primaryCheckChanged"
v-bind="child">
</HierarchicalCheckboxList>
</q-expansion-item>
</div>
<!-- to="/admin/user/user" -->
<div v-else>
<q-item clickable v-ripple :inset-level="level">
<q-item-section>
<q-checkbox :label="title" v-model="selection" />
</q-item-section>
</q-item>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'HierarchicalCheckboxList',
props: {
id: { type: String, required: true },
title: { type: String, required: false },
caption: { type: String, default: '' },
icon: { type: String, default: '' },
value: { type: Boolean, default: false },
level: { type: Number, default: 0 },
children: { type: Array }
},
data () {
return {
localValue: this.$props.value
}
},
computed: {
selection: {
get: function () {
return this.localValue
},
set: function (newvalue) {
this.localValue = newvalue
this.$emit('checked', this.localValue)
// or this.$emit('checked', {id: this.$props.id, value: this.localValue })
}
}
},
methods: {
primaryCheckChanged (d) {
this.$emit('checked', d)
}
}
}
</script>
What works so far
As a work-around I am able to get the checkbox state emitted with $emit('checked'), which I use to send it to the next process. But the parent's state is not updated until I refresh it back from the database.
How do I update the parent component's state using v-bind.sync recursively?
Appreciate any help!!
UI
Figured out how to do it after I broke the code down from the whole 2000 line code to a separate 'trial-n-error' code of 20 lines and then things became simple and clear.
Menu.vue
A few changes in the parent component in the HierarchicalCheckboxList declaration:
Note the sync property
<HierarchicalCheckboxList
v-for="child in children"
:key="child.id"
:u.sync="link.value"
v-bind="child">
</HierarchicalCheckboxList>
HierarchicalCheckboxList.vue
Change the same line of code in the child component (as its recursive)
<HierarchicalCheckboxList
v-for="child in children"
:key="child.id"
:u.sync="child.value"
v-bind="child">
</HierarchicalCheckboxList>
And in the computed set property, emit as below:
this.$emit('update:u', this.localValue)
That's it - parent n children components now stay in snyc.

Vue.js: Including same instance of component multiple times in page

What I am trying to accomplish:
I have some filters that display on a page to filter the products that display on the page. In mobile, I want to hide these filters behind a button that, once pressed, will show the filters in a slide out menu from the side.
While I can duplicate the same components on the page twice, the components are not the exact same instance, that is, clicking on a filter will trigger that function to filter the products on the page, but it sets its own data attributes, which I am using to say "if data attribute 'selected' is true, add a 'selected' class to the component. When I resize the window, the other instance of the component does not have the 'selected' data attribute marked as 'true'.
I expect this, because, from the docs:
Notice that when clicking on the buttons, each one maintains its own, separate count. That’s because each time you use a component, a new instance of it is created.
...but what would be the best way to do this?
I played around with the idea of just setting a class 'mobile' on the component, and the .mobile css would style the components differently, but I need for it to break out where it is nested.
e.g.
<body>
<header>
<!-- desktop -->
<guitar-filters>
<header>
<!-- mobile -->
<guitar-filters>
</body
Here is my Vue component 'guitar-filters' that displays several components called 'instrument-filter':
Vue.component('guitar-filters', {
data: function() {
return {
isMobile: false
}
},
mounted: function() {
var comp = this;
this.setIsMobile();
window.addEventListener('resize', function() {
comp.setIsMobile();
});
},
methods: {
setIsMobile: function() {
this.isMobile = (window.innerWidth <= 900) ? true : false;
}
},
template: `
<ul class="filters" :class="{mobile: isMobile}">
<li>
All
</il>
<li>
Series
<ul>
<instrument-filter filter-by="series" filter="All">All</instrument-filter>
<instrument-filter filter-by="series" filter="Frontier">Frontier</instrument-filter>
<instrument-filter filter-by="series" filter="Legacy">Legacy</instrument-filter>
<instrument-filter filter-by="series" filter="USA">USA</instrument-filter>
</ul>
</li>
<li>
Body Shape
<ul>
<instrument-filter filter-by="bodyType" filter="All">All</instrument-filter>
<instrument-filter filter-by="bodyType" filter="Concert">Concert</instrument-filter>
<instrument-filter filter-by="bodyType" filter="Concertina">Concertina</instrument-filter>
<instrument-filter filter-by="bodyType" filter="Concerto">Concerto</instrument-filter>
<instrument-filter filter-by="bodyType" filter="Orchestra">Orchestra</instrument-filter>
</ul>
</li>
</ul>
`
});
The instrument-filter component:
Vue.component('instrument-filter', {
data: function() {
return {
selected: false
}
},
props : [
'filterBy',
'filter'
],
methods: {
addFilter: function() {
this.$root.$emit('addFilter',{filterBy: this.filterBy,filter: this.filter});
},
clearFilter: function() {
this.$root.$emit('clearFilter',{filterBy: this.filterBy,filter: this.filter});
}
},
template: `
<li :class="{ 'selected' : selected }" #click="selected = !selected; selected ? addFilter() : clearFilter()"><slot></slot></li>
`
});
.css:
ul.filters > li > ul > li.selected::before {
content: "✔️";
...
}
The goal is to have a filter have the 'selected' class in both instances. If I click on 'concert' body shape, and then resize the window to mobile breakpoint, the other instance of that filter component will be selected also.
EDIT: I could hack this. I could move one instance of the component with javascript, but I'm learning Vue, and want to do this the Vue way and best practices.
There's a number of different ways you can handle this. It looks like you've started down the event bus path. Another option could be to use shared app state (see Vuex).
What I've done is similar to shared state, but just using app (same would apply to a common parent component) data. The shared object is passed to both instances of the component. If an item is selected, the appropriate entry is toggled. Since the object is shared, both components stay in sync.
If there was no common parent component, you'd have to look at events or state.
Take a look and see if that helps.
Vue.component('guitar-filters', {
props: [ 'data' ],
data: function() {
return {
isMobile: false
}
},
mounted: function() {
var comp = this;
this.setIsMobile();
window.addEventListener('resize', function() {
comp.setIsMobile();
});
},
methods: {
setIsMobile: function() {
this.isMobile = (window.innerWidth <= 900) ? true : false;
}
},
template: `
<ul class="filters" :class="{mobile: isMobile}">
<li>
All
</il>
<li>
Series
<instrument-filters :list="data.seriesFilters"/>
</li>
<li>
Body Shape
<instrument-filters :list="data.bodyFilters"/>
</li>
</ul>
`
});
Vue.component('instrument-filters', {
props : [ 'list', ],
methods: {
toggle(toggleItem) {
let itemInList = this.list.find((item) => item.value === toggleItem.value);
itemInList.selected = !itemInList.selected;
},
},
template: `
<ul>
<li v-for="item in list" :class="{ 'selected' : item.selected }" #click="toggle(item)">{{ item.label }}</li>
</ul>
`
});
new Vue({
el: "#app",
data: {
filterData: {
seriesFilters: [
{ label: 'All', value: 'All', selected: false },
{ label: 'Frontier', value: 'Frontier', selected: false },
{ label: 'Legacy', value: 'Legacy', selected: false },
{ label: 'USA', value: 'USA', selected: false },
],
bodyFilters: [
{ label: 'All', value: 'All', selected: false },
{ label: 'Concert', value: 'Concert', selected: false },
{ label: 'Concertina', value: 'Concertina', selected: false },
{ label: 'Concerto', value: 'Concerto', selected: false },
{ label: 'Orchestra', value: 'Orchestra', selected: false },
],
}
},
});
ul {
margin-left:20px;
}
ul > li {
cursor: pointer;
}
ul.filters > li > ul > li.selected::before {
content: "✔️";
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<header>
<!-- desktop -->
<guitar-filters :data="filterData" />
</header>
<!-- mobile -->
<guitar-filters :data="filterData" />
</div>
Fiddle: https://jsfiddle.net/nigelw/jpasfkxb

How to $watch property of a list item in VueJS

How can I $watch changes to specific properties of a list item? For instance in the below code, I want to know whenever the Done property on any of the TODO list items changes.
I see from the docs that I can watch subproperties of objects, like myObjects.done in the code below, but I am not sure about the syntax for lists.
I should also mention I would prefer to $watch the data instead of putting event handlers in the UI, and function calls in any spot that changes the property
var vm = new Vue({
el: "#app",
data: {
myObject: { done: true },
todos: [
{ text: "Learn JavaScript", done: false },
{ text: "Learn Vue", done: false },
{ text: "Play around in JSFiddle", done: true },
{ text: "Build something awesome", done: true }
]
},
});
//This works wonderfully on non list items
vm.$watch("myObject.done", function(val)
{
console.log("myObject.done changed", val);
});
//How do I monitor changes to the done property of any of the todo items?
vm.$watch("todos[*].done", function(val)
{
console.log("todos.done changed", val);
})
JSFiddle here: http://jsfiddle.net/eywraw8t/376544/
With your current approach, you'd have to deep-watch the array and do some heavy computations in order to figure out the changed element. Check this link for the example:
Vue - Deep watching an array of objects and calculating the change?
I think the better approach would be using change event handler:
<input type="checkbox" v-model="todo.done" #change="onTodoChange(todo, $event)">
JSFiddle: http://jsfiddle.net/47s0obuc/
To watch specific property, I'd create another component for the list item and pass the item as value to watch the changes from that component.
Vue.component("TaskItem", {
template: `
<li
class="task-item"
:class="{ done: complete }"
>
<p>{{ task.description }}</p>
<input type="checkbox" v-model="complete">
</li>
`,
props: ["task"],
computed: {
complete: {
set(done) {
this.$emit("complete", this.task, done);
// we force update to keep checkbox state synced
// in case if task.done was not toggled by parent component
this.$forceUpdate();
},
get() {
return this.task.done;
}
}
}
});
new Vue({
el: "#app",
template: `
<div>
<ul class="task-list">
<TaskItem
v-for="(task, i) in tasks"
:key="i"
:task="task"
#complete="complete"
/>
</ul>
<button #click="completeFirstTask">Complete first task</button>
</div>
`,
data() {
return {
tasks: [
{ description: "Get milk", done: false },
{ description: "Barber shop", done: true },
{ description: "Fix sleep cycle", done: false }
]
};
},
methods: {
complete(item, done) {
item.done = done;
},
completeFirstTask() {
this.tasks[0].done = true;
}
}
});
https://codesandbox.io/s/wqrp13vp25
I used this and it works for me.
var vm = new Vue({
el: "#app",
data: {
myObject: { done: true },
todos: [
{ text: "Learn JavaScript", done: false },
{ text: "Learn Vue", done: false },
{ text: "Play around in JSFiddle", done: true },
{ text: "Build something awesome", done: true }
]
},
watch:{
todo: function(val) {
console.log ("This TODO is Done", val)
}
});
<template>
<div class="mainDiv" v-for="(index, todo) from todos">
<div>{{todo.text}}</div>
<input type="checkbox" v-model="todo[index].done">
</div>
</template>

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.

Vue.js reactivity inner mechanisms

Given the following Vue code, how does it know to render the v-for again when selected is changed?
It makes complete sense to me when todos is changed.
So, Vue notices that there's a method isSelected involved and then uses "reflection" to watch the selected value, since it's an instance value?
Is that what's happening under the hood?
<div id="app">
<ol>
<li v-for="todo in todos" :class="{ 'selected': isSelected(todo.text) }">
{{ todo.text }}
</li>
</ol>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
todos: [
{ text: 'foo' },
{ text: 'bar' },
{ text: 'quz' }
],
'selected': 'bar'
},
methods: {
isSelected: function(text) {
return text != this.selected;
}
}
})
app.todos.push({ text: 'test' });
app.todos[0].text = 'change';
app.selected = 'foo';
</script>