Get each HTML element in a Vue slot from JavaScript - vue.js

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>

Related

I'm emiting a value from the watcher but this value is not received in the parent component Vue 3 Select component

I have a Custom select component which emits a value in the watch method but when I have to get that value, is not shown in the parent component. (VUE 3)
this is my select component:
<template>
<div class="input-group">
<label class="label">{{ label }}</label>
<select v-model="select">
<option v-for="option in options" :value="option.id" :key="option.id">
{{ option.name }}
</option>
<option :value="select" disabled hidden>Select...</option>
</select>
</div>
</template>
<script lang="ts">
import { ref, watch, getCurrentInstance } from "vue";
export default {
name: "Select",
props: {
options: {
type: Array,
required: true,
},
select: {
type: String,
},
label: {
type: String,
},
},
setup(props) {
const { emit } = getCurrentInstance();
const select = ref(props.value);
watch(select, (value) => {
emit("input", value);
});
return {
select,
};
},
};
</script>
This is the implementation in the parent component
<template>
<Select
label="Group Name"
:options="questions_groups"
v-model="groupName"
/>
<template>
<script>
......
.....
const groupName = ref(null);
<script/>
I need to know why I cannot get the value od the v-model variable in the implementation
for vue3 (because it supports multiple models the event name changed) you need to use update:modelValue
watch(select, (value) => {
emit("update:modelValue", value);
})
for clarity...
modelValue is the default model's name, so
<input v-model="searchText" /> is equivalent to <input v-model:modelValue="searchText" />
having multiple models allows use such as:
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>
more info at https://vuejs.org/guide/components/v-model.html#component-v-model

Getting a single array from an array of object

