I'm a beginner with Vue and JS and I'm struggling to update the Vue instance's data attribute based on user input. This is what I have as the template for the Vue component (located inside taskTemplate variable):
<div>
<input type="text" placeholder="Insert your task" v-model="desc"/>
<p>{{ desc }}</p>
</div>
The Vue component is defined as follows:
Vue.component("task-item", {
props: {
id: Number,
desc: String,
},
template: taskTemplate
});
And this is populated in the HTML as follows:
<div id="task-list">
<task-item
v-for="item in taskList"
v-bind="item"
></task-item>
<div id="add-more">
<button v-on:click="newTask" type="button">Add a new task</button>
</div>
</div>
Where the task list is created with the Vue instance:
var app = new Vue({
el: "#task-list",
data: {
taskList: [
{ id: 1, desc: "" },
{ id: 2, desc: "" },
]
},
methods: {
newTask() {
this.taskList.push({ id: this.taskList.length + 1, desc: "" })
}
}
});
My problem is that after updating the input element in the webpage, the component's property gets updated, but if I type in the console app.taskList[0].desc it still returns an empty string.
The end goal is to send the data the user has introduced in an API call, so if I can access Vue components instead of the taskList within the data property it is still ok. I would like to know the best practices here as well.
Props shouldn't be used in a two-way binding. Instead, bind their value to input :value, and emit any changes to the parent component.
<div>
<input
type="text"
placeholder="Insert your task"
:value="desc"
#input="$emit('update:desc', $event.target.value)"
/>
<p>{{ desc }}</p>
</div>
In the parent component then you have to listen to update event and update the source value:
<div id="task-list">
<task-item
v-for="item in taskList"
:key="item.id" // do not forget about key
v-bind="item"
#update:desc="item.desc = $event"
// or you can use the sync modifier to listen to update events for all item's props.
// v-bind.sync="item"
></task-item>
<div id="add-more">
<button v-on:click="newTask" type="button">Add a new task</button>
</div>
</div>
Try to use :value and #input instead v-model :
Vue.component('task-item', {
template: `
<div>
<input type="text"
placeholder="Insert your task"
:value="desc"
#input="$emit('update', $event.target.value)"/>
<p>{{ desc }}</p>
</div>
`,
props: {
id: Number,
desc: String,
},
//template: taskTemplate
})
new Vue({
el: '#demo',
data: {
taskList: [
{ id: 1, desc: "" },
{ id: 2, desc: "" },
]
},
methods: {
newTask() {
this.taskList.push({ id: this.taskList.length + 1, desc: "" })
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<div id="task-list">
<task-item
v-for="(item, index) in taskList"
:key="index"
v-bind="item"
#update="item.desc = $event"
></task-item>
<div id="add-more">
<button v-on:click="newTask" type="button">Add a new task</button>
</div>
</div>
</div>
Related
How can input of type checkbox present in child component be bonded to v-model on the parent component?
Required Component Way -
Parent-
<button v-if="checkedNames">Confirm</button> //show this button if 1 checkbox is selected
<div v-model="checkedNames" >
<child-component v-for="person in collection" :key="person.id"
:person="person"><child-component>
</div>
new Vue({
el: '...',
data: {
checkedNames: [],
collection: [
{id: 1, name: 'Jack', items:[{id:100,name:"Pen",quantity:5}]},
{id: 2, name: 'John', items:[{id:200,name:"Pen",quantity:10},
{id:201,name:"Paper",quantity:100,}]},
{id: 3, name: 'Mike', items:[{id:300,name:"Pen",quantity:20}]},
]
}
})
Child-
<div v-for="(item, i) in person.items" :key="i">
<input type="checkbox" :id="item.id" :value="item.name" >
<label :for="item.id">{{item.name}}</label>
<input type="number" :id="i" :value="item.quantity" >
<label :for="i">{{item.quantity}}</label>
</div>
new Vue({
el: '...',
props:{person:Object}
})
How to send the item id, quantity and main id if the checkbox is selected to the parent?
Using v-model on Components
new Vue() is to use only to create top-level Vue instance (usually called vm or app). Components used as a child elements must be registered instead - Component Registration
Always use :key with v-for - Maintaining State
Vue.component('checkboxes', {
props: ['value', 'options'],
template: `
<div>
<template v-for="item in optionsWithKey">
<input type="checkbox" :id="item.key" :value="item.name" v-model="model" :key="item.key">
<label :for="item.key">{{item.name}}</label>
</template>
</div>
`,
computed: {
model: {
get() { return this.value },
set(newValue) { this.$emit('input', newValue) }
},
optionsWithKey() {
return this.options.map(item => ({
...item,
key: "checkbox_"+ item.id
}))
}
}
})
new Vue({
el: '#app',
data: {
checkedNames: [],
options:[
{id: 1, name: 'Jack'},
{id: 2, name: 'John'},
{id: 3, name: 'Mike'},
]
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.14/vue.js"></script>
<div id='app'>
<checkboxes v-model="checkedNames" :options="options">
</checkboxes>
<pre>{{ checkedNames }}</pre>
</div>
I'm not sure but try it...
child:
<input type="checkbox" :id="item.id" :value="item.name" :change="$emit('valueChecked',item.name)">
And in parent:
<child-component #valuChecked="getValue"><child-component>
methods:{
getValue(value){
this.checkedNames.push(value);
}
}
See if this helps
// parent template
<child-component #update:checked-names="updateCheckedNames">
<child-component>
// parent
data() {
return {
checkedNames: []
};
},
methods: {
updateCheckedNames(name) {
if (this.checkedNames.includes(name)) {
const index = this.checkedNames.indexOf(name);
this.checkedNames.splice(index, 1);
} else {
this.checkedNames.push(name);
}
}
}
// child template
<div v-for="item in collection">
<input type="checkbox"
:id="item.id"
:value="item.name"
#click="$emit('update:checked-names', item.name)"> // emit here
<label :for="item.id">
{{item.name}}</label>
</div>
i'm new to VueJS and this is my first big project. In this project I have 2 filters for my elements, one is a search bar and the other is based on checkboxs. As you can see in my code, I have a computed propertie with 2 filters, the second filter is supposed to display the products with the same brand bu it doesn't work and I don't know why. If anybody have an idea it would be cool ;)
<div class="col-xs-12">
<input type="text" v-model="search" placeholder="Rechercher" class="search">
</div>
<div class="row">
+ filtres
<transition name="fade">
<div v-if="show" class="filter__container">
<ul>
<li>
<input type="checkbox" v-model="getBrand" v-on:click="filtered" v-bind:value="brand" />
<label for="apple">Apple</label>
</li>
<li>
<input type="checkbox" v-model="getBrand" v-on:click="filtered" v-bind:value="brand" />
<label for="samsung">Samsung</label>
</li>
</ul>
</div>
</transition>
</div>
<div class="row between-xs no-margin grid">
<div v-for="product in filtered" class="containers no-padding no-margin item">
<router-link to="/items">
<div #click="getProduct(product)">
<img :src="product.img" :alt="product.alt" class="img">
<div class="content">
<h3>{{ product.title }}</h3>
<p>{{ product.description }}</p>
<p>{{ product.brand }}</p>
</div>
</div>
</router-link>
</div>
</div>
<script>
import app from '../App'
import {mapActions} from 'vuex';
export default {
components: {
app
},
data() {
return {
show: false,
search: '',
brand: ['apple','samsung'],
getBrand:[],
}
},
computed: {
products() {
return this.$store.state.products;
},
filtered: function () {
return this.products.filter((product) => {
return product.title.toLowerCase().match(this.search.toLowerCase())
return product.brand.match(this.getBrand.includes(brand))
})
},
},
methods: {
...mapActions([
'currentProduct',
]),
getProduct(product) {
this.currentProduct(product);
}
},
};
</script>
export const store = new Vuex.Store({
state: {
products: [{
img: '../icons/img.png',
alt: 'logo',
title: 'Title',
description: 'Description',
brand: 'Apple'
},
{
img: '../icons/img.png',
alt: 'logo',
title: 'Title2',
description: 'Description2',
brand: 'Apple'
},
{
img: '../icons/img.png',
alt: 'logo',
title: 'Title3',
description: 'Description3'
brand: 'Samsung'
},
{
img: '../icons/img.png',
alt: 'logo',
title: 'Title4',
description: 'Description4'
brand: 'Samsung'
}
],
currentProduct: {},
},
getters: {
getProduct: state => state.currentProduct,
},
mutations: {
CURRENT_PRODUCT: (state, product) => {
state.currentProduct = product;
}
},
actions: {
currentProduct: (context, product) => {
context.commit('CURRENT_PRODUCT', product);
}
}
})
You can't return twice from the same function. Either chain the conditions using && or chain in another call to filter.
You're also misusing match. The argument needs to be a RegExp or something that can safely be converted to a RegExp. You can see the problem in the console if you type in a character like [ that has a special meaning in a RegExp. Perhaps you meant includes?
The second condition also seems to be incorrect. Not entirely clear what that combination of match and includes is trying to achieve but I think you're looking for something like this:
return this.products.filter((product) => {
return product.title.toLowerCase().includes(this.search.toLowerCase()) &&
this.getBrand.includes(product.brand)
})
It is worth noting that while both conditions are using a method called includes they are two different methods, one on a string and the other on an array.
This also seems to be wrong:
v-bind:value="brand"
brand is an array of strings and you aren't looping over them with a v-for. Change it to value="Apple" and value="Samsung" instead, ensuring the case matches the data.
I also suggest removing v-on:click="filtered". Not sure what that's trying to do but it seems to be treating a computed property as a click listener.
How i can render v-if component by button(button in parent) click inside v-for loop? and should render only in that item where clicked
<div v-for="item in items">
<button #click >Show child<button>
<div>{{item.name}}</div>
<child v-if="this button clicked" :item="item"><child>
<div>
You have to store info about state of every item (if it was clicked) in your data. Then, when you click on button you should update clicked property for particular item. Finally if item.clicked is set on true you will show your child component (or any other html).
<template>
<div>
<div v-for="item in items" :key="item.id">
<button #click="item.clicked = true" >Show child</button>
{{item.name}}
<div v-if="item.clicked">Item child</div>
</div>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data: function() {
return {
items: [
{
id: 1,
name: 'test1',
clicked: false
},
{
id: 2,
name: 'test2',
clicked: false
},
{
id: 3,
name: 'test3',
clicked: false
}
]
}
}
}
</script>
Plain and simple you just have to set some flag for latter v-if:
<div id="app">
<div v-for="item in items">
<button #click="$set(item, 'shown', true)">Show child</button>
<div>{{ item.name }}</div>
<div v-if="item.shown">Child component</div>
</div>
</div>
Here, $set() is used because initial item could lack shown field, so setting it directly with item.shown=true won't be reactive.
You can also hide button after click:
<button #click="$set(item, 'shown', true)" v-if="!item.shown">Show child</button>
To toggle visibility you just have to do it like this:
<button #click="$set(item, 'shown', !item.shown)">
{{ item.shown ? 'Hide' : 'Show' }} child
</button>
JSFiddle
You can take advantage of an item... index available in v-for directive (e.g. v-for="(item, i) in items"), to bind it (index of the item) to the function which shows an item by changing it's property:
Update: Initial answer has been deleted after requirements refinement.
Since you prefer to keep from mutation of items, you can wrap them in Map object (as keys) and keep visibility settings separately as Map values. Unfortunately, as far as I know, for the time being Vue.js does not support reactivity for Map objects, that's why I have to trigger rerendering manually by using forceUpdate:
Vue.config.devtools = false;
Vue.config.productionTip = false;
Vue.component('child', {
template: '<p>Visible child</p>'
})
new Vue({
el: "#demo",
template: `
<div>
<div v-for="item in items">
<button #click="toggleChild(item)">Toggle child</button>
<div>{{item.name}}</div>
<child v-if="isVisible(item)" :item="item"></child>
</div>
</div>
`,
data () {
return {
itemsMap: new Map(
[
{ name: 'test1' },
{ name: 'test2' }
].map(item => [item, { visible: false }])
)
};
},
methods: {
toggleChild(item) {
this.itemsMap.set(item, { visible: !this.itemsMap.get(item).visible });
this.$forceUpdate();
},
isVisible(item) {
return this.itemsMap.get(item).visible;
}
},
computed: {
items: function() {
return Array.from(this.itemsMap.keys());
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo"></div>
I'm working in a installment payment Component, and I need a dynamic list of Form Inputs, according the number of installment payments the User selects (from 1 to 12).
I don't know how I could get the values of the future Form Inputs.
When I have just one Form Input, I use 'v-model' to capture the value of the input field that already exists.
But with multiple instances of a component inside a loop, I can't figure it out.
// The User choose 6
// The value 6 (Number) goes to a property inside data() with v-model
data () {
return {
numberOfFields: 6
}
}
Then to a v-for
<template v-for="n in numberOfFields">
<input type="text" v-model="????">
</template>
I don't want to create every possibility like:
data(){
return {
inputField1: '',
inputField2: '',
inputField3: '',
// up to inputField12
}
}
I want to capture the value of the input field only if the input exists, but without creating every possible option beforehand.
You can use:
<template v-for="n in numberOfFields">
<input type="text" v-model="$data['inputField' + n]">
</template>
Demo:
new Vue({
el: '#app',
data() {
return {
numberOfFields: 6,
inputField1: '11',
inputField2: '22',
inputField3: '33',
inputField4: '44',
inputField5: '55',
inputField6: '66',
// up to inputField12
}
}
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<template v-for="n in numberOfFields">
<input type="text" v-model="$data['inputField' + n]"> {{ $data['inputField' + n] }}<br>
</template>
</div>
But this is unusual. Generally, we create another object and use it (instead of the data root).
Example using another object, called fields:
new Vue({
el: '#app',
data() {
return {
numberOfFields: 6,
fields: {
inputField1: '11',
inputField2: '22',
inputField3: '33',
inputField4: '44',
inputField5: '55',
inputField6: '66',
// up to inputField12
}
}
}
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<b>Using numberOfFields:</b><br>
<template v-for="n in numberOfFields">
<input type="text" v-model="fields['inputField' + n]"> {{ fields['inputField' + n] }}
</template>
<br><hr><br>
<b>Using (val, key):</b><br>
<template v-for="(val, key) in fields">
<input type="text" v-model="fields[key]"> {{ fields[key] }}
</template>
</div>
I am learning Vue, so I created radio button component, but I am struggling with how can one delete these values. My current solution deletes actual values, but selection is still displayed.
This is the component
<template id="fradio">
<div>
<div class="field is-horizontal">
<div class="field-label" v-bind:class = "{ 'required' : required }">
<label
class = "label"
>{{label}}
</label>
</div>
<div class="field-body">
<div>
<div class="field is-narrow">
<p class="control" v-for="val in values">
<label class = "radio">
<input
type="radio"
v-bind:name = "name"
v-bind:id = "name"
#click = "updateValue(val)"
>
<span>{{val[valueLabel]}}</span>
<span v-if="!valueLabel">{{val}}</span>
</label>
<label class="radio">
<button class="delete is-small" #click="removeValue"></button>
</label>
<slot></slot>
</p>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
label: {
type: String,
required: true,
},
inputclass: {
type: String,
},
required: {
type: Boolean,
default: false,
},
valueLabel:{
type: String,
},
returnValue:{
type: String,
},
values:{},
name:'',
},
data() {
return {
};
},
methods: {
updateValue: function (value) {
var selectedValue;
(!this.returnValue) ? selectedValue = value : selectedValue = value[this.returnValue];
this.$emit('input', selectedValue)
},
removeValue: function() {
this.$emit('input',null);
},
},
}
</script>
It should be easy, but I need someone to point out the obvious...
Update:
I just realized that you may be more focused on the data not dynamically updating, which means that your issue might be that the data in the parent component is not being updated. Most of your data is being passed down as props, so I'd need to see how the event is being fired in the parent component in order to help diagnose what's wrong. Based on the code you provided, it looks like your removeValue() function is emitting an event but I don't see any code that actually removes the value.
I would check the parent component to make sure that it is removing the child component and that should fix your problem!
Initial Answer:
Generally, when removing an item from a v-for list, you need to know the index of the item and use the Array.splice in order to modify the list to remove it.
Here's a generic example off the top of my head.
<template>
<ul>
<li v-for="(fruit, index) in fruits"
#click="removeItem(index)">
{{ fruit }}
</li>
</ul>
</template>
<script>
export default {
data() {
return {
fruits: ['Apple', 'Banana', 'Clementines']
}
},
methods: {
removeItem(index) {
this.fruits.splice(index, 1)
}
}
}
</script>
Let me know if you have any questions!