2
0
mirror of https://github.com/tenrok/vue-select.git synced 2026-06-07 07:12:23 +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:
Jeff Sagal
2016-03-16 10:56:38 -07:00
parent 2449628d10
commit 9f58c32a52
2 changed files with 182 additions and 14 deletions
+144 -11
View File
@@ -104,7 +104,7 @@
</style>
<template>
<div class="dropdown" :class="cssClasses">
<div class="dropdown" :class="dropdownClasses">
<div v-el:toggle @mousedown.prevent="toggleDropdown" class="dropdown-toggle clearfix" type="button">
<span class="form-control" v-if="!searchable && isValueEmpty">
{{ placeholder }}
@@ -121,16 +121,17 @@
v-el:search
v-show="searchable"
v-model="search"
@keydown.delete="maybeDeleteValue"
@keydown.esc="onEscape"
@keydown.up.prevent="typeAheadUp"
@keydown.down.prevent="typeAheadDown"
@keydown.enter.prevent="typeAheadSelect"
@keyup.delete="maybeDeleteValue"
@keyup.esc="onEscape"
@keyup.up.prevent="typeAheadUp"
@keyup.down.prevent="typeAheadDown"
@keyup.enter.prevent="typeAheadSelect"
@blur="open = false"
@focus="open = true"
type="search"
class="form-control"
:placeholder="searchPlaceholder"
:style="{ width: isValueEmpty ? '100%' : 'auto' }"
>
<i v-el:open-indicator role="presentation" class="open-indicator glyphicon-chevron-down glyphicon"></i>
@@ -152,42 +153,104 @@
<script>
export default {
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: {
twoway: true,
required: true
default: null
},
/**
* 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: {
type: Array,
default() { return [] },
},
/**
* Sets the max-height property on the dropdown list.
* @deprecated
* @type {String}
*/
maxHeight: {
type: String,
default: '400px'
},
/**
* Enable/disable filtering the options.
* @type {Boolean}
*/
searchable: {
type: Boolean,
default: true
},
/**
* Equivalent to the `multiple` attribute on a `<select>` input.
* @type {Object}
*/
multiple: {
type: Boolean,
default: false
},
/**
* Equivalent to the `placeholder` attribute on an `<input>`.
* @type {Object}
*/
placeholder: {
type: String,
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: {
type: String,
default: 'expand'
},
/**
* Enables/disables clearing the search text when an option is selected.
* @type {Boolean}
*/
clearSearchOnSelect: {
type: Boolean,
default: true
},
/**
* Tells vue-select what key to use when generating option
* labels when each `option` is an object.
* @type {String}
*/
label: {
type: String,
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() {
@@ -199,6 +262,9 @@
},
watch: {
value(val, old) {
this.onChange && val !== old ? this.onChange(val) : null
},
options() {
this.$set('value', this.multiple ? [] : null)
},
@@ -207,10 +273,16 @@
},
filteredOptions() {
this.typeAheadPointer = 0;
}
},
},
methods: {
/**
* Select a given option.
* @param {Object||String} option
* @return {void}
*/
select(option) {
if (! this.isOptionSelected(option) ) {
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) {
this.open = !this.open
}
@@ -237,6 +318,10 @@
if( this.clearSearchOnSelect ) {
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 ) {
if( this.multiple && this.value ) {
return this.value.indexOf(option) !== -1
@@ -263,6 +353,12 @@
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 ) {
if( typeof option === 'object' && option.value ) {
return option.value;
@@ -309,6 +405,7 @@
/**
* Select the option at the current typeAheadPointer position.
* Optionally clear the search input on selection.
* @return {void}
*/
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() {
if( ! this.search.length ) {
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() {
if( ! this.$els.search.value.length && this.value ) {
return this.multiple ? this.value.pop() : this.$set('value', null)
@@ -337,31 +444,57 @@
},
computed: {
cssClasses() {
/**
* Classes to be output on .dropdown
* @return {Object}
*/
dropdownClasses() {
return {
open: this.open,
searchable: this.searchable
}
},
/**
* Return the placeholder string if it's set
* & there is no value selected.
* @return {String} Placeholder text
*/
searchPlaceholder() {
if( this.isValueEmpty && this.placeholder ) {
return this.placeholder;
}
},
/**
* The currently available options, filtered
* by the search elements value.
* @return {[type]} [description]
*/
filteredOptions() {
return this.$options.filters.filterBy(this.options, this.search)
},
/**
* Check if there aren't any options selected.
* @return {Boolean}
*/
isValueEmpty() {
if( this.value ) {
if( typeof this.value === 'object' ) {
return ! Object.keys(this.value).length
}
return ! this.value.length
}
return true;
},
/**
* Return the current value in array format.
* @return {Array}
*/
valueAsArray() {
if( this.multiple ) {
return this.value
+38 -3
View File
@@ -103,16 +103,24 @@ describe('Select.vue', () => {
options: ['one','two','three']
}
}).$mount()
var select = vm.$children[0]
expect(select.isValueEmpty).toEqual(true)
select.$set('value', ['one'])
expect(select.isValueEmpty).toEqual(false)
select.$set('value', 'one')
select.$set('multiple', false)
select.$set('value', [{l:'f'}])
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', '')
expect(select.isValueEmpty).toEqual(true)
select.$set('value', null)
expect(select.isValueEmpty).toEqual(true)
})
@@ -169,6 +177,33 @@ describe('Select.vue', () => {
}).$mount()
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()
})
})
})
})