I'm trying to implement custom select component with Vuejs 2. As stated in the documentation that i should not modify value props directly and suggested to use event to pass the selected data to parent component. I'm having issue when the option value is an object and got [Object object] instead.
here's my select component template:
<div :class="inputLength">
<select :id="id"
:value="value"
#change="setValue($event.target.value)"
:multiple="multiple"
class="selectpicker">
<option value="">Nothing selected.</option>
<option :selected="option == value" v-for="option in options"
:value="option">
{{ option[label] }}
</option>
</select>
<span v-if="error.any()" class="help-block" v-text="error.all()"></span>
</div>
and here's the script part:
export default {
props: {
value: {
default() {
return ''
}
},
options: {
type: Array,
require: true
},
...
},
methods: {
setValue(val) {
this.error.clear();
this.$emit('input', val);
}
}
}
and here's the parent component
<input-select-horizontal
v-model="form.category"
:label-class="{'col-md-4': true}"
input-length="col-md-8"
:options="categories.all()"
label="name"
:error="form.errors.get('category_id')">
<span slot="label">Category <span class="required" aria-required="true">*</span></span>
the options:
[
{
id: 1,
name: 'Category 1',
description: 'desc 1'
},
{
id: 2,
name: 'Category 2',
description: 'desc 2'
},
...
]
I'm expecting the
form.category = {
id: 1,
name: "Category 1",
description: "desc 1"
}
but got [Object object]
did i miss something?
Your problem lies here:
<option v-for="option in options" :value="option">
{{ option[label] }}
</option>
You're taking a whole object and assigning it to the value attribute of the option element. This won't work, because the value attribute has to be a string. So the object is converted to [Object object].
You should try using :value="option.id", the ID value should get through to the parent component normally and you can use it to find the right category.
As mzgajner mentioned, you can't bind an object because it will convert it to a string.
What you can do however, is to convert your object to a base64 string in the options component, and then decode it again in the select component.
For example:
Component CustomOption
<template>
<option v-bind="{ ...$attrs, value: innerValue }" v-on="$listeners">
<slot>
{{ label || $attrs.value }}
</slot>
</option>
</template>
<script>
export default {
props: {
label: [String, Number, Boolean],
},
computed: {
innerValue() {
return btoa(JSON.stringify(this.$attrs.value));
},
},
};
</script>
Component CustomSelect
<template>
<select
:value="innerValue"
v-bind="$attrs"
v-on="{
...$listeners,
input: onInput,
}"
>
<slot></slot>
</select>
</template>
<script>
export default {
props: {
value: null
},
computed: {
innerValue() {
return btoa(JSON.stringify(this.value));
},
},
methods: {
onInput(e) {
let value = JSON.parse(atob(e.target.value));
this.$emit('input', value);
},
},
};
</script>
https://www.npmjs.com/package/stf-vue-select
<stf-select v-model="value" style="width: 300px; margin: 0 auto">
<div slot="label">Input address</div>
<div slot="value">
<div v-if="value">
<span>{{value.address}} (<small>{{value.text}}</small>)</span>
</div>
</div>
<section class="options delivery_order__options">
<stf-select-option
v-for="item of list" :key="item.id"
:value="item"
:class="{'stf-select-option_selected': item.id === (value && value.id)}"
>
<span>{{item.text}} (<small>{{item.address}}</small>)</span>
</stf-select-option>
</section>
</stf-select>
Related
I'm starting to make a multiselect without any package ...
When I create a select multiple, it is necessary key ctrl for select multiple elements.
I hate this ...
Can I make it with a function?
<template>
<select multiple v-model="selected">
<option v-for="item in items" v-bind:key="item.id">{{item.name}}</option>
</select>
</template>
<script>
export default {
name: 'SelectMultiple',
props: {
items: Array,
},
data(){
return{
selected: ['option1']
}
},
}
</script>
You can try with :value instead v-model and use method on click event for selecting:
Vue.component('myselect', {
template: `
<div>
<select multiple :value="selected" #click="selection">
<option v-for="item in items" :key="item.id" :selected="isSelected(item.name)">{{item.name}}</option>
</select>
{{selected}}
</div>
`,
props: {
items: Array,
},
data(){
return{
selected: ['option1']
}
},
methods: {
isSelected(item) {
return this.selected.includes(item)
},
selection(e) {
const el = e.target.value;
if(this.selected.includes(el)){
this.selected = this.selected.filter(s => s !== el)
} else {
this.selected = [...this.selected, el]
}
}
}
})
new Vue({
el: "#demo",
data(){
return{
items: [{id:1, name:'option1'}, {id:2, name:'option2'}, {id:3, name:'option3'}, {id:4, name:'option4'}, {id:5, name:'option5'}]
}
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<myselect :items="items"></myselect>
</div>
I require a lookup dropdown where I should be able to watch for multiple data changes. Currently I have this in my html:
<input list="allUsernames" type="text" v-model="selectedUser">
<datalist id="allUsernames">
<option v-for="(user, index) in allUsers"
:id="user.USERID"
:value="user.USERNAME"
:email="user.EMAIL">
</option>
</datalist>
My script data tag looks like this:
data(){
return{
allUsers: [],
selectedUserID: '',
selectedUserEmail: '',
selectedUser: '',
}
}
allUsers gets filled by an SQL call containing USERID, USERNAME and EMAIL.
I want a watch to be able to get the :id and the :email part of the option tag, but right now I can only seem to retrieve the value by default:
watch: {
selectedUser(val, oldval)
{
console.log('this only returns the :value binding:' + val);
//how do I get :id and :email values?
}
},
I want to set selectedUserID and selectedUserEmail based on the dropdown option selection made, using the vbindings :id and :email (so that I get the user.USERID and user.EMAIL values), how do I do this?
You can do this more cleanly with only the v-model data, and no watch is necessary:
data(){
return{
allUsers: [],
selectedUser: ''
}
}
Only the value binding is necessary on the options:
<option v-for="(user, index) in allUsers" :value="user.USERNAME"></option>
Use a computed to track the full object of the selected user:
computed: {
selected() {
return this.allUsers.find(user => user.USERNAME === this.selectedUser);
}
}
The selected computed refers to the whole object of the selected user, so you can use selected.USERID and selected.EMAIL in both the instance and the template.
Here's a demo:
new Vue({
el: "#app",
data(){
return{
allUsers: [
{ USERID: 1, USERNAME: 'name1', EMAIL: 'email1' },
{ USERID: 2, USERNAME: 'name2', EMAIL: 'email2' },
{ USERID: 3, USERNAME: 'name3', EMAIL: 'email3' },
],
selectedUser: ''
}
},
computed: {
selected() {
return this.allUsers.find(user => user.USERNAME === this.selectedUser);
}
}
});
.display {
margin-top: 30px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input list="allUsers" type="text" v-model="selectedUser">
<datalist id="allUsers">
<option v-for="(user, index) in allUsers" :value="user.USERNAME"></option>
</datalist>
<div v-if="selected" class="display">
<div>ID: {{ selected.USERID }}</div>
<div>EMAIL: {{ selected.EMAIL }}</div>
</div>
</div>
A good key to remember is that a watch is only necessary if you want to perform an async or time-based action when data changes. Otherwise, in most cases, use a computed.
I have a very simple vue component
<template>
<div class="field has-addons">
<div class="control is-expanded">
<div class="select is-fullwidth">
<select v-model="selected" #change="onChange($event)">
<option disabled value="">Select a list for Sync</option>
<option v-for="option in selectOptions" :value="option.id">{{option.name}}</option>
</select>
</div>
</div>
<div class="control">
<a :href="syncUrl">
<div class="button is-success">Sync</div>
</a>
</div>
</div>
</template>
<style scoped>
</style>
<script>
export default {
props: {
lists: String,
training_id: String
},
mounted: function() {
console.log('mounted');
},
computed: {
syncUrl: function () {
return "/admin/trainings/" + this.training_id + "/sync_list_to_training/" + this.selected
}
},
data: function(){
return {
selectedList: '',
selectOptions: '',
selected: ''
}
},
beforeMount() {
this.selectOptions = JSON.parse(this.lists)
this.inputName = this.name
},
methods: {
onChange(event) {
console.log(event.target.value)
}
}
}
</script>
It works, I can display the select with my options and value.
By the way the component doesn't appear in Vue Inspector, no props or data inspection.
And the select doesn't work.
I have "mounted" in my log, the select is populated with the right data from the props. But when I seelct an option the change event is not fired.
Why? The component is working and not working, i cannot find a solution.
Edit: even if I change the component to the "vue Example"
<template>
<div class="field has-addons">
<select v-model="selected">
<option v-for="option in options" v-bind:value="option.value">
{{ option.text }}
</option>
</select>
<span>Selected: {{ selected }}</span>
</div>
</template>
<style scoped>
</style>
<script>
export default {
data: function(){
return {
selected: 'A',
options: [
{ text: 'One', value: 'A' },
{ text: 'Two', value: 'B' },
{ text: 'Three', value: 'C' }
]
}
}
}
</script>
Is doesn't work and I cannot see it in inspector...
Here is the invocation
import Vue from 'vue/dist/vue.esm'
import TurbolinksAdapter from 'vue-turbolinks';
Vue.use(TurbolinksAdapter)
import ActiveCampaign from '../../../application/components/active_campaign/ActiveCampaign.vue'
document.addEventListener('turbolinks:load', () => {
const vueApp = new Vue({
el: '#app-container',
components: {
'active_campaign': ActiveCampaign,
},
})
})
No error in console: vue-devtools Detected Vue v2.6.10
UPD:
Can you try this?
Another hack is to wait for the next tick:
<input type="checkbox" v-model="myModel" #change="myFunction()">
...
myFunction: function() {
var context = this;
Vue.nextTick(function () {
alert(context.myModel);
}
}
This is from here https://github.com/vuejs/vue/issues/293#issuecomment-265716984
I'm a newbie of Vue, and I'm trying to simply clear the data of input component once I've submitted, but it seems I'm missing something, because since it's parent data is cleared, I still see the filled value of the input component.
Here is a living example.
I've set to the input child component v-model="title" from it's parent wrapper. Once I submit the data to the parent, I call addItem and in the end, I supposed to clear the data model just by clear it this.title = '', but I probably do something wrong on how to bind data from parent to child.
And above the code, starting from the parent component:
<template>
<form #submit="addItem" class="todo-insert">
<input-item
icon="create"
name="title"
placeholder="Add a ToVue item..."
v-model="title"
/>
<button-item tone="confirm" class="todo-insert__action">
Aggiungi
</button-item>
</form>
</template>
<script>
import ButtonItem from '#vue/Form/ButtonItem/ButtonItem.vue'
import InputItem from '#vue/Form/InputItem/InputItem.vue'
import uuid from 'uuid'
export default {
name: 'TodoInsert',
components: {
ButtonItem,
InputItem
},
data () {
return {
title: ''
}
},
methods: {
addItem (e) {
e.preventDefault()
const todo = {
id: uuid.v4(),
isComplete: false,
title: this.title
}
this.$emit('add-todo', todo)
this.title = ''
}
}
}
</script>
<style lang="scss" scoped src="./TodoList.scss"></style>
This is the child input component:
<template lang="html">
<label class="input">
<div v-if="label" class="input__label text-sans text-sans--label">
{{ label }}
</div>
<div class="input__row">
<input
:autocomplete="autocomplete"
:class="[hasPlaceholderLabel, isDirty]"
:name="name"
:placeholder="placeholder"
class="input__field"
type="text"
v-on:input="updateValue($event.target.value)"
v-on:blur="updateValue($event.target.value)"
>
<div v-if="placeholderLabel" class="input__placeholder text-sans text-sans--placeholder">
{{ placeholderLabel }}
</div>
<div v-if="icon" class="input__icon-area">
<icon-item
:name="icon"
/>
</div>
</div>
</label>
</template>
<script>
import IconItem from '../../IconItem/IconItem.vue'
export default {
name: 'InputItem',
props: {
autocomplete: {
type: String,
default: 'off'
},
icon: String,
label: String,
name: {
type: String,
default: 'input-text'
},
placeholder: String,
placeholderLabel: String
},
computed: {
hasPlaceholderLabel () {
return this.placeholderLabel ? 'input__field--placeholder-label' : ''
},
isDirty () {
// return this.$attrs.value ? 'input__field--dirty' : ''
return 'input__field--dirty'
}
},
methods: {
updateValue: function (value) {
this.$emit('input', value)
}
},
components: {
IconItem
}
}
</script>
<style lang="scss" src="./InputItem.scss"></style>
What am I missing?
Your child component is bound unidirectionally. It means that it can change the value, but does not receive any update from the parent component. To receive updates, you need to receive the property value in your child:
props: {
value: String
}
Then, you need to pass the value received to the input :
<input
:value="value"
:autocomplete="autocomplete"
:class="[hasPlaceholderLabel, isDirty]"
:name="name"
:placeholder="placeholder"
class="input__field"
type="text"
v-on:input="updateValue($event.target.value)"
v-on:blur="updateValue($event.target.value)"
>
Now the input should update when the parent component changes the value
I've created a simple component named DefaultButton.
It bases on properties, that are being set up whenever this component is being created.
The point is that after mounting it, It does not react on changes connected with "defaultbutton", that is an object located in properties
<template>
<button :class="buttonClass" v-if="isActive" #click="$emit('buttonAction', defaultbutton.id)" >
{{ this.defaultbutton.text }}
</button>
<button :class="buttonClass" v-else disabled="disabled">
{{ this.defaultbutton.text }}
</button>
</template>
<script>
export default {
name: "defaultbutton",
props: {
defaultbutton: Object
},
computed: {
buttonClass() {
return `b41ngt ${this.defaultbutton.state}`;
},
isActive() {
return (this.defaultbutton.state === "BUTTON_ACTIVE" || this.defaultbutton.state === "BUTTON_ACTIVE_NOT_CHOSEN");
}
}
};
</script>
Having following component as a parent one:
<template>
<div v-if="state_items.length == 2" class="ui placeholder segment">
{{ this.state_items[0].state }}
{{ this.state_items[1].state }}
{{ this.current_active_state }}
<div class="ui two column very relaxed stackable grid">
<div class="column">
<default-button :defaultbutton="state_items[0]" #buttonAction="changecurrentstate(0)"/>
</div>
<div class="middle aligned column">
<default-button :defaultbutton="state_items[1]" #buttonAction="changecurrentstate(1)"/>
</div>
</div>
<div class="ui vertical divider">
Or
</div>
</div>
</template>
<script type="text/javascript">
import DefaultButton from '../Button/DefaultButton'
export default {
name: 'changestatebox',
data() {
return {
current_active_state: 1
}
},
props: {
state_items: []
},
components: {
DefaultButton
},
methods: {
changecurrentstate: function(index) {
if(this.current_active_state != index) {
this.state_items[this.current_active_state].state = 'BUTTON_ACTIVE_NOT_CHOSEN';
this.state_items[index].state = 'BUTTON_ACTIVE';
this.current_active_state = index;
}
},
},
mounted: function () {
this.state_items[0].state = 'BUTTON_ACTIVE';
this.state_items[1].state = 'BUTTON_ACTIVE_NOT_CHOSEN';
}
}
</script>
It clearly shows, using:
{{ this.state_items[0].state }}
{{ this.state_items[1].state }}
{{ this.current_active_state }}
that the state of these items are being changed, but I am unable to see any results on the generated "DefaultButtons". Classes of objects included in these components are not being changed.
#edit
I've completely changed the way of delivering the data.
Due to this change, I've abandoned the usage of an array; instead I've used two completely not related object.
The result is the same - class of the child component's object is not being
DefaulButton.vue:
<template>
<button :class="buttonClass" v-if="isActive" #click="$emit('buttonAction', defaultbutton.id)" >
{{ this.defaultbutton.text }}
</button>
<button :class="buttonClass" v-else disabled="disabled">
{{ this.defaultbutton.text }}
</button>
</template>
<style lang="scss">
import './DefaultButton.css';
</style>
<script>
export default {
name: "defaultbutton",
props: {
defaultbutton: {
type: Object,
default: () => ({
id: '',
text: '',
state: '',
})
}
},
computed: {
buttonClass() {
return `b41ngt ${this.defaultbutton.state}`;
},
isActive() {
return (this.defaultbutton.state === "BUTTON_ACTIVE" ||
this.defaultbutton.state === "BUTTON_ACTIVE_NOT_CHOSEN");
}
}
};
</script>
ChangeStateBox.vue:
<template>
<div class="ui placeholder segment">
{{ this.state_first.state }}
{{ this.state_second.state }}
{{ this.current_active_state }}
<div class="ui two column very relaxed stackable grid">
<div class="column">
<default-button :defaultbutton="state_first" #buttonAction="changecurrentstate(0)"/>
</div>
<div class="middle aligned column">
<default-button :defaultbutton="state_second" #buttonAction="changecurrentstate(1)"/>
</div>
</div>
<div class="ui vertical divider">
Or
</div>
</div>
</template>
<script type="text/javascript">
import DefaultButton from '../Button/DefaultButton'
export default {
name: 'changestatebox',
data() {
return {
current_active_state: 1
}
},
props: {
state_first: {
type: Object,
default: () => ({
id: '',
text: ''
})
},
state_second: {
type: Object,
default: () => ({
id: '',
text: ''
})
},
},
components: {
DefaultButton
},
methods: {
changecurrentstate: function(index) {
if(this.current_active_state != index) {
if(this.current_active_state == 1){
this.$set(this.state_first, 'state', "BUTTON_ACTIVE_NOT_CHOSEN");
this.$set(this.state_second, 'state', "BUTTON_ACTIVE");
} else {
this.$set(this.state_first, 'state', "BUTTON_ACTIVE");
this.$set(this.state_second, 'state', "BUTTON_ACTIVE_NOT_CHOSEN");
}
this.current_active_state = index;
}
},
},
created: function () {
this.state_first.state = 'BUTTON_ACTIVE';
this.state_second.state = 'BUTTON_ACTIVE_NOT_CHOSEN';
}
}
</script>
You're declaring props wrong. It is either an array of prop names or it is an object with one entry for each prop declaring its type, or it is an object with one entry for each prop declaring multiple properties.
You have
props: {
state_items: []
},
but to supply a default it should be
props: {
state_items: {
type: Array,
default: []
}
},
But your problem is most likely that you're mutating state_items in such a way that Vue can't react to the change
Your main problem is the way you are changing the button state, according with Array change detection vue can't detect mutations by indexing.
Due to limitations in JavaScript, Vue cannot detect the following
changes to an array:
When you directly set an item with the index, e.g.
vm.items[indexOfItem] = newValue When you modify the length of the
array, e.g. vm.items.length = newLength
In case someone will be having the same issue:
#Roy J as well as #DobleL were right.
The reason behind this issue was related with the wrong initialization of state objects.
According to the documentation:
Vue cannot detect property addition or deletion.
Since Vue performs the getter/setter conversion process during instance
initialization, a property must be present in the
data object in order for Vue to convert it and make it reactive.
Before reading this sentence, I used to start with following objects as an initial data:
var local_state_first = {
id: '1',
text: 'Realized',
};
var local_state_second = {
id: '2',
text: 'Active'
};
and the correct version of it looks like this:
var local_state_first = {
id: '1',
text: 'Realized',
state: 'BUTTON_ACTIVE'
};
var local_state_second = {
id: '2',
text: 'Active',
state: 'BUTTON_ACTIVE'
};
whereas declaring the main component as:
<change-state-box :state_first="local_state_first" :state_second="local_state_second" #buttonAction="onbuttonAction"/>
Rest of the code remains the same ( take a look at #edit mark in my main post )