I am trying to iterate through the array of object called sources at change event of my select input then pushing this single array to a new element conditionally
<template>
<div class="sourceselection">
<div class="jumbotron">
<h2><span class="glyphicon glyphicon-list"></span> News List</h2>
<h4>Select News Source</h4>
<select class="form-control" #change="sourceChanged">
<option v-bind:value="source.id" v-for="source in sources" :key="source.id">{{source.name}}</option>
</select>
</div>
<div v-if="source">
<h6 >{{source.description}}</h6>
<a v-bind:href="source.url"></a>
</div>
</div>
</template>
<script>
export default {
name: 'SourceSelection',
data () {
return {
sources: [],
source: '',
}
},
methods: {
sourceChanged (event) {
for (var i = 0; i < this.sources.length; i++){
if (this.sources[i].id == event.target.value){
this.source = this.sources[i];
}
}
}
},
created : function () {
this.$http.get('https://newsapi.org/v2/sources?language=en&apiKey=cf0866b5aaef42e995f9db37bb3f7ef4')
.then(response => {
this.sources = response.data.sources;
})
.catch(error => console.log(error));
}
}
</script>
i expect to pull the description into my h6 if the source exist
At first create a state in data and name it sourceId,
then bind it to select <select v-model="sourceId" class="form-control">.
Now we need a computed filed which returns the source return this.sources.find(s => s.id === this.sourceId)
<template>
<div class="sourceselection">
<div class="jumbotron">
<h2><span class="glyphicon glyphicon-list"></span> News List</h2>
<h4>Select News Source</h4>
<select v-model="sourceId" class="form-control">
<option v-for="source in sources" :value="source.id" :key="source.id">
{{ source.name }}
</option>
</select>
</div>
<div v-if="source">
<h6 >{{ source.description }}</h6>
<a v-bind:href="source.url"></a>
</div>
</div>
</template>
<script>
export default {
name: 'SourceSelection',
data () {
return {
sources: [],
sourceId: ''
}
},
computed: {
source () {
return this.sources.find(s => s.id === this.sourceId)
}
},
created() {
this.$http.get('https://newsapi.org/v2/sources?language=en&apiKey=cf0866b5aaef42e995f9db37bb3f7ef4')
.then(response => {
this.sources = response.data.sources;
})
.catch(error => console.log(error));
}
}
</script>
Instead of using an event handler you can bind the selected value directly to your source data.
here is a demo:
Vue.config.devtools = false
Vue.config.productionTip = false
new Vue({
el: '#app',
data() {
return {
sources: [{
id: '1',
description: 'one desc',
name: 'one'
}, {
id: '2',
description: 'two desc',
name: 'two'
}, ],
source: ''
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<select class="form-control" v-model="source">
<option v-bind:value="source" v-for="source in sources" :key="source.id">{{source.name}}</option>
</select>
<h6>{{ source.description }}</h6>
</div>

Change options and value at the same time

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.

How to get index data from object used in v-for in select

I have a component involving a select element. Below, opts is an array of objects.
Vue.component('atcf-select', {
props: [
'opts',
],
data() {
return {
element_index: '',
};
},
template: `
<div>
<select #change="onChange(opt,index)">
<option v-for="(opt,index) in opts">
{{ opt.text }} {{opt.index}}
</option>
</select>
</div>
`,
methods: {
onChange(opt,index) {
//Do something with opt and index...
}
}
};
The problem is obviously I cannot get the selected opt object and its index, and use it as a parameter for onChange method. What is the correct way to get the selected option's index and object?
You won't be able to pass the opt or index values to the change listener on the select element because it is outside the scope of the v-for.
If you don't specify any parameters for the onChange handler, Vue will implicitly pass an event object. From there, you can get the selectedIndex value via e.target.selectedIndex.
Here's an example:
new Vue({
el: '#app',
data() {
return {
opts: [
{ value: 'a', text: 'A' },
{ value: 'b', text: 'B' },
{ value: 'c', text: 'C' },
]
}
},
methods: {
onChange(e) {
let index = e.target.selectedIndex;
let option = this.opts[index];
console.log(index, option);
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.8/vue.min.js"></script>
<div id="app">
<select #change="onChange">
<option v-for="(opt, index) in opts" :key="index" :value="opt.value">
{{ opt.text }}
</option>
</select>
</div>
You can use v-model
{{ option.value }} - {{ option.text }}
Index of {{valeureSelectionnee}} is : {{ IndexValeureSelectionnee }}

issue caused the Row selection overwrite

I asked question on adding/removing row in how to use "v-for" for adding or removing a row with multiple components
However, I got a bug: when I adding a row, the items in the first row filled to second row and when I changed the second row, the 1st row is also overwritten as the same as 2nd row.
i must did sth really wrong.
in .js
var data1={selected: null, items: ["A1","B1"]};
Vue.component('comp1',{
template: ` <select v-model="selected">
<option disabled value="">Please select</option>
<option v-for="item in items" :value="item">{{item}}
</option>
</select>`,
data:function(){
return data1
}
});
var data2={selected: null, items: ["A2","B2"]};
Vue.component('comp2',{
template: ` <select v-model="selected">
<option disabled value="">Please select</option>
<option v-for="item in items" :value="item">{{item}}
</option>
</select>`,
data:function(){
return data2
}
});
new Vue({
el: '#app',
data: {
rows: []
},
computed:{
newId(){
return this.rows.length == 0 ? 1 : Math.max(...this.rows.map(r => r.id)) + 1
}
},
methods: {
addRow: function() {
this.rows.push({id: this.newId });
},
removeRow: function(row) {
this.rows.splice(this.rows.indexOf(row), 1)
}
},
});
in .html
<div id="app">
<div v-for="row in rows">
<comp1></comp1>
<comp2></comp2>
<button #click="removeRow(row)">Remove Row</button>
</div>
<button #click="addRow">Add Row</button>
</div>
You need to add the key.
<div v-for="row in rows" :key="row.id">
And you shouldn't share the data between the components, so move your data into the components.
data: function() {
return {
selected: null,
items: ["A1", "B1"]
}
}
Here is a working version.
Vue.component('comp1', {
template: ` <select v-model="selected">
<option disabled value="">Please select</option>
<option v-for="item in items" :value="item">{{item}}
</option>
</select>`,
data: function() {
return {
selected: null,
items: ["A1", "B1"]
}
}
});
Vue.component('comp2', {
template: ` <select v-model="selected">
<option disabled value="">Please select</option>
<option v-for="item in items" :value="item">{{item}}
</option>
</select>`,
data: function() {
return {
selected: null,
items: ["A2", "B2"]
}
}
});
new Vue({
el: '#app',
data: {
rows: []
},
computed: {
newId() {
return this.rows.length == 0 ? 1 : Math.max(...this.rows.map(r => r.id)) + 1
}
},
methods: {
addRow: function() {
this.rows.push({
id: this.newId
});
},
removeRow: function(row) {
this.rows.splice(this.rows.indexOf(row), 1)
}
},
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.js"></script>
<div id="app">
<div v-for="row in rows" :key="row.id">
<comp1></comp1>
<comp2></comp2>
<button #click="removeRow(row)">Remove Row</button>
</div>
<button #click="addRow">Add Row</button>
</div>
Specifically, the way you are sharing data is because you are defining the data like this:
var data1={selected: null, items: ["A1","B1"]};
And returning that object from your data function in the component:
data:function(){
return data1
}
This means that every instance of that component is sharing the same data. That's not what you want with components. Each component should have it's own copy of the data. In this case, there is no need whatsoever to define the data object returned from the data function outside the component.