From 6c57415aa666a34306ff18b28112ce6e20b611ff Mon Sep 17 00:00:00 2001 From: Adam Berecz Date: Thu, 16 Dec 2021 16:12:36 +0100 Subject: [PATCH] feat: can add new options not only tags #150 --- README.md | 12 +- src/Multiselect.d.ts | 4 + src/Multiselect.vue | 31 ++- src/composables/useKeyboard.js | 46 +++- src/composables/useOptions.js | 59 +++-- tests/unit/composables/useKeyboard.spec.js | 278 ++++++++++++++++++++- tests/unit/composables/useOptions.spec.js | 196 ++++++++++++++- 7 files changed, 580 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 81262d4..ea1b0d7 100644 --- a/README.md +++ b/README.md @@ -240,9 +240,12 @@ Join our [Discord channel](https://discord.gg/WhX2nG6GTQ) or [open an issue](htt | **filterResults** | `boolean` | `true` | Whether option list should be filtered by search query. This may be set to `false` if you are handling filtering manually when returning async options. | | **minChars** | `number` | `0` | The minimum number of characters that should be typed to refresh async option list. If `0` it will refresh even when the search field becomes empty. | | **resolveOnLoad** | `boolean` | `true` | Whether async options should be loaded initially (with an empty query). This should be `true` if you are planning to load non-object value(s) initially while using async options (to fetch matching objects for values). | -| **appendNewTag** | `boolean` | `true` | Whether it should append new tag automatically to option list when using `tags` mode with `createTag`. If set to `false` you need to take care of appending a new tag to the provided `:options` list upon `@tag` event. | -| **createTag** | `boolean` | `false` | Whether it should allow creating new tag based on search query when using `tags` mode. | -| **addTagOn** | `array` | `['enter']` | The list of keys that creates a new tag while typing in the search field when having `createTag` enabled. Possible values: `'enter'\|'space'\|'tab'\|';'\|','`. | +| **appendNewTag** | `boolean` | `true` | **Deprecated 2.3.0: use `appendNewOption` instead.**
Whether it should append new tag automatically to option list when using `tags` mode with `createTag`. If set to `false` you need to take care of appending a new tag to the provided `:options` list upon `@tag` event. | +| **createTag** | `boolean` | `false` | **Deprecated 2.3.0: use `createOption` instead.**
Whether it should allow creating new tags based on search query when using `tags` mode. | +| **addTagOn** | `array` | `['enter']` | **Deprecated 2.3.0: use `addOptionOn` instead.**
The list of keys that creates a new tag while typing in the search field when having `createTag` enabled. Possible values: `'enter'\|'space'\|'tab'\|';'\|','`. | +| **appendNewOption** | `boolean` | `true` | Whether it should append new option automatically to option list when `searchable` and `createTag` are enabled. If set to `false` you need to take care of appending a new option to the provided `:options` list upon `@option` event. | +| **createOption** | `boolean` | `false` | Whether it should allow creating new options based on search query when `searchable` is enabled. | +| **addOptionOn** | `array` | `['enter']` | The list of keys that creates a new option while typing in the search field when having `createOption` enabled. Possible values: `'enter'\|'space'\|'tab'\|';'\|','`. | | **hideSelected** | `boolean` | `true` | Whether selected options should be excluded from the option list when using `multiple` or `tags` mode. | | **showOptions** | `boolean` | `true` | Whether option list should be displayed. Can be used to create free-typed tags. | | **object** | `boolean` | `false` | Whether the value should be stored as an object.
If **false**:
`value: ['js','jsx','ts']`
If **true**:
`value: [`
  `{value:'js',label:'Javascript'},`
  `{value:'jsx',label:'JSX'},`
  `{value:'ts',label:'Typescript'}`
