Change options and value at the same time - vue.js

I have a problem when I try to change the options of my select and its value at the same time. If I use v-model, it works properly but if I use v-bind:value + v-on:change, it will not work.
Here is a js fiddle that will illustrate the problem : https://jsfiddle.net/2vcer6dz/18/
The first time you click on the button "change", only the first select value will be 3. If you reclick they all become 3.
Html
<div id="app">
<select v-model="value">
<option v-for="item in options" :key="item.Value" :value="item.Value">{{item.Text}}</option>
</select>
<select :value="value" v-on:change="value = $event.target.value">
<option v-for="item in options" :key="item.Value" :value="item.Value">{{item.Text}}</option>
</select>
<select-option v-model="value" :options="options"></select-option>
<br />
<input type="button" value="change" v-on:click="change" />
</div>
<template id="template-select-option">
<select :value="value" v-on:change="update($event.target.value)">
<option v-for="item in options" :key="item.Value" :value="item.Value">{{item.Text}}</option>
</select>
</template>
Javascript
Vue.component('select-option', {
template: '#template-select-option',
props: ['value', 'options'],
methods: {
update: function (value) {
this.$emit('input', value);
}
}
});
new Vue({
el: '#app',
data: {
value: 1,
options: [{Value:1, Text:1}, {Value:2, Text:2}]
},
methods: {
change: function () {
this.options = [{Value:1, Text:1}, {Value:2, Text:2}, {Value:3, Text:3}];
this.value = 3;
}
}
});
Expected result
All selects should have the value "3" when you click on the button "change"

Changing the options and the value at the same time is confusing Vue. This is probably a minor bug in Vue. If you use $nextTick to push the value change off to the next update cycle, they all work.
change: function () {
this.options = [{Value:1, Text:1}, {Value:2, Text:2}, {Value:3, Text:3}];
this.$nextTick(() => {
this.value = 3;
});
}

It seems that this is a known bug which was closed because a workaround was found.
The workaround is to declare another property and cast v-model on it. This solutions is easier to implement inside a component.
https://jsfiddle.net/6gbfhuhn/8/
Html
<template id="template-select-option">
<select v-model="innerValue">
<option v-for="item in options" :key="item.Value" :value="item.Value">{{item.Text}}</option>
</select>
</template>
Javascript
Vue.component('select-option', {
template: '#template-select-option',
props: ['value', 'options'],
computed: {
innerValue: {
get: function() { return this.value; },
set: function(newValue) { this.$emit('input', newValue); }
}
}
});
Note: In the github thread, it is suggested to use a computed property instead, but if you use a computed property, vue will throw warning every time you change the value in your dropdown because computed property don't have setter.

Related

VUE, Can't use selected option value in a select component

Im trying to use a selected option value. Can't show the value or save it.
This is my child component
`
<script>
export default {
props: {
options : {
type:Array,
},
selectOpt:undefined,
}
emits : ['input','change','option:selected']
}
</script>
<template>
<div>
<h1>
Hi, I'm a component
</h1>
<select
v-model="selectOpt"
#change="$emit('input', event.target.value)">
<option v-for="option in options"
:key="option"
>{{option}}</option>
</select>
</div>
</template>
`
This is my parent
`
<script >
import Comp from './Comp.vue'
export default {
data() {
return {
options : [1,2,3,4,5,6],
optSelected : undefined,
}
},
components: {
Comp
}
}
</script>
<template>
<Comp v-model="optSelected" :options="options"></Comp>
<p>
--->{{optSelected}}
</p>
</template>
`
I tried changin the 'input' event and 'change' event. not sure what im doing wrong.
i've found a solution that requires a vue-select library that i prefer not to use.
It's a simple detail: in vue 3, you need to use update:modelValue in order to change the v-model in parent component. (Reference: https://v3-migration.vuejs.org/breaking-changes/v-model.html)
And another thing: you souldn't use the prop as a v-model to prevent side effects in your application. You can read more about it here: https://eslint.vuejs.org/rules/no-mutating-props.html
Hope it helps:
<script>
export default {
props: {
options: {
type: Array
},
modelValue: undefined
},
emits: ['update:modelValue'],
watch: {
innerValue(newValue) {
this.$emit('update:modelValue', newValue)
},
modelValue(newValue) {
this.innerValue = newValue;
}
},
data() {
return {
innerValue: this.modelValue
};
}
};
</script>
<template>
<div>
<h1>Hi, I'm a component</h1>
<select v-model="innerValue">
<option v-for="option in options" :key="option">
{{ option }}
</option>
</select>
</div>
</template>
[Edit] Using Fallthrough Attribute:
You can use the v-bind="$atrrs":
<script>
export default {
props: {
options: {
type: Array
},
},
};
</script>
<template>
<div>
<h1>Hi, I'm a component</h1>
<select v-bind="$attrs">
<option v-for="option in options" :key="option">
{{ option }}
</option>
</select>
</div>
</template>
Read more: https://vuejs.org/guide/components/attrs.html#attribute-inheritance-on-multiple-root-nodes

