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

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

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

ComboBox template component with VueJS

I want to make a Combobox template Component with vuejs v3 and to do so I have the following code:
<template>
<select name={{selector_name}} class= "combobox" id={{selector_id}}>
v-for=opt in {{selector_options}}
<option value=opt>
opt
</option>
</select>
</template>
<script>
export default {
name: 'ComboBox',
data() {
return {
selector_name: null,
selector_id: null,
selector_options : null,
}
},
props: {
selector_name: {
type: String,
required: true
},
selector_id: {
type: String,
default: "combobox"
},
selector_options: {
type: Array,
required: true
}
},
methods: {
onChange: (event) => {
console.log(event.target.value);
},
},
computed: {},
}
</script>
But the way that I use v-for does not seem to be correct to me, can you please tell me how can I correct that? thanks in advance.
I see a lot of things, to be clear and answer your questions, v-for is used on a element.
<template>
// For mor readability i recommend you do bind your property:
// - name={{selector_name}} become :name="selector_name"
<select :name="selector_name" class= "combobox" :id="selector_id">
<!-- v-for is required with a unique key, you can take the index and use it -->
<option v-for="(opt, index) in selector_options" :key="`opt ${index}`" value=opt>
{{ opt }}
</option>
</select>
</template>
You can't define a props and data with the same name. Props, inject inside a data the property and value.
It's exactly the same, but the data come from a parent where you pass a value to the child.
So use props if you need to pass data, but not define it too inside data.
There is a work example of all of that (i use data and not props for readability).

Why do I have en error "Unexpected mutation of "modelValue" prop" in Vue?

So i'm working with you and I have a select component, I created a modelValue prop which is an empty string, and passed it in v-model:
<select v-model="modelValue" #change="changeOption">
<option
disabled
value="">
Choose from the list
</option>
<option
v-for="option in options"
:key="option.value"
:value="option.value">
{{ option.name }}
</option>
</select>
props:{
modelValue:{
type: String
},
options:{
type: Array,
default: () => []
}
}
But for some reason it gives an error: Unexpected mutation of "modelValue" prop.
I just started working with Vue so I'm not sure where this error could come from. I was following a tutorial and everything seemed to work there.
You can't modify a prop directly.
The best way is create a computed with get set functions and emit update inside set function.
https://v2.vuejs.org/v2/guide/computed.html#Computed-Setter
computed: {
localModelValue: {
get() {
return this.modelValue
},
set(newValue) {
this.$emit('update:modelValue', newValue)
},
},
},
And in the component instance declare prop as sync
https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier
:model-value.sync="..."
create life cycle "computed"
computed: {
localModelValue: {
get() {
return this.modelValue
},
},
},
After that change v-model="modelValue" to v-model="localModelValue"

Unable to register custom component globally with vue.js

I have made selectbox component and wants to reuse it in other components. Si I want to register that component globally. I have imported that component in main.js but doesnot works.
main.js
import Selectbox from "#/modules/Selectbox.vue";
Vue.component("Selectbox", Selectbox);
Selectbox.vue
<template>
<div>
<label>{{ label }}</label>
<select #change="$emit('input', $event.target.value)">
<option
v-for="opt in options"
:key="opt.value"
:value="opt.value"
:selected="value === opt.value"
>
{{ errorMessage }}
{{ opt.label || "No label" }}
</option>
</select>
</div>
</template>
<script>
export default {
props: {
label: {
type: String,
required: true
},
},
data() {
return {
errorMessage: "",
option: "lorem",
options: [
{ label: "lorem", value: "lorem" },
{ label: "ipsum", value: "ipsum" }
]
};
},
};
</script>
Test.vue
<template>
<div>
<Selectbox v-model="ward_no"/>
</div>
</template>
<script>
export default {
data() {
return {
ward_no: '',
};
}
}
</script>
There is nothing wrong in the way that you are trying to register global component but you are missing script tag.
UPDATE: After talking to #prabinasht on skype and reviewing her code, I saw that in multiple files she forgot to remove locally imported/registered component and at the same time the component was registered globally too, so that was the problem.
Register the component this way
Vue.component('Selectbox', require('#/modules/Selectbox.vue').default)

VueJs reactivity with parent component property object

I'm having difficulty to get parent component's property object, with dynamically populated properties to make the values available inside of the same component.
A bit hard to explain, so please have a look at the example below:
Parent Component
<script>
export default {
data() {
return {
fields: {},
}
}
}
</script>
Child Component
<template>
<select
#change="update()"
v-model="field"
>
<option
v-for="option in options"
:value="option.value"
>
{{ option.name }}
</option>
</select>
</template>
<script>
export default {
props: {
initialOptions: {
type: Array,
required: true
}
},
data() {
return {
field: '',
options: this.initialOptions
}
},
mounted() {
if (
(this.field === undefined || this.field === '') &&
this.options.length > 0
) {
this.field = this.options[0].value;
}
this.update();
},
methods: {
update() {
this.$emit('input', this.field);
}
}
}
</script>
DOM
<parent-component inline-template>
<div>
<child-component>
:initial-options="[{..}, {..}]"
v-model="fields.type_id"
></child-component>
</div>
<div :class="{ dn : fields.type_id == 2 }">
// ...
</div>
</parent-component>
Using Vue console I can see that fields object gets all of the child component models with their associated values as they emit input when they are mounted, however for some strange reason the :class="{ dn : fields.type_id == 2 }" does not append the class dn when the selection changes to 2. Dom doesn't seem to reflect the changes that are synced between parent and child components.
Any help on how to make it work?
Here is what I was trying to get at in comments. Vue cannot detect changes to properties that are added dynamically to an object unless you add them using $set. Your fields object does not have a type_id property, but it gets added because you are using v-model="fields.type_id". As such, Vue does not know when it changes.
Here, I have added it and the color of the text changes as you would expect.
console.clear()
Vue.component("child-component", {
template: `
<select
#change="update()"
v-model="field"
>
<option
v-for="option in options"
:value="option.value"
>
{{ option.name }}
</option>
</select>
`,
props: {
initialOptions: {
type: Array,
required: true
}
},
data() {
return {
field: '',
options: this.initialOptions
}
},
mounted() {
if (
(this.field === undefined || this.field === '') &&
this.options.length > 0
) {
this.field = this.options[0].value;
}
this.update();
},
methods: {
update() {
this.$emit('input', this.field);
}
}
})
new Vue({
el: "#app",
data: {
fields: {
type_id: null
}
}
})
.dn {
color: red;
}
<script src="https://unpkg.com/vue#2.2.6/dist/vue.js"></script>
<div id="app">
<div>
<child-component :initial-options="[{name: 'test', value: 1}, {name: 'test2', value: 2}]" v-model="fields.type_id"></child-component>
</div>
<div :class="{ dn : fields.type_id == 2 }">
Stuff
</div>
</div>
It looks like you are trying to make a re-usable component.
I would ask myself what the value of a re-usable component is when the parent component has to handle more than half of the effort. The component might be better named...
<DifficultToUseSelect/>.
Essentially, you are creating a component that provides, all by itself, all of the following HTML...
<select></select>
Everything else is managed by the parent component.
It would probably be more useful to do any of the following...
Encapsulate often needed options in a specific select component, as in
StateAbbrevsSelect v-model="state"
Pass the name of a data model to a select component. The component would then load and manage its own data via the model.
Pass the URL of a web service to the component, which it then calls to load its options.
Again, the main point I am trying to convey here is that making a re-usable component where more than half of the effort is handled by the parent component is really not very re-usable.