`]` | @@ -290,7 +293,8 @@ mounted() { | **@open** | | Emitted after opening the option list. | | **@search-change** | `query` | Emitted after a character is typed. | | **@select** | `option` | Emitted after an option or tag is selected. | -| **@tag** | `query` | Emitted after enter is hit when a new tag is being created. | +| **@tag** | `query` | **Deprecated 2.3.0: use `@option` instead**. Emitted after enter is hit when a new tag is being created. | +| **@option** | `query` | Emitted after enter is hit when a new option is being created. | | **@clear** | | Emitted when the options are cleared. | | **@paste** | `Event` | Emitted when value is pasted into the search field. | diff --git a/src/Multiselect.d.ts b/src/Multiselect.d.ts index 894b8c5..54dc55a 100644 --- a/src/Multiselect.d.ts +++ b/src/Multiselect.d.ts @@ -29,8 +29,11 @@ declare class Multiselect extends Vue { minChars?: number; resolveOnLoad?: boolean; appendNewTag?: boolean; + appendNewOption?: boolean; createTag?: boolean; + createOption?: boolean; addTagOn?: string[]; + addOptionOn?: string[]; hideSelected?: boolean; showOptions?: boolean; object?: boolean; @@ -54,6 +57,7 @@ declare class Multiselect extends Vue { $emit(eventName: 'remove', e: {originalEvent: Event, value: any, option: any}): this; $emit(eventName: 'search-change', e: {originalEvent: Event, query: string}): this; $emit(eventName: 'tag', e: {originalEvent: Event, query: string}): this; + $emit(eventName: 'option', e: {originalEvent: Event, query: string}): this; $emit(eventName: 'paste', e: {originalEvent: Event}): this; $emit(eventName: 'open'): this; $emit(eventName: 'close'): this; diff --git a/src/Multiselect.vue b/src/Multiselect.vue index 1b01f73..da1298a 100644 --- a/src/Multiselect.vue +++ b/src/Multiselect.vue @@ -209,7 +209,7 @@ name: 'Multiselect', emits: [ 'open', 'close', 'select', 'deselect', - 'input', 'search-change', 'tag', 'update:modelValue', + 'input', 'search-change', 'tag', 'option', 'update:modelValue', 'change', 'clear' ], props: { @@ -281,12 +281,32 @@ createTag: { type: Boolean, required: false, - default: false, + default: undefined, + }, + createOption: { + type: Boolean, + required: false, + default: undefined, }, appendNewTag: { type: Boolean, required: false, - default: true, + default: undefined, + }, + appendNewOption: { + type: Boolean, + required: false, + default: undefined, + }, + addTagOn: { + type: Array, + required: false, + default: undefined, + }, + addOptionOn: { + type: Array, + required: false, + default: undefined, }, caret: { type: Boolean, @@ -367,11 +387,6 @@ required: false, default: true, }, - addTagOn: { - type: Array, - required: false, - default: () => (['enter']), - }, required: { type: Boolean, required: false, diff --git a/src/composables/useKeyboard.js b/src/composables/useKeyboard.js index 58a2208..34cf1c9 100644 --- a/src/composables/useKeyboard.js +++ b/src/composables/useKeyboard.js @@ -1,10 +1,11 @@ -import { toRefs } from 'composition-api' +import { toRefs, computed } from 'composition-api' export default function useKeyboard (props, context, dep) { const { - mode, addTagOn, createTag, openDirection, searchable, + mode, addTagOn, openDirection, searchable, showOptions, valueProp, groups: groupped, + addOptionOn: addOptionOn_, createTag, createOption: createOption_, } = toRefs(props) // ============ DEPENDENCIES ============ @@ -19,6 +20,25 @@ export default function useKeyboard (props, context, dep) const blur = dep.blur const fo = dep.fo + // ============== COMPUTED ============== + + // no export + const createOption = computed(() => { + return createTag.value || createOption_.value || false + }) + + // no export + const addOptionOn = computed(() => { + if (addTagOn.value !== undefined) { + return addTagOn.value + } + else if (addOptionOn_.value !== undefined) { + return addOptionOn_.value + } + + return ['enter'] + }) + // =============== METHODS ============== // no export @@ -28,7 +48,7 @@ export default function useKeyboard (props, context, dep) // In such case we need to set the pointer manually to the // first option, which equals to the option created from // the search value. - if (mode.value === 'tags' && !showOptions.value && createTag.value && searchable.value && !groupped.value) { + if (mode.value === 'tags' && !showOptions.value && createOption.value && searchable.value && !groupped.value) { setPointer(fo.value[fo.value.map(o => o[valueProp.value]).indexOf(search.value)]) } } @@ -54,7 +74,7 @@ export default function useKeyboard (props, context, dep) case 'Enter': e.preventDefault() - if (mode.value === 'tags' && addTagOn.value.indexOf('enter') === -1 && createTag.value) { + if (addOptionOn.value.indexOf('enter') === -1 && createOption.value) { return } @@ -63,11 +83,19 @@ export default function useKeyboard (props, context, dep) break case ' ': - if (searchable.value && mode.value !== 'tags' && !createTag.value) { + if (!createOption.value && !searchable.value) { + e.preventDefault() + + preparePointer() + selectPointer() return } - if (mode.value === 'tags' && ((addTagOn.value.indexOf('space') === -1 && createTag.value) || !createTag.value)) { + if (!createOption.value) { + return false + } + + if (addOptionOn.value.indexOf('space') === -1 && createOption.value) { return } @@ -80,11 +108,7 @@ export default function useKeyboard (props, context, dep) case 'Tab': case ';': case ',': - if (mode.value !== 'tags') { - return - } - - if (addTagOn.value.indexOf(e.key.toLowerCase()) === -1 || !createTag.value) { + if (addOptionOn.value.indexOf(e.key.toLowerCase()) === -1 || !createOption.value) { return } diff --git a/src/composables/useOptions.js b/src/composables/useOptions.js index 18c009f..5278f31 100644 --- a/src/composables/useOptions.js +++ b/src/composables/useOptions.js @@ -7,11 +7,11 @@ import arraysEqual from './../utils/arraysEqual' export default function useOptions (props, context, dep) { const { - options, mode, trackBy: trackBy_, limit, hideSelected, createTag, label, - appendNewTag, multipleLabel, object, loading, delay, resolveOnLoad, + options, mode, trackBy: trackBy_, limit, hideSelected, createTag, createOption: createOption_, label, + appendNewTag, appendNewOption: appendNewOption_, multipleLabel, object, loading, delay, resolveOnLoad, minChars, filterResults, clearOnSearch, clearOnSelect, valueProp, canDeselect, max, strict, closeOnSelect, groups: groupped, groupLabel, - groupOptions, groupHideEmpty, groupSelect, + groupOptions, groupHideEmpty, groupSelect, } = toRefs(props) // ============ DEPENDENCIES ============ @@ -41,6 +41,22 @@ export default function useOptions (props, context, dep) // ============== COMPUTED ============== + // no export + const createOption = computed(() => { + return createTag.value || createOption_.value || false + }) + + // no export + const appendNewOption = computed(() => { + if (appendNewTag.value !== undefined) { + return appendNewTag.value + } else if (appendNewOption_.value !== undefined) { + return appendNewOption_.value + } + + return true + }) + // no export // extendedOptions const eo = computed(() => { @@ -89,8 +105,8 @@ export default function useOptions (props, context, dep) const fo = computed(() => { let options = eo.value - if (createdTag.value.length) { - options = createdTag.value.concat(options) + if (createdOption.value.length) { + options = createdOption.value.concat(options) } options = filterOptions(options) @@ -120,7 +136,7 @@ export default function useOptions (props, context, dep) }) const noOptions = computed(() => { - return !eo.value.length && !resolving.value && !createdTag.value.length + return !eo.value.length && !resolving.value && !createdOption.value.length }) @@ -129,8 +145,8 @@ export default function useOptions (props, context, dep) }) // no export - const createdTag = computed(() => { - if (createTag.value === false || !search.value) { + const createdOption = computed(() => { + if (createOption.value === false || !search.value) { return [] } @@ -270,6 +286,8 @@ export default function useOptions (props, context, dep) return } + handleOptionAppend(option) + blur() select(option) break @@ -284,6 +302,8 @@ export default function useOptions (props, context, dep) return } + handleOptionAppend(option) + select(option) if (clearOnSelect.value) { @@ -312,15 +332,7 @@ export default function useOptions (props, context, dep) return } - if (getOption(option[valueProp.value]) === undefined && createTag.value) { - context.emit('tag', option[valueProp.value]) - - if (appendNewTag.value) { - appendOption(option) - } - - clearSearch() - } + handleOptionAppend(option) if (clearOnSelect.value) { clearSearch() @@ -373,6 +385,19 @@ export default function useOptions (props, context, dep) } } + const handleOptionAppend = (option) => { + if (getOption(option[valueProp.value]) === undefined && createOption.value) { + context.emit('tag', option[valueProp.value]) + context.emit('option', option[valueProp.value]) + + if (appendNewOption.value) { + appendOption(option) + } + + clearSearch() + } + } + const selectAll = () => { if (mode.value === 'single') { return diff --git a/tests/unit/composables/useKeyboard.spec.js b/tests/unit/composables/useKeyboard.spec.js index f42ca5e..3592318 100644 --- a/tests/unit/composables/useKeyboard.spec.js +++ b/tests/unit/composables/useKeyboard.spec.js @@ -12,7 +12,7 @@ describe('useKeyboard', () => { options: [], mode: 'tags', searchable: true, - createTag: true, + createOption: true, showOptions: false, }) @@ -119,7 +119,7 @@ describe('useKeyboard', () => { expect(getValue(select)).toStrictEqual(2) }) - it('should not select pointer when mode=tags and createTag and addTagOn does not contain enter', async () => { + it('should not select pointer when mode=tags and createTag and addOptionTag does not contain enter', async () => { let select = createSelect({ mode: 'tags', value: [], @@ -137,7 +137,7 @@ describe('useKeyboard', () => { expect(getValue(select)).toStrictEqual([]) }) - it('should select pointer when mode=tags and createTag and addTagOn does contain enter', async () => { + it('should select pointer when mode=tags and createTag and addOptionTag does contain enter', async () => { let select = createSelect({ mode: 'tags', value: [], @@ -154,6 +154,42 @@ describe('useKeyboard', () => { expect(getValue(select)).toStrictEqual([2]) }) + + it('should not select pointer when mode=single and createOption and addOptionOn does not contain enter', async () => { + let select = createSelect({ + mode: 'single', + value: null, + options: [1,2,3], + addOptionOn: ['space'], + createOption: true, + }) + + select.vm.setPointer(select.vm.getOption(2)) + + keydown(select, 'enter') + + await nextTick() + + expect(getValue(select)).toStrictEqual(null) + }) + + it('should select pointer when mode=single and createOption and addOptionOn does contain enter', async () => { + let select = createSelect({ + mode: 'single', + value: null, + options: [1,2,3], + addOptionOn: ['space', 'enter'], + createOption: true, + }) + + select.vm.setPointer(select.vm.getOption(2)) + + keydown(select, 'enter') + + await nextTick() + + expect(getValue(select)).toStrictEqual(2) + }) }) describe('esc', () => { @@ -261,6 +297,24 @@ describe('useKeyboard', () => { expect(getValue(select)).toStrictEqual([1,2]) }) + it('should select pointer if not searchable and createOption and single and addOptionOn contains space', async () => { + let select = createSelect({ + mode: 'single', + value: 1, + options: [1,2,3], + addOptionOn: ['space'], + createOption: true, + }) + + select.vm.setPointer(select.vm.getOption(2)) + + keydown(select, 'space') + + await nextTick() + + expect(getValue(select)).toStrictEqual(2) + }) + it('should select pointer if searchable and createTag and tags and addTagOn contains space', async () => { let select = createSelect({ mode: 'tags', @@ -280,6 +334,25 @@ describe('useKeyboard', () => { expect(getValue(select)).toStrictEqual([1, 2]) }) + it('should select pointer if searchable and createOption and single and addOptionOn contains space', async () => { + let select = createSelect({ + mode: 'single', + value: 1, + options: [1,2,3], + searchable: true, + addOptionOn: ['space'], + createOption: true, + }) + + select.vm.setPointer(select.vm.getOption(2)) + + keydown(select, 'space') + + await nextTick() + + expect(getValue(select)).toStrictEqual(2) + }) + it('should not select pointer if not searchable and createTag and tags and addTagOn does not contain space', async () => { let select = createSelect({ mode: 'tags', @@ -298,6 +371,24 @@ describe('useKeyboard', () => { expect(getValue(select)).toStrictEqual([1]) }) + it('should not select pointer if not searchable and createOption and single and addOptionOn does not contain space', async () => { + let select = createSelect({ + mode: 'single', + value: 1, + options: [1,2,3], + addOptionOn: ['enter'], + createOption: true, + }) + + select.vm.setPointer(select.vm.getOption(2)) + + keydown(select, 'space') + + await nextTick() + + expect(getValue(select)).toStrictEqual(1) + }) + it('should not select pointer if searchable and createTag and tags and addTagOn does not contain space', async () => { let select = createSelect({ mode: 'tags', @@ -316,6 +407,25 @@ describe('useKeyboard', () => { expect(getValue(select)).toStrictEqual([1]) }) + + it('should not select pointer if searchable and createOption and single and addOptionOn does not contain space', async () => { + let select = createSelect({ + mode: 'single', + value: 1, + options: [1,2,3], + searchable: true, + addOptionOn: ['enter'], + createOption: true, + }) + + select.vm.setPointer(select.vm.getOption(2)) + + keydown(select, 'space') + + await nextTick() + + expect(getValue(select)).toStrictEqual(1) + }) }) describe('up', () => { @@ -432,6 +542,24 @@ describe('useKeyboard', () => { expect(getValue(select)).toStrictEqual([1]) }) + it('should not do anything when if mode=single and addOptionOn contains ; but createOption is false', async () => { + let select = createSelect({ + mode: 'single', + value: 1, + options: [1,2,3], + addOptionOn: [';'], + createOption: false, + }) + + select.vm.setPointer(select.vm.getOption(2)) + + keydown(select, { key: ';' }) + + await nextTick() + + expect(getValue(select)).toStrictEqual(1) + }) + it('should not do anything when if mode=tags and addTagOn does not contain ;', async () => { let select = createSelect({ mode: 'tags', @@ -450,6 +578,24 @@ describe('useKeyboard', () => { expect(getValue(select)).toStrictEqual([1]) }) + it('should not do anything when if mode=tags and addOptionOn does not contain ;', async () => { + let select = createSelect({ + mode: 'single', + value: 1, + options: [1,2,3], + addOptionOn: [','], + createOption: true, + }) + + select.vm.setPointer(select.vm.getOption(2)) + + keydown(select, { key: ';' }) + + await nextTick() + + expect(getValue(select)).toStrictEqual(1) + }) + it('should select pointer if mode=tags and addTagOn contains ;', async () => { let select = createSelect({ mode: 'tags', @@ -467,6 +613,24 @@ describe('useKeyboard', () => { expect(getValue(select)).toStrictEqual([1,2]) }) + + it('should select pointer if mode=single and addOptionOn contains ;', async () => { + let select = createSelect({ + mode: 'single', + value: 1, + options: [1,2,3], + addOptionOn: [';'], + createOption: true, + }) + + select.vm.setPointer(select.vm.getOption(2)) + + keydown(select, { key: ';' }) + + await nextTick() + + expect(getValue(select)).toStrictEqual(2) + }) }) describe('comma', () => { @@ -503,6 +667,24 @@ describe('useKeyboard', () => { expect(getValue(select)).toStrictEqual([1]) }) + it('should not do anything when if mode=single and addOptionOn contains , but createOption is false', async () => { + let select = createSelect({ + mode: 'single', + value: 1, + options: [1,2,3], + addOptionOn: [','], + createOption: false, + }) + + select.vm.setPointer(select.vm.getOption(2)) + + keydown(select, { key: ',' }) + + await nextTick() + + expect(getValue(select)).toStrictEqual(1) + }) + it('should not do anything when if mode=tags and addTagOn does not contain ,', async () => { let select = createSelect({ mode: 'tags', @@ -521,6 +703,24 @@ describe('useKeyboard', () => { expect(getValue(select)).toStrictEqual([1]) }) + it('should not do anything when if mode=single and addOptionOn does not contain ,', async () => { + let select = createSelect({ + mode: 'single', + value: 1, + options: [1,2,3], + addOptionOn: [';'], + createOption: true, + }) + + select.vm.setPointer(select.vm.getOption(2)) + + keydown(select, { key: ',' }) + + await nextTick() + + expect(getValue(select)).toStrictEqual(1) + }) + it('should select pointer if mode=tags and addTagOn contains ,', async () => { let select = createSelect({ mode: 'tags', @@ -538,6 +738,24 @@ describe('useKeyboard', () => { expect(getValue(select)).toStrictEqual([1,2]) }) + + it('should select pointer if mode=single and addTagOn contains ,', async () => { + let select = createSelect({ + mode: 'single', + value: 1, + options: [1,2,3], + addTagOn: [','], + createTag: true, + }) + + select.vm.setPointer(select.vm.getOption(2)) + + keydown(select, { key: ',' }) + + await nextTick() + + expect(getValue(select)).toStrictEqual(2) + }) }) describe('tab', () => { @@ -574,6 +792,24 @@ describe('useKeyboard', () => { expect(getValue(select)).toStrictEqual([1]) }) + it('should not do anything when if mode=single and addOptionOn contains tab but createOption is false', async () => { + let select = createSelect({ + mode: 'single', + value: 1, + options: [1,2,3], + addOptionOn: ['tab'], + createOption: false, + }) + + select.vm.setPointer(select.vm.getOption(2)) + + keydown(select, { key: 'Tab' }) + + await nextTick() + + expect(getValue(select)).toStrictEqual(1) + }) + it('should not do anything when if mode=tags and addTagOn does not contain tab', async () => { let select = createSelect({ mode: 'tags', @@ -592,6 +828,24 @@ describe('useKeyboard', () => { expect(getValue(select)).toStrictEqual([1]) }) + it('should not do anything when if mode=single and addOptionOn does not contain tab', async () => { + let select = createSelect({ + mode: 'single', + value: 1, + options: [1,2,3], + addOptionOn: ['enter'], + createOption: true, + }) + + select.vm.setPointer(select.vm.getOption(2)) + + keydown(select, { key: 'Tab' }) + + await nextTick() + + expect(getValue(select)).toStrictEqual(1) + }) + it('should select pointer if mode=tags and addTagOn contains tab', async () => { let select = createSelect({ mode: 'tags', @@ -609,6 +863,24 @@ describe('useKeyboard', () => { expect(getValue(select)).toStrictEqual([1,2]) }) + + it('should select pointer if mode=single and addOptionOn contains tab', async () => { + let select = createSelect({ + mode: 'single', + value: 1, + options: [1,2,3], + addOptionOn: ['tab'], + createOption: true, + }) + + select.vm.setPointer(select.vm.getOption(2)) + + keydown(select, { key: 'Tab' }) + + await nextTick() + + expect(getValue(select)).toStrictEqual(2) + }) }) }) }) \ No newline at end of file diff --git a/tests/unit/composables/useOptions.spec.js b/tests/unit/composables/useOptions.spec.js index 66805fe..9c88f2b 100644 --- a/tests/unit/composables/useOptions.spec.js +++ b/tests/unit/composables/useOptions.spec.js @@ -63,7 +63,7 @@ describe('useOptions', () => { ]) }) - it('should append createdTag to `fo` when createTag true', () => { + it('should append createdOption to `fo` when createTag true', () => { const select = createSelect({ mode: 'tags', createTag: true, @@ -76,7 +76,7 @@ describe('useOptions', () => { expect(select.vm.fo[0].v).toStrictEqual('new-tag') }) - it('should not append createdTag to `fo` when if it already exists exists', () => { + it('should not append createdOption to `fo` when if it already exists exists', () => { const select = createSelect({ mode: 'tags', createTag: true, @@ -981,7 +981,7 @@ describe('useOptions', () => { describe('noResults', () => { it('should be true if no options match search', () => { let select = createSelect({ - options: ['Java', 'Javascript'] + options: ['Java', 'Javascript'], }) select.vm.search = 'jav' expect(select.vm.noResults).toBe(false) @@ -1602,6 +1602,101 @@ describe('useOptions', () => { destroy(select) }) + it('should emit option and clear search and not append option on select if createOption true and option does not exist', async () => { + let select = createSelect({ + mode: 'single', + value: null, + options: [1,2,3], + createOption: true, + appendNewOption: false, + object: true, + valueProp: 'v' + }) + + select.vm.search = 'value' + select.vm.handleOptionClick({ v: 'value', label: 'value' }) + + await nextTick() + + expect(select.emitted('option')[0][0]).toStrictEqual('value') + expect(select.vm.search).toBe('') + expect(select.vm.fo.length).toBe(3) + }) + + it('should append option if createOption && appendNewOption true and option does not exist', async () => { + let select = createSelect({ + mode: 'single', + value: null, + options: [1,2,3], + createOption: true, + appendNewOption: true, + hideSelected: false, + valueProp: 'v' + }) + + select.vm.handleOptionClick({ v: 'value', label: 'value' }) + + await nextTick() + + expect(select.emitted('option')[0][0]).toStrictEqual('value') + expect(select.vm.fo).toStrictEqual([ + { v: 1, label: 1 }, + { v: 2, label: 2 }, + { v: 3, label: 3 }, + { v: 'value', label: 'value' }, + ]) + }) + + it('should not append option if it already exists', async () => { + let select = createSelect({ + mode: 'single', + value: null, + options: [1,2,3], + createOption: true, + appendNewOption: true, + hideSelected: false, + valueProp: 'v', + }) + + select.vm.handleOptionClick({ v: 'value', label: 'value' }) + + await nextTick() + + select.vm.handleOptionClick({ v: 'value', label: 'value' }) + + await nextTick() + + expect(select.vm.fo).toStrictEqual([ + { v: 1, label: 1 }, + { v: 2, label: 2 }, + { v: 3, label: 3 }, + { v: 'value', label: 'value' }, + ]) + }) + + it('should not append option if appendNewOption is false', async () => { + let select = createSelect({ + mode: 'single', + value: null, + options: [1,2,3], + createOption: true, + appendNewOption: false, + hideSelected: false, + object: true, + valueProp: 'v', + }) + + select.vm.handleOptionClick({ v: 'value', label: 'value' }) + + await nextTick() + + expect(select.vm.fo).toStrictEqual([ + { v: 1, label: 1 }, + { v: 2, label: 2 }, + { v: 3, label: 3 }, + ]) + }) + /* MULTISELECT */ it('should remove option from value if selected when multiple', async () => { @@ -1712,6 +1807,101 @@ describe('useOptions', () => { expect(select.vm.pointer).toStrictEqual(select.vm.getOption(2)) }) + it('should emit option and clear search and not append option on select if createOption true and option does not exist', async () => { + let select = createSelect({ + mode: 'multiple', + value: [], + options: [1,2,3], + createOption: true, + appendNewOption: false, + object: true, + valueProp: 'v' + }) + + select.vm.search = 'value' + select.vm.handleOptionClick({ v: 'value', label: 'value' }) + + await nextTick() + + expect(select.emitted('option')[0][0]).toStrictEqual('value') + expect(select.vm.search).toBe('') + expect(select.vm.fo.length).toBe(3) + }) + + it('should append option if createOption && appendNewOption true and option does not exist', async () => { + let select = createSelect({ + mode: 'multiple', + value: [], + options: [1,2,3], + createOption: true, + appendNewOption: true, + hideSelected: false, + valueProp: 'v' + }) + + select.vm.handleOptionClick({ v: 'value', label: 'value' }) + + await nextTick() + + expect(select.emitted('option')[0][0]).toStrictEqual('value') + expect(select.vm.fo).toStrictEqual([ + { v: 1, label: 1 }, + { v: 2, label: 2 }, + { v: 3, label: 3 }, + { v: 'value', label: 'value' }, + ]) + }) + + it('should not append option if it already exists', async () => { + let select = createSelect({ + mode: 'multiple', + value: [], + options: [1,2,3], + createOption: true, + appendNewOption: true, + hideSelected: false, + valueProp: 'v', + }) + + select.vm.handleOptionClick({ v: 'value', label: 'value' }) + + await nextTick() + + select.vm.handleOptionClick({ v: 'value', label: 'value' }) + + await nextTick() + + expect(select.vm.fo).toStrictEqual([ + { v: 1, label: 1 }, + { v: 2, label: 2 }, + { v: 3, label: 3 }, + { v: 'value', label: 'value' }, + ]) + }) + + it('should not append option if appendNewOption is false', async () => { + let select = createSelect({ + mode: 'multiple', + value: [], + options: [1,2,3], + createOption: true, + appendNewOption: false, + hideSelected: false, + object: true, + valueProp: 'v', + }) + + select.vm.handleOptionClick({ v: 'value', label: 'value' }) + + await nextTick() + + expect(select.vm.fo).toStrictEqual([ + { v: 1, label: 1 }, + { v: 2, label: 2 }, + { v: 3, label: 3 }, + ]) + }) + it('should blur input after select if closeOnSelect=true && mode=multiple', async () => { let select = createSelect({ mode: 'multiple',