How can I access the bound value of a <select>'s currently selected <option>?

Say I am making a simple component which wraps a <select>. This component supports v-model, as documented here.
Vue.component('custom-select', {
template: '#component',
props: ['options', 'value'],
});
<script src="https://cdn.jsdelivr.net/npm/vue#2/dist/vue.js"></script>
<script type="text/x-template" id="component">
<div id="component">
<select :value="value" #input="$emit('input', $event.target.value)">
<option v-for='option in options' :value="option">
<slot v-bind="{ option }"></slot>
</option>
</select>
</div>
</script>
This works fine if the options are strings. However, if they are a different type (e.g. objects), then the values emitted are cast to strings (e.g. '[object Object]'). This is because $event.target.value pulls the value from the DOM, which will always be a string type.
Is there a way to get the original bound value of the selected <option>? I'm aware of v-model as an option, but it complicates things as it requires adding watchers.
EDIT I have discovered that Vue seems to assign the original bound value to the _value property on the DOM node, though I'm not sure if accessing that is a good idea since it's underscore prefixed and seems to be undocumented.
Let's say options prop is an array of object as below
You can change the event emitter of child component to return object instead of string like this:
<style>
[v-cloak] {
display: none;
}
</style>
<!-- // App -->
<div id="app">
<div v-cloak>
Value in parent: {{selectedValue}}
<br><br>
<custom-select :options='selectOptions' v-model='selectedValue'></custom-select>
</div>
</div>
<!-- // JS Code -->
<script src="https://cdn.jsdelivr.net/npm/vue#2/dist/vue.js"></script>
<script type="text/x-template" id="component">
<div id="component">
<select #change="$emit('input', options.find(option => option.value == $event.target.value))">
<option v-for='option in options' :value="option.value">
{{ option.text }}
</option>
</select>
</div>
</script>
<script>
// Mount App
new Vue({
el: '#app',
data() {
return {
selectOptions: [
{ text: 'Apple', value: 'apple', price: '10' },
{ text: 'Banana', value: 'banana', price: '20' },
{ text: 'Strawberry', value: 'strawberry', price: '30' },
],
selectedValue: {}
}
},
// Custom component
components: {
'custom-select': Vue.component('custom-select', {
template: '#component',
props: ['options', 'value'],
})
}
})
</script>
While I still haven't found an exact solution to my original question, I've found a design pattern that I think solves the issue satisfactorily. By using a computed property with a getter and setter, I can use v-model on the <select> without needing watchers or any internal component state.
Vue.component('custom-select', {
template: '#component',
props: ['options', 'value'],
computed: {
valueProxy: {
get() {
return this.value;
},
set(newValue) {
this.$emit('input', newValue);
},
},
},
});
<script src="https://cdn.jsdelivr.net/npm/vue#2/dist/vue.js"></script>
<script type="text/x-template" id="component">
<div id="component">
<select v-model="valueProxy">
<option v-for='option in options' :value="option">
<slot v-bind="{ option }"></slot>
</option>
</select>
</div>
</script>

