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>
Related
I have a very simple task i am trying to solve. I want to make the vue model update or display in a new property called chunk2 when there is a change. I am sending the value back to using this.chunk=this.chunk2. How can I achieve this. The result after the button is clicked should say "This is a chun"..in which the "k" gets removed.
new Vue({
el: "#app",
data: {
chunk:"this is a chunk",
chunk2:"",
todos: [
{ text: "Learn JavaScript", done: false },
{ text: "Learn Vue", done: false },
{ text: "Play around in JSFiddle", done: true },
{ text: "Build something awesome", done: true }
]
},
methods: {
toggle: function(){
this.chunk.split('n')[1];
alert(this.chunk.split('n')[1]);
this.chunk=this.chunk2
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h2>Todos:</h2>
{{chunk}}
{{chunk2}}
<br>
<button v-on:click="toggle()">magic
</button>
</div>
Observations :
Strings are immutable. Hence, The string manipulation methods such as split returns a new array and will not update the original string. Ex :
let chunk = 'this is a chunk';
const newStr = chunk.split('n');
console.log(chunk); // this is a chunk (Not changing the original string)
console.log(newStr); // updating the maniupulated values in a new variable.
As chunk2 is an empty string and you are assigning it back into chunk will update chunk also empty.
this.chunk.split('n') will split the string with excluding n. Hence, If you want to include n. You have to split it with k.
Working Demo :
new Vue({
el: "#app",
data: {
chunk:"this is a chunk",
chunk2:"",
todos: [
{ text: "Learn JavaScript", done: false },
{ text: "Learn Vue", done: false },
{ text: "Play around in JSFiddle", done: true },
{ text: "Build something awesome", done: true }
]
},
methods: {
toggle: function(){
this.chunk = this.chunk.split('k')[0];
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h2>Todos:</h2>
{{chunk}}
{{chunk2}}
<br>
<button v-on:click="toggle()">magic
</button>
</div>
In my application I am receiving object as below :
{
"data1":[
{},{}{}
],
"data2":[ {},{},{}....],
"data3":[ {},{},{}.....]
}
If someone can help me on how to use v-for here? I want to loop through "data1", "data2"... using v-for. ( in sinlge v-for precisely )
UPDATE:I would like to have object like this.
data :[{
title :"data1",
values: [{ } {} {}]
},
{
title :"data1",
values: [{ } {} {}]
},
.....
]
You can do something like this :
<div id="app">
<h2>Todos:</h2>
<div v-for="t1 in todos.todos1">
<label>{{t1.text}}</label>
</div>
<div v-for="t2 in todos.todos2">
<label>{{t2.text}}</label>
</div>
<div v-for="t3 in todos.todos3">
<label>{{t3.text}}</label>
</div>
</div>
new Vue({
el: "#app",
data: {
todo:{},
todos:{todos1: [
{ text: "Learn JavaScript 1", done: false },
{ text: "Learn Vue 1", done: false }
],
todos2: [
{ text: "Play around in JSFiddle 2", done: true },
{ text: "Build something awesome 2", done: true }
],
todos3: [
{ text: "Learn Vue 3", done: false },
{ text: "Play around in JSFiddle 3", done: true },
]
}
},
created(){
this.todo = Object.values(this.todos)
console.log(this.todo)
}
})
You can do something like
<div v-for="(value, propertyName, index) in items"></div>
WARNING
When iterating over an object, the order is based on the enumeration order of Object.keys(), which is not guaranteed to be consistent across JavaScript engine implementations.
The above can be found on the Vue Documentation.
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.
I wanted to make a two-way data binding on my form input in Vue.js 2.3. However, I cannot use the v-model directive, because I want the data to be updated only on clicking the submit button. Meanwhile, the input value may be updated from another Vue method, so it should be bound to the data property text. I made up something like this jsFiddle:
<div id="demo">
<input :value="text" ref="input">
<button #click="update">OK</button>
<p id="result">{{text}}</p>
</div>
new Vue({
el: '#demo',
data: function() {
return {
text: ''
};
},
methods: {
update: function () {
this.text = this.$refs.input.value;
}
}
});
It works, but it does not scale well when there are more inputs. Is there a simpler way to accomplish this, without using $refs?
You can use an object and bind its properties to the inputs. Then, in your update method, you can copy the properties over to another object for display purposes. Then, you can set a deep watcher to update the values for the inputs whenever that object changes. You'll need to use this.$set when copying the properties so that the change will register with Vue.
new Vue({
el: '#demo',
data: function() {
return {
inputVals: {
text: '',
number: 0
},
displayVals: {}
};
},
methods: {
update() {
this.copyObject(this.displayVals, this.inputVals);
},
copyObject(toSet, toGet) {
Object.keys(toGet).forEach((key) => {
this.$set(toSet, key, toGet[key]);
});
}
},
watch: {
displayVals: {
deep: true,
handler() {
this.copyObject(this.inputVals, this.displayVals);
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id="demo">
<input v-model="inputVals.text">
<input v-model="inputVals.number">
<button #click="update">OK</button>
<input v-for="val, key in displayVals" v-model="displayVals[key]">
</div>
If you're using ES2015, you can copy objects directly, so this isn't as verbose:
new Vue({
el: '#demo',
data() {
return {
inputVals: { text: '', number: 0 },
displayVals: {}
};
},
methods: {
update() {
this.displayVals = {...this.inputVals};
},
},
watch: {
displayVals: {
deep: true,
handler() {
this.inputVals = {...this.displayVals};
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id="demo">
<input v-model="inputVals.text">
<input v-model="inputVals.number">
<button #click="update">OK</button>
<input v-for="val, key in displayVals" v-model="displayVals[key]">
</div>
You can use two separate data properties, one for the <input>'s value, the other for the committed value after the OK button is clicked.
<div id="demo">
<input v-model="editText">
<button #click="update">OK</button>
<p id="result">{{text}}</p>
</div>
new Vue({
el: '#demo',
data: function() {
return {
editText: '',
text: ''
};
},
methods: {
update: function () {
this.text = this.editText;
}
}
});
Updated fiddle
With a slightly different approach than the other answers I think you can achieve something that is easily scalable.
This is a first pass, but using components, you could build your own input elements that submitted precisely when you wanted. Here is an example of an input element that works like a regular input element when it is outside of a t-form component, but only updates v-model on submit when inside a t-form.
Vue.component("t-input", {
props:["value"],
template:`
<input type="text" v-model="internalValue" #input="onInput">
`,
data(){
return {
internalValue: this.value,
wrapped: false
}
},
watch:{
value(newVal){
this.internalValue = newVal
}
},
methods:{
update(){
this.$emit('input', this.internalValue)
},
onInput(){
if (!this.wrapped)
this.$emit('input', this.internalValue)
}
},
mounted(){
if(this.$parent.isTriggeredForm){
this.$parent.register(this)
this.wrapped = true
}
}
})
Here is an example of t-form.
Vue.component("t-form",{
template:`
<form #submit.prevent="submit">
<slot></slot>
</form>
`,
data(){
return {
isTriggeredForm: true,
inputs:[]
}
},
methods:{
submit(){
for(let input of this.inputs)
input.update()
},
register(input){
this.inputs.push(input)
}
}
})
Having those in place, your job becomes very simple.
<t-form>
<t-input v-model="text"></t-input><br>
<t-input v-model="text2"></t-input><br>
<t-input v-model="text3"></t-input><br>
<t-input v-model="text4"></t-input><br>
<button>Submit</button>
</t-form>
This template will only update the bound expressions when the button is clicked. You can have as many t-inputs as you want.
Here is a working example. I included t-input elements both inside and outside the form so you can see that inside the form, the model is only updated on submit, and outside the form the elements work like a typical input.
console.clear()
//
Vue.component("t-input", {
props: ["value"],
template: `
<input type="text" v-model="internalValue" #input="onInput">
`,
data() {
return {
internalValue: this.value,
wrapped: false
}
},
watch: {
value(newVal) {
this.internalValue = newVal
}
},
methods: {
update() {
this.$emit('input', this.internalValue)
},
onInput() {
if (!this.wrapped)
this.$emit('input', this.internalValue)
}
},
mounted() {
if (this.$parent.isTriggeredForm) {
this.$parent.register(this)
this.wrapped = true
}
}
})
Vue.component("t-form", {
template: `
<form #submit.prevent="submit">
<slot></slot>
</form>
`,
data() {
return {
isTriggeredForm: true,
inputs: []
}
},
methods: {
submit() {
for (let input of this.inputs)
input.update()
},
register(input) {
this.inputs.push(input)
}
}
})
new Vue({
el: "#app",
data: {
text: "bob",
text2: "mary",
text3: "jane",
text4: "billy"
},
})
<script src="https://unpkg.com/vue#2.2.6/dist/vue.js"></script>
<div id="app">
<t-form>
<t-input v-model="text"></t-input><br>
<t-input v-model="text2"></t-input><br>
<t-input v-model="text3"></t-input><br>
<t-input v-model="text4"></t-input><br>
<button>Submit</button>
</t-form>
Non-wrapped:
<t-input v-model="text"></t-input>
<h4>Data</h4>
{{$data}}
<h4>Update Data</h4>
<button type="button" #click="text='jerome'">Change Text</button>
</div>
I am fairly new to vue and can't figure out how to add data values within a template. I am trying to build a very basic form builder. If I click on a button it should add another array of data into a components variable. This is working. The I am doing a v-for to add input fields where some of the attributes are apart of the array for that component. I get it so it will add the input but no values are being passed into the input.
I have created a jsfiddle with where I am stuck at. https://jsfiddle.net/a9koj9gv/2/
<div id="app">
<button #click="add_text_input">New Text Input Field</button>
<my-component v-for="comp in components"></my-component>
<pre>{{ $data | json }}</pre>
</div>
new Vue({
el: "#app",
data: function() {
return {
components: [{
name: "first_name",
showname: "First Name",
type: "text",
required: "false",
fee: "0"
}]
}
},
components: {
'my-component': {
template: '<div>{{ showname }}: <input v-bind:name="name" v-bind:type="type"></div>',
props: ['showname', 'type', 'name']
}
},
methods: {
add_text_input: function() {
var array = {
name: "last_name",
showname: "Last Name",
type: "text",
required: "false",
fee: "0"
};
this.components.push(array);
}
}
})
I appreciate any help as I know I am just missing something obvious.
Thanks
Use props to pass data into the component.
Currently you have <my-component v-for="comp in components"></my-component>, which doesn't bind any props to the component.
Instead, do:
<my-component :showname="comp.showname"
:type="comp.type"
:name="comp.name"
v-for="comp in components"
></my-component>
Here is a fork of your fiddle with the change.
while asemahle got it right, here is a boiled down version on how to expose data to the child component. SFC:
async created() {
await this.setTemplate();
},
methods: {
async setTemplate() {
// const templateString = await axios.get..
this.t = {
template: templateString,
props: ['foo'],
}
},
},
data() {
return {
foo: 'bar',
t: null
}
}
};
</script>
<template>
<component :is="t" :foo="foo"></component>
It pulls a template string that is compiled/transpiled into a js-render-function. In this case with Vite with esm to have a client-side compiler available:
resolve: {
alias: {
// https://github.com/vuejs/core/tree/main/packages/vue#with-a-bundler
vue: "vue/dist/vue.esm-bundler.js",
the index.js bundle size increases by few kb, in my case 30kb (which is minimal)
You could now add some what-if-fail and show-while-loading with defineasynccomponent