Vue.js - Watcher for property with variable keypath - vuejs2

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>

Related

Vue2: Why can I not set new value when using computed property?

I have a property from an API (romanizationSystem_FK) that sometimes is a null value. If there is a null value, I want to set the value of that property to -1.
I've tried to do this logic via a computed property, but for some reason if the value is null it still does not get set to -1 by the computed property. Anyone spot any errors ?
Template:
<select
v-model="romanizationSystem_FK"
#change="$emit('variation-updates', formValues)"
>
<option value=""></option>
<option
v-for="option in romanizationSystemOptions"
:key="option.code"
:value="option.code"
>
{{ option.system }}
</option>
</select>
script:
props: {
variation: {
type: Object,
default: () => ({})
},
romanizationSystemOptions: {
type: Array,
default: () => []
},
data() {
return {
formValues: {
...this.variation
}
}
},
computed: {
romanizationSystem_FK: {
get() {
return this.formValues.romanizationSystem_FK
},
set(val) {
if (val === null) {
this.formValues.romanizationSystem_FK = -1
} else {
this.formValues.romanizationSystem_FK = val
}
}
}
You can use Nullish coalescing operator (??). I am just creating a very simple demo to show how it works.
new Vue({
el: '#app',
data: {
formValues: {
romanizationSystem_FK: null
}
},
computed: {
romanizationSystem_FK() {
return this.formValues.romanizationSystem_FK ?? '-1'
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input v-model="romanizationSystem_FK"/>
</div>

Update child component value on axios response using v-model

Vue 3
I am trying to update the value of the data variable from the Axios response. If I print the value in the parent component it's getting printed and updates on the response but the variable's value is not updating in the child component.
What I am able to figure out is my child component is not receiving the updated values. But I don't know why is this happening.
input-field is a global component.
Vue 3
Parent Component
<template>
<input-field title="First Name" :validation="true" v-model="firstName.value" :validationMessage="firstName.validationMessage"></input-field>
</template>
<script>
export default {
data() {
return {
id: 0,
firstName: {
value: '',
validationMessage: '',
},
}
},
created() {
this.id = this.$route.params.id;
this.$http.get('/users/' + this.id).then(response => {
this.firstName.value = response.data.data.firstName;
}).catch(error => {
console.log(error);
});
},
}
</script>
Child Component
<template>
<div class="form-group">
<label :for="identifier">{{ title }}
<span class="text-danger" v-if="validation">*</span>
</label>
<input :id="identifier" :type="type" class="form-control" :class="validationMessageClass" :placeholder="title" v-model="inputValue">
<div class="invalid-feedback" v-if="validationMessage">{{ validationMessage }}</div>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
required: true,
},
validation: {
type: Boolean,
required: false,
default: false,
},
type: {
type: String,
required: false,
default: 'text',
},
validationMessage: {
type: String,
required: false,
default: '',
},
modelValue: {
required: false,
default: '',
}
},
emits: [
'update:modelValue'
],
data() {
return {
inputValue: this.modelValue,
}
},
computed: {
identifier() {
return this.title.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, '');
},
validationMessageClass() {
if (this.validationMessage) {
return 'is-invalid';
}
return false;
}
},
watch: {
inputValue() {
this.$emit('update:modelValue', this.inputValue);
},
},
}
</script>
The reason your child will never receive an update from your parent is because even if you change the firstName.value your child-component will not re-mount and realize that change.
It's bound to a property that it internally creates (inputValue) and keeps watching that and not the modelValue that's been passed from the parent.
Here's an example using your code and it does exactly what it's supposed to and how you would expect it to work.
It receives a value once (firstName.value), creates another property (inputValue) and emits that value when there's a change.
No matter how many times the parent changes the firstName.value property, the child doesn't care, it's not the property that the input v-model of the child looks at.
You can do this instead
<template>
<div class="form-group">
<label :for="identifier"
>{{ title }}
<span class="text-danger" v-if="validation">*</span>
</label>
<input
:id="identifier"
:type="type"
class="form-control"
:class="validationMessageClass"
:placeholder="title"
v-model="localValue" // here we bind localValue as v-model to the input
/>
<div class="invalid-feedback" v-if="validationMessage">
{{ validationMessage }}
</div>
</div>
</template>
<script>
export default {
... // your code
computed: {
localValue: {
get() {
return this.modelValue;
},
set(value) {
this.$emit("update:modelValue", value);
},
},
},
};
</script>
We remove the watchers and instead utilize a computed property which will return the modelValue in it's getter (so whenever the parent passes a new value we actually use that and not the localValue) and a setter that emits the update event to the parent.
Here's another codesandbox example illustrating the above solution.

How to fire on change event of select while setting v-model?

I have Dropdown.vue and Sprint.vue use Dropdown.vue
Dropdown.vue code
<template>
<div>
<select
v-model="selected"
#input="$emit('input', $event.target.value)"
#change="emitChangeEvent"
>
<option
v-for="(option, idx) in options"
:key="`option-${idx}`"
:value="option.value"
>
{{ option.text }}
</option>
</select>
</div>
</template>
<script>
export default {
name: 'Dropdown',
props: {
options: {
type: Array,
default: () => [],
},
value: {
required: false,
},
},
computed: {
selected: {
get() {
return this.value;
},
set(newValue) {},
},
},
methods: {
emitChangeEvent() {
this.$emit('change', this.selected);
},
},
};
</script>
In Sprint.vue
<template>
<div>
<Dropdown
v-model="sprint"
:options="sprints"
#change="sprintChanged"
/>
{{ sprint }}
<button #click="change">
Change Sprint
</button>
</div>
</template>
<script lang="ts">
import {
Component, Prop, Vue, Emit,
} from 'vue-property-decorator';
import Dropdown from '#/components/Dropdown.vue';
#Component({
components: {
Dropdown,
},
})
export default class Sprint extends Vue {
private sprint = {};
private sprints = [
{ text: 'Sprint A', value: 'A' },
{ text: 'Sprint B', value: 'B' },
{ text: 'Sprint C', value: 'C' },
];
change() {
this.sprint = 'B';
}
sprintChanged(value: any) {
console.log(`value=${value}, sprint=${this.sprint}`);
}
}
</script>
When we select an option, an event was fired. But when I click on the button to set selected option by programming, there is no event of select is fired.
What am I missing?
As far as I can see, there are two main problems in your Dropdown.vue component.
1) You are binding the v-model of the element to the computed property selected, and emitting selected in your emitChangeEvent
2) and the selected computed properties doesn't have a setter so nothing happens when the value change.
To check if this is the only problem, a quick solution is to change your emitChangeEvent to this:
emitChangeEvent(event) {
this.$emit('change', event.target.value);
}
Emitting directly the value from event will skip any problem in the definition of the computed property.

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.

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)