Get each HTML element in a Vue slot from JavaScript

I am creating a custom select component in VueJS 2. The component is to be used as below by the end-user.
<custom-select>
<option value="value 1">Option 1</option>
<option value="value 2">Option 2</option>
<option value="value 3">Option 3</option>
...
<custom-select>
I know the Vue <slot> tag and usage. But how do I get the user provided <option> tags as an array/list so I can get its value and text separately for custom rendering inside the component?
Those <option>s would be found in the default slot array (this.$slots.default), and you could get to the inner text and value of the <option>s like this:
export default {
mounted() {
const options = this.$slots.default.filter(node => node.tag === 'option')
for (const opt of options) {
const innerText = opt.children.map(c => c.text).join()
const value = opt.data.attrs.value
console.log({ innerText, value })
}
}
}
demo
You can achieve it, using v-bind and computed property
new Vue({
el: '#vue',
data: {
selected: '',
values: [
{
code: '1',
name: 'one'
},
{
code: '2',
name: 'two'
}
]
},
computed: {
selectedValue() {
var self = this;
var name = "";
this.values.filter(function(value) {
if(value.code == self.selected) {
name = value.name
return;
}
})
return name;
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="vue">
<div>
<select v-model="selected">
<option v-for="value in values" v-bind:value="value.code">
{{ value.name }}
</option>
</select>
</div>
<strong>{{ selected }} {{ selectedValue }}</strong>
</div>

VueJS Using #click on <option> elements and #change on <select> element

I have a simple function that manipulates a data between true and false. I use it to make my div hidden and visible like a toggle. I also have a with three element. I found out that I cannot use #click for <option> elements, I need to use #change for my <select>.
But in this way, whenever an is selected, the function is being triggered and my data toggles between true and false. Here is my <select> element;
<select #change="isDisabled">
<option>Please select a security type</option>
<option>No Security</option>
<option>Personal</option>
<option>Enterprise</option>
</select>
IsDisabled function takes a variable and change its values between true and false so my div becomes hidden and visible as follows;
<div v-if="noSecurity">something</div>
But here is the thing, I only want to trigger the function when the user select the "No Security" option. Now it's being triggered whenever I select an option, so it turned out to be some kind of a toggle. But I want to hide the div when I select the "No Security" option and show the div if something different is selected. What should I do?
I've made a CodeSandbox where you could see the result :
https://codesandbox.io/s/magical-meitner-63eno?file=/src/App.vue
But here is the explanation:
<template>
<section>
<select #change="isDisabled">
<option>Please select a security type</option>
<option>No Security</option>
<option>Personal</option>
<option>Enterprise</option>
</select>
<div v-if="noSecurity">You Choose no security, that's dangerous !</div>
</section>
</template>
<script>
import HelloWorld from "./components/HelloWorld";
export default {
name: "App",
data() {
return {
noSecurity: false,
};
},
methods: {
isDisabled(e) {
console.log("e", e.target.value);
if (e.target.value === "No Security") {
// do your change
return (this.noSecurity = !this.noSecurity);
}
// to allow reset if another option is selected
if (this.noSecurity) {
return this.noSecurity = false;
}
},
},
};
</script>
Basically when you use the #change handler, your function will receive an event, in this event you can catch the target value with event.target.value.
Doing so, you do a condition if the value is equal to No Security (so the selected item), you change your state, if it's not No Security, you do nothing, or you do something else you would like to do.
Appart from that, I advice you to change your method name isDisabled to a global convention name like handleChange, or onChange.
Pass id values in your option so when you get the select event you're clear that No security or whatver the name you would like to change will be the same.
Because if one day you change No security to another name, you have to update all your conditions in your app. Try to avoid conditions with strings values like this if you can.
<option value="1">No Security</option> // :value="securityType.Id" for example if coming from your database
<option value="2">Personal</option>
<option value="3">Enterprise</option>
then in your function it will be
if (e.target.value === noSecurityId) {
// do your change
this.noSecurity = !this.noSecurity;
}
//...
There's no need for the additional noSecurity variable. Create your select with v-model to track the selected value. Give each option a value attribute.
<select v-model="selected">
<option value="">Please select a security type</option>
<option value="none">No Security</option>
<option value="personal">Personal</option>
<option value="enterprise">Enterprise</option>
</select>
Check that value:
<div v-if="selected === 'none'">something</div>
You can still use the noSecurity check if you prefer by creating a computed:
computed: {
noSecurity() {
return this.selected === 'none';
}
}
Here's a demo showing both:
new Vue({
el: "#app",
data() {
return {
selected: ''
}
},
computed: {
noSecurity() {
return this.selected === 'none';
}
},
methods: {},
created() {}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<select v-model="selected">
<option value="">Please select a security type</option>
<option value="none">No Security</option>
<option value="personal">Personal</option>
<option value="enterprise">Enterprise</option>
</select>
<div v-if="selected === 'none'">something</div>
<div v-if="noSecurity">something</div>
</div>
Is using v-model instead of using a method is option for you? If it is, please try the following:
HTML:
<div id="hello-vue" class="demo">
<select v-model="security">
<option>Please select a security type</option>
<option>No Security</option>
<option>Personal</option>
<option>Enterprise</option>
</select>
<div v-if="security=='No Security'">something</div>
</div>
JS:
const HelloVueApp = {
data() {
return {
security: undefined
}
}
}

Vue.js custom component with HTML <select> and v-model (W3C compliant)

I'm new to Vue.js (using Nuxt.js) and what I'm trying to achieve is to have a Select component that I can reuse everywhere and is W3C compliant.
With the help of #Jasmonate answers, I managed to create this component, it's working. But the value attribute is still visible in the source code and so isn't W3C compliant. Maybe the problem is coming from somewhere else in the project ?!
Parent component
<custom-select
:options="options"
v-model="selectedOption"
></custom-select>
<span>Selected : {{ selectedOption }}</span>
<script>
data() {
return {
selectedOption: "A",
options: [
{ label: "One", value: "A" },
{ label: "Two", value: "B" },
{ label: "Three", value: "C" }
],
};
}
</script>
custom-select.vue
<template>
<select :value="value" #input="clicked">
<option
v-for="option in options"
:key="option.label"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</template>
<script>
export default {
props: {
value: {
required: true
},
options: {
type: Array,
required: true
}
},
methods: {
clicked($event) {
this.$emit("input", $event.target.value);
}
}
};
</script>
I read those documentation pages:
Form Input Bindings
Components Basics
And also looked around the web to find example of v-model in a custom component, but it's always about the input tag. The only example I found about a custom select with v-model isn't actually a select tag, like the Vue Select plugin or this thread on StackOverflow.
v-model is syntax sugar. By default, the value is a prop that has the name value, and it changes (two-way-binding) whenever the event input is emitted.
Also, v-model is bound on the select element, not option.
Your code can be modified as such:
<template>
<select :value="value" #input="clicked">
<option
v-for="option in options"
:key="option.label"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</template>
<script>
export default {
props: {
value: {
required: true
},
options: {
type: Array,
required: true
}
},
methods: {
clicked($event) {
this.$emit('input', $event.target.value);
}
}
};
</script>
Documentation here: https://v2.vuejs.org/v2/guide/components.html#Using-v-model-on-Components
You can also change the prop name and event name that v-model uses, see: https://v2.vuejs.org/v2/guide/components-custom-events.html#Customizing-Component-v-model