mirror of
https://github.com/tenrok/vue-select.git
synced 2026-06-22 10:30:34 +03:00
- search input is 100% width when value is empty
- value is no longer required, two-way binding is not enforced - add deprecated tag to maxHeight (this should just be changed with CSS) - add onChange prop for Vuex compatibility - fixed bug in isValueEmpty, added regression test - added docblocks
This commit is contained in:
+144
-11
@@ -104,7 +104,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="dropdown" :class="cssClasses">
|
<div class="dropdown" :class="dropdownClasses">
|
||||||
<div v-el:toggle @mousedown.prevent="toggleDropdown" class="dropdown-toggle clearfix" type="button">
|
<div v-el:toggle @mousedown.prevent="toggleDropdown" class="dropdown-toggle clearfix" type="button">
|
||||||
<span class="form-control" v-if="!searchable && isValueEmpty">
|
<span class="form-control" v-if="!searchable && isValueEmpty">
|
||||||
{{ placeholder }}
|
{{ placeholder }}
|
||||||
@@ -121,16 +121,17 @@
|
|||||||
v-el:search
|
v-el:search
|
||||||
v-show="searchable"
|
v-show="searchable"
|
||||||
v-model="search"
|
v-model="search"
|
||||||
@keydown.delete="maybeDeleteValue"
|
@keyup.delete="maybeDeleteValue"
|
||||||
@keydown.esc="onEscape"
|
@keyup.esc="onEscape"
|
||||||
@keydown.up.prevent="typeAheadUp"
|
@keyup.up.prevent="typeAheadUp"
|
||||||
@keydown.down.prevent="typeAheadDown"
|
@keyup.down.prevent="typeAheadDown"
|
||||||
@keydown.enter.prevent="typeAheadSelect"
|
@keyup.enter.prevent="typeAheadSelect"
|
||||||
@blur="open = false"
|
@blur="open = false"
|
||||||
@focus="open = true"
|
@focus="open = true"
|
||||||
type="search"
|
type="search"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
:placeholder="searchPlaceholder"
|
:placeholder="searchPlaceholder"
|
||||||
|
:style="{ width: isValueEmpty ? '100%' : 'auto' }"
|
||||||
>
|
>
|
||||||
|
|
||||||
<i v-el:open-indicator role="presentation" class="open-indicator glyphicon-chevron-down glyphicon"></i>
|
<i v-el:open-indicator role="presentation" class="open-indicator glyphicon-chevron-down glyphicon"></i>
|
||||||
@@ -152,42 +153,104 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
/**
|
||||||
|
* Contains the currently selected value. Very similar to a
|
||||||
|
* `value` attribute on an <input>. In most cases, you'll want
|
||||||
|
* to set this as a two-way binding, using :value.sync. However,
|
||||||
|
* this will not work with Vuex, in which case you'll need to use
|
||||||
|
* the onChange callback property.
|
||||||
|
* @type {Object||String||null}
|
||||||
|
*/
|
||||||
value: {
|
value: {
|
||||||
twoway: true,
|
default: null
|
||||||
required: true
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of strings or objects to be used as dropdown choices.
|
||||||
|
* If you are using an array of objects, vue-select will look for
|
||||||
|
* a `label` key (ex. [{label: 'This is Foo', value: 'foo'}]). A
|
||||||
|
* custom label key can be set with the `label` prop.
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
options: {
|
options: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default() { return [] },
|
default() { return [] },
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the max-height property on the dropdown list.
|
||||||
|
* @deprecated
|
||||||
|
* @type {String}
|
||||||
|
*/
|
||||||
maxHeight: {
|
maxHeight: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '400px'
|
default: '400px'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable/disable filtering the options.
|
||||||
|
* @type {Boolean}
|
||||||
|
*/
|
||||||
searchable: {
|
searchable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Equivalent to the `multiple` attribute on a `<select>` input.
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
multiple: {
|
multiple: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Equivalent to the `placeholder` attribute on an `<input>`.
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
placeholder: {
|
placeholder: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a Vue transition property on the `.dropdown-menu`. vue-select
|
||||||
|
* does not include CSS for transitions, you'll need to add them yourself.
|
||||||
|
* @type {String}
|
||||||
|
*/
|
||||||
transition: {
|
transition: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'expand'
|
default: 'expand'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables/disables clearing the search text when an option is selected.
|
||||||
|
* @type {Boolean}
|
||||||
|
*/
|
||||||
clearSearchOnSelect: {
|
clearSearchOnSelect: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells vue-select what key to use when generating option
|
||||||
|
* labels when each `option` is an object.
|
||||||
|
* @type {String}
|
||||||
|
*/
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'label'
|
default: 'label'
|
||||||
}
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An optional callback function that is called each time the selected
|
||||||
|
* value(s) change. When integrating with Vuex, use this callback to trigger
|
||||||
|
* an action, rather than using :value.sync to retreive the selected value.
|
||||||
|
* @type {Function}
|
||||||
|
* @default {null}
|
||||||
|
*/
|
||||||
|
onChange: Function
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@@ -199,6 +262,9 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
value(val, old) {
|
||||||
|
this.onChange && val !== old ? this.onChange(val) : null
|
||||||
|
},
|
||||||
options() {
|
options() {
|
||||||
this.$set('value', this.multiple ? [] : null)
|
this.$set('value', this.multiple ? [] : null)
|
||||||
},
|
},
|
||||||
@@ -207,10 +273,16 @@
|
|||||||
},
|
},
|
||||||
filteredOptions() {
|
filteredOptions() {
|
||||||
this.typeAheadPointer = 0;
|
this.typeAheadPointer = 0;
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a given option.
|
||||||
|
* @param {Object||String} option
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
select(option) {
|
select(option) {
|
||||||
if (! this.isOptionSelected(option) ) {
|
if (! this.isOptionSelected(option) ) {
|
||||||
if (this.multiple) {
|
if (this.multiple) {
|
||||||
@@ -230,6 +302,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.onAfterSelect(option)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called from this.select after each selection.
|
||||||
|
* @param {Object||String} option
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
onAfterSelect(option) {
|
||||||
if (!this.multiple) {
|
if (!this.multiple) {
|
||||||
this.open = !this.open
|
this.open = !this.open
|
||||||
}
|
}
|
||||||
@@ -237,6 +318,10 @@
|
|||||||
if( this.clearSearchOnSelect ) {
|
if( this.clearSearchOnSelect ) {
|
||||||
this.search = ''
|
this.search = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if( this.onChange ) {
|
||||||
|
// this.onChange(this.$get('value'))
|
||||||
|
// }
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -255,6 +340,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given option is currently selected.
|
||||||
|
* @param {Object||String} option
|
||||||
|
* @return {Boolean} True when selected || False otherwise
|
||||||
|
*/
|
||||||
isOptionSelected( option ) {
|
isOptionSelected( option ) {
|
||||||
if( this.multiple && this.value ) {
|
if( this.multiple && this.value ) {
|
||||||
return this.value.indexOf(option) !== -1
|
return this.value.indexOf(option) !== -1
|
||||||
@@ -263,6 +353,12 @@
|
|||||||
return this.value === option;
|
return this.value === option;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the selected option has option['value'] return it.
|
||||||
|
* Otherwise, return the entire option.
|
||||||
|
* @param {Object||String} option
|
||||||
|
* @return {Object||String}
|
||||||
|
*/
|
||||||
getOptionValue( option ) {
|
getOptionValue( option ) {
|
||||||
if( typeof option === 'object' && option.value ) {
|
if( typeof option === 'object' && option.value ) {
|
||||||
return option.value;
|
return option.value;
|
||||||
@@ -309,6 +405,7 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Select the option at the current typeAheadPointer position.
|
* Select the option at the current typeAheadPointer position.
|
||||||
|
* Optionally clear the search input on selection.
|
||||||
* @return {void}
|
* @return {void}
|
||||||
*/
|
*/
|
||||||
typeAheadSelect() {
|
typeAheadSelect() {
|
||||||
@@ -321,6 +418,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there is any text in the search input, remove it.
|
||||||
|
* Otherwise, blur the search input to close the dropdown.
|
||||||
|
* @return {[type]} [description]
|
||||||
|
*/
|
||||||
onEscape() {
|
onEscape() {
|
||||||
if( ! this.search.length ) {
|
if( ! this.search.length ) {
|
||||||
this.$els.search.blur()
|
this.$els.search.blur()
|
||||||
@@ -329,6 +431,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the value on Delete keypress when there is no
|
||||||
|
* text in the search input, & there's tags to delete
|
||||||
|
* @return {this.value}
|
||||||
|
*/
|
||||||
maybeDeleteValue() {
|
maybeDeleteValue() {
|
||||||
if( ! this.$els.search.value.length && this.value ) {
|
if( ! this.$els.search.value.length && this.value ) {
|
||||||
return this.multiple ? this.value.pop() : this.$set('value', null)
|
return this.multiple ? this.value.pop() : this.$set('value', null)
|
||||||
@@ -337,31 +444,57 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
cssClasses() {
|
|
||||||
|
/**
|
||||||
|
* Classes to be output on .dropdown
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
dropdownClasses() {
|
||||||
return {
|
return {
|
||||||
open: this.open,
|
open: this.open,
|
||||||
searchable: this.searchable
|
searchable: this.searchable
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the placeholder string if it's set
|
||||||
|
* & there is no value selected.
|
||||||
|
* @return {String} Placeholder text
|
||||||
|
*/
|
||||||
searchPlaceholder() {
|
searchPlaceholder() {
|
||||||
if( this.isValueEmpty && this.placeholder ) {
|
if( this.isValueEmpty && this.placeholder ) {
|
||||||
return this.placeholder;
|
return this.placeholder;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently available options, filtered
|
||||||
|
* by the search elements value.
|
||||||
|
* @return {[type]} [description]
|
||||||
|
*/
|
||||||
filteredOptions() {
|
filteredOptions() {
|
||||||
return this.$options.filters.filterBy(this.options, this.search)
|
return this.$options.filters.filterBy(this.options, this.search)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there aren't any options selected.
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
isValueEmpty() {
|
isValueEmpty() {
|
||||||
if( this.value ) {
|
if( this.value ) {
|
||||||
|
if( typeof this.value === 'object' ) {
|
||||||
|
return ! Object.keys(this.value).length
|
||||||
|
}
|
||||||
return ! this.value.length
|
return ! this.value.length
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the current value in array format.
|
||||||
|
* @return {Array}
|
||||||
|
*/
|
||||||
valueAsArray() {
|
valueAsArray() {
|
||||||
if( this.multiple ) {
|
if( this.multiple ) {
|
||||||
return this.value
|
return this.value
|
||||||
|
|||||||
@@ -103,16 +103,24 @@ describe('Select.vue', () => {
|
|||||||
options: ['one','two','three']
|
options: ['one','two','three']
|
||||||
}
|
}
|
||||||
}).$mount()
|
}).$mount()
|
||||||
|
|
||||||
var select = vm.$children[0]
|
var select = vm.$children[0]
|
||||||
expect(select.isValueEmpty).toEqual(true)
|
expect(select.isValueEmpty).toEqual(true)
|
||||||
|
|
||||||
select.$set('value', ['one'])
|
select.$set('value', ['one'])
|
||||||
expect(select.isValueEmpty).toEqual(false)
|
expect(select.isValueEmpty).toEqual(false)
|
||||||
select.$set('value', 'one')
|
|
||||||
select.$set('multiple', false)
|
select.$set('value', [{l:'f'}])
|
||||||
expect(select.isValueEmpty).toEqual(false)
|
expect(select.isValueEmpty).toEqual(false)
|
||||||
|
|
||||||
|
select.$set('value', 'one')
|
||||||
|
expect(select.isValueEmpty).toEqual(false)
|
||||||
|
|
||||||
|
select.$set('value', {label: 'foo', value: 'foo'})
|
||||||
|
expect(select.isValueEmpty).toEqual(false)
|
||||||
|
|
||||||
select.$set('value', '')
|
select.$set('value', '')
|
||||||
expect(select.isValueEmpty).toEqual(true)
|
expect(select.isValueEmpty).toEqual(true)
|
||||||
|
|
||||||
select.$set('value', null)
|
select.$set('value', null)
|
||||||
expect(select.isValueEmpty).toEqual(true)
|
expect(select.isValueEmpty).toEqual(true)
|
||||||
})
|
})
|
||||||
@@ -169,6 +177,33 @@ describe('Select.vue', () => {
|
|||||||
}).$mount()
|
}).$mount()
|
||||||
expect(vm.$children[0].$els.toggle.querySelector('.selected-tag').textContent).toContain('Baz')
|
expect(vm.$children[0].$els.toggle.querySelector('.selected-tag').textContent).toContain('Baz')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('can run a callback when the selection changes', (done) => {
|
||||||
|
const vm = new Vue({
|
||||||
|
template: '<div><v-select :on-change="foo" value="bar" :options="options"></v-select></div>',
|
||||||
|
components: { vSelect },
|
||||||
|
data: {
|
||||||
|
val: null,
|
||||||
|
options: ['foo','bar','baz']
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
foo(value) {
|
||||||
|
this.val = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).$mount()
|
||||||
|
|
||||||
|
vm.$children[0].select('foo')
|
||||||
|
Vue.nextTick(function() {
|
||||||
|
expect(vm.$get('val')).toEqual('foo')
|
||||||
|
|
||||||
|
vm.$children[0].$set('value', 'baz')
|
||||||
|
Vue.nextTick(function() {
|
||||||
|
expect(vm.$get('val')).toEqual('baz')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user