VueJS: How do you provide a context variable to #change handler? - vue.js

I have a select widget inside a loop:
<div v-for="(item, index) in items" :key="index">
<select #change="handleChange">
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
</select>
</div>
When handleChange is triggered, I want to get following pieces of information:
index of the parent loop
value of the selected option
Above code only gives me #2. How do I get #1?

There are several ways that you can do this:
Solution 1: Bind index to an arbitrary HTML5 data- attribute
This approach involves using :data-index="index" on your select element, and then using the e.target.dataset.index to retrieve the bound index of the element
new Vue({
el: '#app',
data: {
items: ['lorem', 'ipsum', 'dolor', 'sit', 'amet']
},
methods: {
handleChange(e) {
console.log(e.target.value, e.target.dataset.index);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="app">
<div v-for="(item, index) in items" :key="index">
<select #change="handleChange" :data-index="index">
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
</select>
</div>
</div>
Of course, you can store the index as any other arbitrarily-named data- attribute, e.g. <select #change="handleChange" :data-abc="index"> and then simply accessing it in your JS using e.target.dataset.abc.
Solution 2: Pass index as an argument to the #click callback
This method involves passing $event and index as parameters to your handleChange callback. The $event variable in VueJS allows you to pass the original JS event to the handler:
new Vue({
el: '#app',
data: {
items: ['lorem', 'ipsum', 'dolor', 'sit', 'amet']
},
methods: {
handleChange(e, idx) {
console.log(e.target.value, idx);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="app">
<div v-for="(item, index) in items" :key="index">
<select #change="handleChange($event, index)">
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
</select>
</div>
</div>

for parent loop index,
<select #change="handleChange(index)">
for value of selected option, use v-model for two way binding

Related

Vue v-for loop binding attribute

I'm trying to figure out how to do this in vue, I'm stuck trying to bind the value of the "selected" in the options.
In the simplified code below, I get exactly what I want, but only for the first product. It binds correctly with the first computed property:
<template>
<div>
<div v-for="index in 2" :key="index">
<select>
<option :selected="product1 === ''">Empty</option>
<option
v-for="(product, index) of products"
:key="index"
:selected="product1 === product.name"
>{{product.name}}</option>
</select>
</div>
</div>
</template>
<script>
// chosen products come from vuex store
computed: {
product1() {
return store.state.product1;
},
product2() {
return store.state.product2;
},
}
</script>
But then how can I change this to be automatic in the v-for loop, probably using the index?
What I need is actually a loop that will render this:
<template>
<div>
<select>
<option :selected="product1 === ''">Empty</option>
<option
v-for="(product, index) of products"
:key="index"
:selected="product1 === product.name"
>{{product.name}}
</option>
</select>
<select>
<option :selected="product2 === ''">Empty</option>
<option
v-for="(product, index) of products"
:key="index"
:selected="product2 === product.name"
>{{product.name}}
</option>
</select>
</div>
</template>
I've tried using something like:
:selected="`product${index}` === product.name"
but that gives a string, not the computed property value...
you can pass parameter to your computed property:
<template>
<div>
<div v-for="index in 2" :key="index">
<select>
<option :selected="getProduct(index) === ''">Empty</option>
<option
v-for="(product, index) of products"
:key="index"
:selected="getProduct(index) === product.name"
>{{product.name}}</option>
<script>
// chosen products come from vuex store
computed: {
getProduct() {
return index=>
store.state['product'+index];
},
}
</script>
You can put the selected products in an array and then access it by index:
computed: {
selectedProducts () {
return [store.state.product1, store.state.product2];
}
}
And then you can do :selected="selectedProducts[index] === product.name".

How can I add class="required" in the following select v-model?

<div v-for="value in day" class="checkboxFour">
<input type="checkbox" id="need" value="value.val" v-model="value.selected" style="width: 10%!important;">
<label for="need" style=" width: 30%!important;">{{value.name}}</label>
<select v-model="value.from" class="select-style">From
<option value="08:00">08.00</option>
<option value="12:00">12.00</option>
<option value="20:00">20.00</option>
<option value="23:00">23.00</option>
</select>
<select v-model="value.to" class="select-style">To
<option value="08:00">08.00</option>
<option value="12:00">12.00</option>
<option value="20:00">20.00</option>
<option value="23:00">23.00</option>
</select>
<br>
</div>
This is select option. When I use required="". I am getting getAttribute error. How can I able to correct the same?
Also how can I use selected in the following case? My target is select a particular value previously and then user can change according to his need? Please help me to obtain the same?
The required attribute is a boolean (an not a class). You don't need to give it a value; if it is present in the select tag, the select is required.
You can also bind it to a boolean value to change whether the select is required.
new Vue({
el: '#app',
data: {
isRequired: false
}
});
[required] {
outline: thin solid red;
}
<script src="//unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
<select required>
<option>An option</option>
</select> This one is always required
<div>
<select :required="isRequired">
<option>Whatever</option>
</select>
<input type="checkbox" v-model="isRequired"> Is Required: {{isRequired}}
</div>
</div>
As it mentioned in Vue.JS docs you can use v-bind:class to achieve what you wanted:
<select v-model="value.from" v-bind:class="{required: isRequired}">
which required is data, computed field in your app/component.
Update 1:
new Vue({
el: "#myApp",
data: {
// you should define a variable in your data
isRequired: false
},
methods: {
doSomething: function(){
this.isRequired = true;
}
}
});

remove option from second select if already selected [Ordered multiple selection]

I had select option repeater.
What I want is that when I selected johndoe at the first option, it will no longer display on the second select option.
here's my html
<div id="app">
<h1>Vue JS Multiple Fields Repeater</h1>
<div class="col-sm-6">
<div class="panel panel-default relative has-absolute" v-for="(field, index) in users.usersRepeater">
<button #click="addUsersField" type="button">
Add
</button>
<button #click="deleteUsersField(index)" v-if="field != 0" type="button">
Delete
</button>
<div class="panel-body has-absolute">
<div class="form-group">
<label for="users" class="control-label col-sm-3 text-left">Users {{field}}</label>
<select :name="'users'+index"
class="form-control"
id="users">
<option value="" hidden>Select User</option>
<option value="1">John Doe</option>
<option value="2">Mark Doe</option>
<option value="3">Mae Doe</option>
<option value="4">John Smith</option>
<option value="5">Mae Smith</option>
</select>
</div>
</div>
</div>
</div>
</div>
here's my vue.js
new Vue({
el: '#app',
data: {
users: {
usersRepeater: [{ user: '' }]
}
},
methods: {
addUsersField: function() {
this.users.usersRepeater.push({
user: ''
});
},
deleteUsersField: function(index) {
this.users.usersRepeater.splice(index, 1);
},
}
});
here's the fiddle -> https://jsfiddle.net/0e3csn5y/23/
Ok I have this working now. I liked the question because making an ordered selection is a generic case. But, for me anyway, it wasn't straightforward. The breakthrough was realising that, when you number the choices, the whole state of the component could be encapsulated in one array, allUsers. Available users and choices then become computed properties, based on this array. Moral of the story: get your store right, with no interactions between elements of the store.
My answer weighs in at 130 lines. How long and hard would this be without Vue? Mind boggles.
Stack wants me to post some code, so here's the computed property that generates an array of choices made, in order of their priority, from the all users array...
choices(){
return this.store.allUsers.map((aUser,index)=>{
if(aUser.selection != null)
return {idxAllUsers : index, selection: aUser.selection};
else
return null;
})
.filter(aSelection=>aSelection != null)
.sort((a,b)=>{return a.selection - b.selection})
.map(a=>a.idxAllUsers);
},
I found this one very helpful.

Set same value for sibling component's select

Using v-for, I am looping through a component. The component is for each client. In this component, I have same form for each client and when a select value is selected for the first component (client 1), I want to select this value for every client.
Do I need to pass the data to the root and create a single source of truth variable?
I tried setting up a basic version:
<div id="app">
<my-comp v-for="x in 2" v-bind:val="x"></my-comp>
</div>
Vue.component('my-comp', {
props: ['val'],
template: `
<div>
<div>
<label>Status</label>
<select :data-client="val" #change="statusChanged">
<option selected="" disabled="" value="0"></option>
<option value="xxx">Xxx</option>
<option value="yyy">Yyy</option>
<option value="zzz">Zzz</option>
</select>
</div>
</div>
`,
methods: {
statusChanged(e) {
var client = e.target.getAttribute('data-client')
if (client == 1) {
alert('set same value for client 2')
}
}
}
})
new Vue({
el: '#app',
})
Here is a fiddle: https://jsfiddle.net/w53164t2/
I considered a little bit after my original answer and have come up with something I think is a little bit more real world than the example fiddle provided in the original question; specifically it is easy to make all the selects reflect the same value if they are all using the same source value, however I expect in a real world scenario each component would be independently bound to a single client. Each client would want their individual value to change, with the one caveat that if a "master" client changed, then all non-master clients should change to the master client's value.
To that end, this might be a case where I think a component specific bus is appropriate. The master would emit an event when it's value changed and the the other clients would set their value with respect to the master.
console.clear()
const MyCompBus = new Vue()
Vue.component('my-comp', {
props: ['val', 'master'],
computed:{
selected:{
get(){return this.val},
set(v){
this.$emit('update:val', v)
if (this.master)
MyCompBus.$emit("master-updated", v)
}
}
},
methods:{
onMasterUpdated(newMasterValue){
if (this.master) return
this.selected = newMasterValue
}
},
created(){
MyCompBus.$on('master-updated', this.onMasterUpdated)
},
beforeDestroy(){
MyCompBus.$off('master-updated', this.onMasterUpdated)
},
template: `
<div>
<div>
<label>Status</label>
<select v-model="selected">
<option selected="" disabled="" value="0"></option>
<option value="xxx">Xxx</option>
<option value="yyy">Yyy</option>
<option value="zzz">Zzz</option>
</select>
</div>
</div>
`,
})
new Vue({
el: '#app',
data:{
masterValue: null,
clients:[
{id: 1, selectedValue: null, master: true},
{id: 2, selectedValue: null},
{id: 3, selectedValue: null},
{id: 4, selectedValue: null},
]
}
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<my-comp v-for="client in clients"
:val.sync="client.selectedValue"
:master="client.master"
:key="client.id">
</my-comp>
{{clients}}
</div>
Original Answer
Bind them all to the same value using v-model.
Vue.component('my-comp', {
props: ['value'],
computed:{
selected:{
get(){return this.value},
set(v){this.$emit('input', v)}
}
},
template: `
<div>
<div>
<label>Status</label>
<select v-model="selected">
<option selected="" disabled="" value="0"></option>
<option value="xxx">Xxx</option>
<option value="yyy">Yyy</option>
<option value="zzz">Zzz</option>
</select>
</div>
</div>
`,
})
And in the template:
<my-comp v-for="x in 2" v-model="selectedValue" :key="x"></my-comp>
Here is the updated fiddle.
If you want to stick with val as the property you can use .sync instead.
Vue.component('my-comp', {
props: ['val'],
computed:{
selected:{
get(){return this.val},
set(v){this.$emit('update:val', v)}
}
},
template: `
<div>
<div>
<label>Status</label>
<select v-model="selected">
<option selected="" disabled="" value="0"></option>
<option value="xxx">Xxx</option>
<option value="yyy">Yyy</option>
<option value="zzz">Zzz</option>
</select>
</div>
</div>
`,
})
And in the template:
<my-comp v-for="x in 2" :val.sync="selectedValue" :key="x"></my-comp>
Example fiddle.
If you want just one of them designated as a "master" select, then add a property that does so.
Vue.component('my-comp', {
props: ['val', 'master'],
computed:{
selected:{
get(){return this.val},
set(v){if (this.master) this.$emit('update:val', v)}
}
},
template: `
<div>
<div>
<label>Status</label>
<select v-model="selected">
<option selected="" disabled="" value="0"></option>
<option value="xxx">Xxx</option>
<option value="yyy">Yyy</option>
<option value="zzz">Zzz</option>
</select>
</div>
</div>
`,
})
And in the template:
<my-comp v-for="x in 5" :val.sync="selectedValue" :master="1 == x" :key="x"></my-comp>
Example fiddle.

How to return boolean and not string when using select?

I have this:-
<div class="form-group">
<label for="">Allow Multiple</label>
<select class="form-control" v-model="allowMultiple">
<option value=true>Yes</option>
<option value=false>No</option>
</select>
</div>
I set allowMultiple=true when I initialize it, but when I select No then allowMultiple='false' So its no longer a Boolean but a String? How to get it to be Boolean?
In HTML, if you set attribute value in a tag, the value will be default type--string.So you can use vue v-model to bind it to other type value, for example, Boolean, Number and etc.
Following code is working, the result is what you want
new Vue({
el:'#app',
data: {
allowMultiple: false
},
methods: {
print: function () {
alert(this.allowMultiple);
}
}
})
<div class="form-group" id='app'>
<label for="">Allow Multiple</label>
<select class="form-control" v-model="allowMultiple" #change='print'>
<option :value='true'>Yes</option>
<option :value='false'>No</option>
</select>
</div>
<script src="https://unpkg.com/vue#2.4.2/dist/vue.min.js"></script>
Here is a way to do it in Vue.
Instead of hard coding your options in html. Use the Vue way, set an array of options in your Vue data then use v-for to render all the options from the array.
Each option should have 2 properties: a text and a value. The value should be the boolean you are looking for.
Now whenever the user changes the selected option, it will always be boolean.
new Vue({
el: '#app',
data: {
allowMultiple: true,
options: [
{text: 'Yes', value: true},
{text: 'No', value: false},
],
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="app">
<div class="form-group">
<div>Allow multiple: {{allowMultiple}} [Type: {{typeof allowMultiple}}]</div><br>
<label for="">Allow Multiple</label>
<select class="form-control" v-model="allowMultiple">
<option v-for="option in options" :value="option.value">{{option.text}}</option>
</select>
</div>
</div>
The easier and quicker way I found that works for me is this:
<select id="selected" v-model="item.selected">
<option :value=0>No</option>
<option :value=1>Yes</option>
</select>
Maybe it's useful for anyone.
Try this:
<option value=1>Yes</option>
<option value=0>No</option>