diff --git a/.gitignore b/.gitignore index 5bbc68f..9e75cf3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ node_modules npm-debug.log .idea test/unit/coverage -.coveralls.yml \ No newline at end of file +.coveralls.yml +.flowconfig diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js index 5715989..af1888e 100644 --- a/build/webpack.base.conf.js +++ b/build/webpack.base.conf.js @@ -19,8 +19,8 @@ module.exports = { 'src': path.resolve(__dirname, '../src'), 'assets': path.resolve(__dirname, '../docs/assets'), 'mixins': path.resolve(__dirname, '../src/mixins'), - 'components': path.resolve(__dirname, '../docs/components'), - 'vue$': 'vue/dist/vue.js' + 'components': path.resolve(__dirname, '../src/components'), + 'vue$': 'vue/dist/vue.common.js', } }, resolveLoader: { diff --git a/package.json b/package.json index d6e48c3..972fbe8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-select", - "version": "1.3.3", + "version": "2.0.0-alpha", "description": "A native Vue.js component that provides similar functionality to Select2 without the overhead of jQuery.", "author": "Jeff Sagal ", "private": false, @@ -62,7 +62,7 @@ "vue": "^2.0.3", "vue-hot-reload-api": "^1.2.0", "vue-html-loader": "^1.2.3", - "vue-loader": "^9.6.0", + "vue-loader": "^9.9.5", "vue-resource": "^1.0.3", "vue-style-loader": "^1.0.0", "vuex": "^0.6.3", diff --git a/src/components/Select.vue b/src/components/Select.vue index 81174b0..2c8b953 100644 --- a/src/components/Select.vue +++ b/src/components/Select.vue @@ -202,7 +202,7 @@ -
Loading...
+
Loading...
@@ -237,7 +237,7 @@ /** * Contains the currently selected value. Very similar to a * `value` attribute on an . You can listen for changes - * using 'change' event using v-on + * using 'change' event using v-on * @type {Object||String||null} */ value: { @@ -349,8 +349,12 @@ * @type {Function} * @default {null} */ - onChange: Function, - + onChange: { + type: Function, + default: function(val) { + this.$emit('input', val) + } + }, /** * Enable/disable creating options from searchInput. @@ -378,7 +382,7 @@ createOption: { type: Function, default: function (newOption) { - if (typeof this.currentOptions[0] === 'object') { + if (typeof this.mutableOptions[0] === 'object') { return {[this.label]: newOption} } return newOption @@ -399,45 +403,80 @@ return { search: '', open: false, - currentSelection: null, - currentOptions: [], - showLoading: false + mutableValue: null, + mutableOptions: [], + mutableLoading: false } }, watch: { - value(val, old) { - this.currentSelection = val + /** + * When the value prop changes, update + * the internal mutableValue. + * @param {mixed} val + * @return {void} + */ + value(val) { + this.mutableValue = val }, - currentSelection(val, old) { + + /** + * Maybe run the onChange callback. + * @param {string|object} val + * @param {string|object} old + * @return {void} + */ + mutableValue(val, old) { if (this.multiple) { this.onChange ? this.onChange(val) : null - this.$emit('change', val) } else { - if(val !== old) { - this.onChange? this.onChange(val) : null - this.$emit('change', val) - } + this.onChange && val !== old ? this.onChange(val) : null } }, + + /** + * When options change, update + * the internal mutableOptions. + * @param {array} val + * @return {void} + */ options(val) { - this.currentOptions = val + this.mutableOptions = val }, - currentOptions() { + + /** + * Maybe reset the mutableValue + * when mutableOptions change. + * @return {[type]} [description] + */ + mutableOptions() { if (!this.taggable && this.resetOnOptionsChange) { - this.currentSelection = this.multiple ? [] : null + this.mutableValue = this.multiple ? [] : null } }, - multiple(val) { - this.currentSelection = val ? [] : null + + /** + * Always reset the mutableValue when + * the multiple prop changes. + * @param {Boolean} val + * @return {void} + */ + multiple(val) { + this.mutableValue = val ? [] : null } }, + created() { + this.mutableValue = this.value + this.mutableOptions = this.options.slice(0) + this.mutableLoading = this.loading + }, + methods: { /** * Select a given option. - * @param {Object||String} option + * @param {Object|String} option * @return {void} */ select(option) { @@ -448,19 +487,18 @@ option = this.createOption(option) if (this.pushTags) { - console.log("adding " + option +" to "+ this.currentOptions) - this.currentOptions.push(option) + this.mutableOptions.push(option) } } if (this.multiple) { - if (!this.currentSelection) { - this.currentSelection = [option] + if (!this.mutableValue) { + this.mutableValue = [option] } else { - this.currentSelection.push(option) + this.mutableValue.push(option) } } else { - this.currentSelection = option + this.mutableValue = option } } @@ -469,27 +507,27 @@ /** * De-select a given option. - * @param {Object||String} option + * @param {Object|String} option * @return {void} */ deselect(option) { if (this.multiple) { let ref = -1 - this.currentSelection.forEach((val) => { + this.mutableValue.forEach((val) => { if (val === option || typeof val === 'object' && val[this.label] === option[this.label]) { ref = val } }) - var index = this.currentSelection.indexOf(ref) - this.currentSelection.splice(index, 1) + var index = this.mutableValue.indexOf(ref) + this.mutableValue.splice(index, 1) } else { - this.currentSelection = null + this.mutableValue = null } }, /** * Called from this.select after each selection. - * @param {Object||String} option + * @param {Object|String} option * @return {void} */ onAfterSelect(option) { @@ -521,13 +559,13 @@ /** * Check if the given option is currently selected. - * @param {Object||String} option - * @return {Boolean} True when selected || False otherwise + * @param {Object|String} option + * @return {Boolean} True when selected | False otherwise */ isOptionSelected(option) { - if (this.multiple && this.currentSelection) { + if (this.multiple && this.mutableValue) { let selected = false - this.currentSelection.forEach(opt => { + this.mutableValue.forEach(opt => { if (typeof opt === 'object' && opt[this.label] === option[this.label]) { selected = true } else if (opt === option) { @@ -537,7 +575,7 @@ return selected } - return this.currentSelection === option + return this.mutableValue === option }, /** @@ -559,14 +597,14 @@ * @return {this.value} */ maybeDeleteValue() { - if (!this.$refs.search.value.length && this.currentSelection) { - return this.multiple ? this.currentSelection.pop() : this.currentSelection = null + if (!this.$refs.search.value.length && this.mutableValue) { + return this.multiple ? this.mutableValue.pop() : this.mutableValue = null } }, /** * Determine if an option exists - * within this.currentOptions array. + * within this.mutableOptions array. * * @param {Object || String} option * @return {boolean} @@ -574,7 +612,7 @@ optionExists(option) { let exists = false - this.currentOptions.forEach(opt => { + this.mutableOptions.forEach(opt => { if (typeof opt === 'object' && opt[this.label] === option) { exists = true } else if (opt === option) { @@ -596,7 +634,7 @@ return { open: this.open, searchable: this.searchable, - loading: this.showLoading + loading: this.mutableLoading } }, @@ -620,7 +658,13 @@ * @return {array} */ filteredOptions() { - let options = this.$options.filters.filterBy?this.$options.filters.filterBy(this.currentOptions, this.search):this.currentOptions + + let options = this.mutableOptions.filter((option) => { + if( typeof option === 'object' ) { + return option[this.label].indexOf(this.search) > -1 + } + return option.indexOf(this.search) > -1 + }) if (this.taggable && this.search.length && !this.optionExists(this.search)) { options.unshift(this.search) } @@ -632,11 +676,11 @@ * @return {Boolean} */ isValueEmpty() { - if (this.currentSelection) { - if (typeof this.currentSelection === 'object') { - return !Object.keys(this.currentSelection).length + if (this.mutableValue) { + if (typeof this.mutableValue === 'object') { + return !Object.keys(this.mutableValue).length } - return !this.currentSelection.length + return !this.mutableValue.length } return true; @@ -648,19 +692,14 @@ */ valueAsArray() { if (this.multiple) { - return this.currentSelection - } else if (this.currentSelection) { - return [this.currentSelection] + return this.mutableValue + } else if (this.mutableValue) { + return [this.mutableValue] } return [] } }, - created: function() { - this.currentSelection = this.value - this.currentOptions = this.options.slice(0) - this.showLoading = this.loading - } } diff --git a/src/dev.js b/src/dev.js index 4cd2518..cae4133 100644 --- a/src/dev.js +++ b/src/dev.js @@ -1,5 +1,6 @@ import Vue from 'vue' -import vSelect from '../src/components/Select.vue' +import vSelect from './components/Select.vue' +import countries from '../old_docs/data/advanced.js' Vue.component('v-select', vSelect) @@ -7,5 +8,10 @@ Vue.config.devtools = true /* eslint-disable no-new */ new Vue({ - el: 'body' + el: '#app', + data: { + placeholder: "placeholder", + value: null, + options: countries + } }) diff --git a/src/mixins/ajax.js b/src/mixins/ajax.js index 65d127e..69ebe1a 100644 --- a/src/mixins/ajax.js +++ b/src/mixins/ajax.js @@ -64,4 +64,4 @@ module.exports = { return this.showLoading = toggle } } -} \ No newline at end of file +} diff --git a/test/unit/specs/Select.spec.js b/test/unit/specs/Select.spec.js index 81ea301..e423b54 100644 --- a/test/unit/specs/Select.spec.js +++ b/test/unit/specs/Select.spec.js @@ -64,11 +64,11 @@ describe('Select.vue', () => { template: '
', components: {vSelect}, data: { - value: ['one'], + value: 'one', options: ['one', 'two', 'three'] } }).$mount() - expect(vm.$children[0].currentSelection).toEqual(vm.value) + expect(vm.$children[0].mutableValue).toEqual(vm.value) }) it('can accept an array of objects and pre-selected value (single)', () => { @@ -80,7 +80,7 @@ describe('Select.vue', () => { options: [{label: 'This is Foo', value: 'foo'}, {label: 'This is Bar', value: 'bar'}] } }).$mount() - expect(vm.$children[0].currentSelection).toEqual(vm.value) + expect(vm.$children[0].mutableValue).toEqual(vm.value) }) it('can accept an array of objects and pre-selected values (multiple)', () => { @@ -92,7 +92,7 @@ describe('Select.vue', () => { options: [{label: 'This is Foo', value: 'foo'}, {label: 'This is Bar', value: 'bar'}] } }).$mount() - expect(vm.$children[0].currentSelection).toEqual(vm.value) + expect(vm.$children[0].mutableValue).toEqual(vm.value) }) it('can deselect a pre-selected object', () => { @@ -104,7 +104,7 @@ describe('Select.vue', () => { } }).$mount() vm.$children[0].select({label: 'This is Foo', value: 'foo'}) - expect(vm.$children[0].currentSelection.length).toEqual(1) + expect(vm.$children[0].mutableValue.length).toEqual(1) }) it('can deselect a pre-selected string', () => { @@ -116,7 +116,7 @@ describe('Select.vue', () => { } }).$mount() vm.$children[0].select('foo') - expect(vm.$children[0].currentSelection.length).toEqual(1) + expect(vm.$children[0].mutableValue.length).toEqual(1) }) it('can determine if the value prop is empty', () => { @@ -164,10 +164,10 @@ describe('Select.vue', () => { vm.multiple = false Vue.nextTick(() => { - expect(vm.$children[0].currentSelection).toEqual(null) + expect(vm.$children[0].mutableValue).toEqual(null) vm.multiple = true Vue.nextTick(() => { - expect(vm.$children[0].currentSelection).toEqual([]) + expect(vm.$children[0].mutableValue).toEqual([]) done() }) }) @@ -175,20 +175,20 @@ describe('Select.vue', () => { it('can retain values present in a new array of options', () => { const vm = new Vue({ - template: '
', + template: '
', components: {vSelect}, data: { value: ['one'], options: ['one', 'two', 'three'] } }).$mount() - vm.$children[0].options = ['one', 'five', 'six'] - expect(vm.$children[0].currentSelection).toEqual(['one']) + vm.options = ['one', 'five', 'six'] + expect(vm.$children[0].mutableValue).toEqual(['one']) }) it('can determine if an object is already selected', () => { const vm = new Vue({ - template: '
', + template: '
', components: {vSelect}, data: { value: [{label: 'one'}], @@ -199,54 +199,57 @@ describe('Select.vue', () => { expect(vm.$children[0].isOptionSelected({label: 'one'})).toEqual(true) }) + it('can use v-model syntax for a two way binding to a parent component', (done) => { + const vm = new Vue({ + template: '
', + components: {vSelect}, + data: { + value: 'foo', + options: ['foo','bar','baz'] + } + }).$mount() + + expect(vm.$children[0].value).toEqual('foo') + expect(vm.$children[0].mutableValue).toEqual('foo') + + vm.$children[0].mutableValue = 'bar' + + Vue.nextTick(() => { + expect(vm.value).toEqual('bar') + done() + }) + }) + describe('change Event', () => { - it('can run a callback when the selection changes', (done) => { + it('will trigger the input event when the selection changes', (done) => { const vm = new Vue({ - template: `
`, - components: {vSelect}, - methods: { - cb(val) { - console.log("Value Changed to "+val) - } + template: `
`, + data: { + foo: '' } }).$mount() - spyOn(vm, 'cb') - - vm.$children[0].select('bar') + vm.$refs.select.select('bar') Vue.nextTick(() => { - expect(vm.cb).toHaveBeenCalledWith('bar') - vm.$children[0].select('baz') - - Vue.nextTick(() => { - expect(vm.cb).toHaveBeenCalledWith('baz') - done() - }) + expect(vm.foo).toEqual('bar') + done() }) }) it('should run change when multiple is true and the value changes', (done) => { const vm = new Vue({ - template: `
`, - methods: { - cb(val) { - } + template: `
`, + data: { + foo: '' } }).$mount() - spyOn(vm, 'cb') - - vm.$children[0].select('bar') + vm.$refs.select.select('bar') Vue.nextTick(() => { - expect(vm.cb).toHaveBeenCalledWith(['foo','bar']) - vm.$children[0].select('baz') - - Vue.nextTick(() => { - expect(vm.cb).toHaveBeenCalledWith(['foo','bar','baz']) - done() - }) + expect(vm.foo).toEqual(['foo','bar']) + done() }) }) @@ -487,12 +490,14 @@ describe('Select.vue', () => { return {top: 0, bottom: 1} } }) + + let mock = Mock({ + '../mixins/pointerScroll': {methods} + }) const vm = new Vue({ - template: '
', + template: `
`, components: { - 'v-select': Mock({ - '../mixins/pointerScroll': {methods} - }) + 'v-select': mock }, }).$mount() @@ -525,7 +530,7 @@ describe('Select.vue', () => { describe('Removing values', () => { it('can remove the given tag when its close icon is clicked', (done) => { const vm = new Vue({ - template: '
', + template: '
', components: {vSelect}, data: { value: ['one'], @@ -534,7 +539,7 @@ describe('Select.vue', () => { }).$mount() vm.$children[0].$refs.toggle.querySelector('.close').click() Vue.nextTick(() => { - expect(vm.$children[0].currentSelection).toEqual([]) + expect(vm.$children[0].mutableValue).toEqual([]) done() }) }) @@ -542,7 +547,7 @@ describe('Select.vue', () => { it('should remove the last item in the value array on delete keypress when multiple is true', () => { const vm = new Vue({ - template: '
', + template: '
', components: {vSelect}, data: { value: ['one', 'two'], @@ -551,13 +556,13 @@ describe('Select.vue', () => { }).$mount() vm.$children[0].maybeDeleteValue() Vue.nextTick(() => { - expect(vm.$children[0].currentSelection).toEqual(['one']) + expect(vm.$children[0].mutableValue).toEqual(['one']) }) }) it('should set value to null on delete keypress when multiple is false', () => { const vm = new Vue({ - template: '
', + template: '
', components: {vSelect}, data: { value: 'one', @@ -566,7 +571,7 @@ describe('Select.vue', () => { }).$mount() vm.$children[0].maybeDeleteValue() Vue.nextTick(() => { - expect(vm.$children[0].currentSelection).toEqual(null) + expect(vm.$children[0].mutableValue).toEqual(null) }) }) }) @@ -574,7 +579,7 @@ describe('Select.vue', () => { describe('Labels', () => { it('can generate labels using a custom label key', () => { const vm = new Vue({ - template: '
', + template: '
', components: {vSelect}, data: { value: [{name: 'Baz'}], @@ -594,13 +599,11 @@ describe('Select.vue', () => { }).$mount() expect(vm.$children[0].searchPlaceholder).toEqual('foo') - vm.$children[0].currentSelection = {label: 'one'} + vm.$children[0].mutableValue = {label: 'one'} Vue.nextTick(() => { expect(vm.$children[0].searchPlaceholder).not.toBeDefined() done() }) - - // expect(vm.$children[0].searchPlaceholder()).toEqual('foo') }) }) @@ -670,7 +673,7 @@ describe('Select.vue', () => { searchSubmit(vm, 'three') Vue.nextTick(() => { - expect(vm.$children[0].currentSelection).toEqual(['one', 'three']) + expect(vm.$children[0].mutableValue).toEqual(['one', 'three']) done() }) }) @@ -687,7 +690,7 @@ describe('Select.vue', () => { searchSubmit(vm, 'two') Vue.nextTick(() => { - expect(vm.$children[0].currentSelection).toEqual([{label: 'one'}, {label: 'two'}]) + expect(vm.$children[0].mutableValue).toEqual([{label: 'one'}, {label: 'two'}]) done() }) }) @@ -703,7 +706,7 @@ describe('Select.vue', () => { }).$mount() searchSubmit(vm, 'three') - expect(vm.$children[0].currentOptions).toEqual(['one', 'two', 'three']) + expect(vm.$children[0].mutableOptions).toEqual(['one', 'two', 'three']) }) it('wont add a freshly created option/tag to the options list when pushTags is false', () => { @@ -717,7 +720,7 @@ describe('Select.vue', () => { }).$mount() searchSubmit(vm, 'three') - expect(vm.$children[0].currentOptions).toEqual(['one', 'two']) + expect(vm.$children[0].mutableOptions).toEqual(['one', 'two']) }) it('should select an existing option if the search string matches a string from options', (done) => { @@ -735,7 +738,7 @@ describe('Select.vue', () => { searchSubmit(vm) Vue.nextTick(() => { - expect(vm.$children[0].currentSelection[0]).toBe(two) + expect(vm.$children[0].mutableValue[0]).toBe(two) done() }) }) @@ -757,7 +760,7 @@ describe('Select.vue', () => { // This needs to be wrapped in nextTick() twice so that filteredOptions can // calculate after setting the search text, and move the typeAheadPointer index to 0. Vue.nextTick(() => { - expect(vm.$children[0].currentSelection.label).toBe(two.label) + expect(vm.$children[0].mutableValue.label).toBe(two.label) done() }) }) @@ -771,9 +774,9 @@ describe('Select.vue', () => { options: [{label: 'one'}] } }).$mount() - vm.$children[0].options = [{label: 'two'}] + vm.$children[0].mutableOptions = [{label: 'two'}] Vue.nextTick(() => { - expect(vm.$children[0].currentSelection).toEqual([{label: 'one'}]) + expect(vm.$children[0].mutableValue).toEqual([{label: 'one'}]) done() }) }) @@ -795,17 +798,20 @@ describe('Select.vue', () => { it('should trigger the onSearch callback when the search text changes', (done) => { const vm = new Vue({ template: '
', + data: { + called: false + }, methods: { - foo() { + foo(val) { + this.called = val } } }).$mount() - spyOn(vm.$refs.select, 'onSearch') vm.$refs.select.search = 'foo' Vue.nextTick(() => { - expect(vm.$refs.select.onSearch).toHaveBeenCalledWith('foo', vm.$refs.select.toggleLoading) + expect(vm.called).toEqual('foo') done() }) }) @@ -813,18 +819,22 @@ describe('Select.vue', () => { it('should not trigger the onSearch callback if the search text is empty', (done) => { const vm = new Vue({ template: '
', + data: { called: false }, methods: { - foo() { + foo(val) { + this.called = ! this.called } } }).$mount() - spyOn(vm.$refs.select, 'onSearch') - vm.$refs.select.search = '' - + vm.$refs.select.search = 'foo' Vue.nextTick(() => { - expect(vm.$refs.select.onSearch).not.toHaveBeenCalled() - done() + expect(vm.called).toBe(true) + vm.$refs.select.search = '' + Vue.nextTick(() => { + expect(vm.called).toBe(true) + done() + }) }) }) @@ -874,9 +884,9 @@ describe('Select.vue', () => { options: ['one', 'two', 'three'] } }).$mount() - vm.$children[0].options = ['four', 'five', 'six'] + vm.$children[0].mutableOptions = ['four', 'five', 'six'] Vue.nextTick(() => { - expect(vm.$children[0].currentSelection).toEqual('one') + expect(vm.$children[0].mutableValue).toEqual('one') done() }) }) @@ -890,9 +900,9 @@ describe('Select.vue', () => { options: ['one', 'two', 'three'] } }).$mount() - vm.$children[0].options = ['four', 'five', 'six'] + vm.$children[0].mutableOptions = ['four', 'five', 'six'] Vue.nextTick(() => { - expect(vm.$children[0].currentSelection).toEqual(null) + expect(vm.$children[0].mutableValue).toEqual(null) done() }) })