diff --git a/docs/api/slots.md b/docs/api/slots.md index 1f3a94f..e69de29 100644 --- a/docs/api/slots.md +++ b/docs/api/slots.md @@ -1,65 +0,0 @@ -::: tip -Vue Select leverages scoped slots to allow for total customization of the presentation layer. -Slots can be used to change the look and feel of the UI, or to simply swap out text. -::: - -## Selected Option(s) - -### `selected-option` - -#### Scope: - -- `option {Object}` - A selected option - -```html - - {{ getOptionLabel(option) }} - -``` - -### `selected-option-container` - -#### Scope: - -- `option {Object}` - A selected option -- `deselect {Function}` - Method used to deselect a given option when `multiple` is true -- `disabled {Boolean}` - Determine if the component is disabled -- `multiple {Boolean}` - If the component supports the selection of multiple values - -```html - - - - {{ getOptionLabel(option) }} - - - - -``` - -## Component Actions - -### `spinner` - -```html - -
Loading...
-
-``` - -## Dropdown - -### `option` - -#### Scope: - -- `option {Object}` - The currently iterated option from `filteredOptions` - -```html - - {{ getOptionLabel(option) }} - -``` diff --git a/docs/guide/slots.md b/docs/guide/slots.md index 7bfe9b1..ad4710a 100644 --- a/docs/guide/slots.md +++ b/docs/guide/slots.md @@ -1,22 +1,13 @@ -::: tip 🚧 -This section of the guide is a work in progress! Check back soon for an update. -Vue Select currently offers quite a few scoped slots, and you can check out the -[API Docs for Slots](../api/slots.md) in the meantime while a good guide is put together. -::: +## Scoped Slots -#### Scoped Slot `option` +Vue Select offers a number of scoped slots that allow you to customize many parts of the +component for your app. You can make small adjustments with slots, or you can swap out all elements +of the default UI for your own. -vue-select provides the scoped `option` slot in order to create custom dropdown templates. +All of Vue Selects scoped slots follow a similar pattern. Each slot is scoped with an object with at +least two keys: `bindings` and `events`. -```html - - - -``` +`bindings {Object}` Data that is bound to an element within the slot (HTML attributes, classes, etc) +`events {Object}` Event handlers for elements within the slot + -Using the `option` slot with props `"option"` provides the current option variable to the template. - - diff --git a/src/components/Deselect.vue b/src/components/Deselect.vue index e7605b2..11562e3 100644 --- a/src/components/Deselect.vue +++ b/src/components/Deselect.vue @@ -1,22 +1,22 @@ diff --git a/src/components/Select.vue b/src/components/Select.vue index 14efe2c..9ef97c6 100644 --- a/src/components/Select.vue +++ b/src/components/Select.vue @@ -3,13 +3,14 @@
- - - {{ option.label }} + + + {{ selected.label }} @@ -20,23 +21,24 @@
- + + + - + -
Loading...
+
Loading...
@@ -50,8 +52,8 @@ @mousedown.prevent="onMousedown" @mouseup="onMouseUp" > - -
  • {{ getOptionLabel(option) }}
  • + +
  • {{ option.label }}
  • Sorry, no matching options. @@ -269,30 +271,32 @@ */ getOptionKey: { type: Function, - default(option) { + default (option) { if (typeof option === 'object' && option.id) { - return option.id + return option.id; } else { try { - return JSON.stringify(option) - } catch(e) { + return JSON.stringify(option); + } catch (e) { return console.warn( `[vue-select warn]: Could not stringify option ` + `to generate unique key. Please provide'getOptionKey' prop ` + `to return a unique key for each option.\n` + - 'https://vue-select.org/api/props.html#getoptionkey' - ) - return null + 'https://vue-select.org/api/props.html#getoptionkey', + ); } } - } + }, }, - - getDropdownOptionScope: { + + getOptionScope: { type: Function, - default(option, index) { + default (option, index) { + const optionProperties = typeof option === 'object' ? {...option} : {}; + return { - option, + ...optionProperties, + label: this.getOptionLabel(option), attributes: { key: this.getOptionKey(option), class: { @@ -308,7 +312,7 @@ e.preventDefault(); e.stopPropagation(); return this.selectable(option) ? this.select(option) : null; - } + }, }, }; }, @@ -316,7 +320,7 @@ getSelectedOptionScope: { type: Function, - default(option, index) { + default (option, index) { return { label: this.getOptionLabel(option), deselect: this.getOptionDeselectScope(option), @@ -325,7 +329,7 @@ option: this.normalizeOptionForSlot(option), deselect: this.deselect, multiple: this.multiple, - class: "vs__selected", + class: 'vs__selected', }, events: { 'mouseover': () => this.selectable(option) ? this.typeAheadPointer = index : null, @@ -333,7 +337,7 @@ e.preventDefault(); e.stopPropagation(); return this.selectable(option) ? this.select(option) : null; - } + }, }, }; }, @@ -341,7 +345,7 @@ getOptionDeselectScope: { type: Function, - default(option) { + default (option) { return { component: childComponents.Deselect, bindings: { @@ -353,9 +357,9 @@ }, events: { 'click': () => this.deselect(option), - } - } - } + }, + }; + }, }, /** @@ -1017,11 +1021,38 @@ 'keydown': this.onSearchKeyDown, 'blur': this.onSearchBlur, 'focus': this.onSearchFocus, - 'input': (e) => this.search = e.target.value + 'input': (e) => this.search = e.target.value, + }, + }, + clear: { + component: this.childComponents.Deselect, + bindings: { + 'disabled': this.disabled, + 'type': 'button', + 'class': 'vs__clear', + 'title': 'Clear Selection', + 'style': { 'display': this.showClearButton ? 'block': 'none' } + }, + events: { + 'click': () => this.clearSelection(), + }, + }, + noOptions: { + text: 'Sorry, no options', + options: this.scopedOptions, + search: this.search, + attributes: { + class: 'vs__no-options', + }, + events: { + mousedown: e => e.stopPropagation(), }, }, spinner: { - loading: this.mutableLoading + loading: this.mutableLoading, + bindings: { + 'class': 'vs__spinner', + }, }, openIndicator: { attributes: { @@ -1123,7 +1154,7 @@ }, scopedOptions() { - return this.filteredOptions.map((option, index) => this.getDropdownOptionScope(this.normalizeOptionForSlot(option), index)); + return this.filteredOptions.map((option, index) => this.getOptionScope(option, index)); }, /** diff --git a/tests/unit/Components.spec.js b/tests/unit/Components.spec.js index 3c43710..191645f 100644 --- a/tests/unit/Components.spec.js +++ b/tests/unit/Components.spec.js @@ -6,7 +6,7 @@ describe('Components API', () => { it('swap the Deselect component', () => { const Deselect = Vue.component('Deselect', { render (createElement) { - return createElement('button', 'remove'); + return createElement('span', 'remove'); }, }); diff --git a/tests/unit/Deselecting.spec.js b/tests/unit/Deselecting.spec.js index db14a79..2fb5ebc 100755 --- a/tests/unit/Deselecting.spec.js +++ b/tests/unit/Deselecting.spec.js @@ -1,13 +1,22 @@ -import { selectWithProps } from "../helpers"; +import { mount } from '@vue/test-utils'; +import { selectWithProps } from '../helpers'; +import VueSelect from '../../src/components/Select.vue'; describe("Removing values", () => { it("can remove the given tag when its close icon is clicked", () => { - const Select = selectWithProps({ multiple: true }); - Select.vm.$data._value = 'one'; + const Select = mount(VueSelect, { + propsData: { + multiple: true, + options: ['foo', 'bar'], + value: 'foo', + }, + }); - Select.find(".vs__deselect").trigger("click"); - expect(Select.emitted().input).toEqual([[[]]]); - expect(Select.vm.selectedValue).toEqual([]); + const deselect = jest.spyOn(Select.vm, 'deselect'); + + Select.find("button.vs__deselect").trigger("click"); + + expect(deselect).toHaveBeenCalled(); }); it("should not remove tag when close icon is clicked and component is disabled", () => { @@ -55,36 +64,44 @@ describe("Removing values", () => { }); expect(Select.vm.showClearButton).toEqual(true); + expect(Select.find('.vs__clear').isVisible()).toBe(true); }); it("should not be displayed on multiple select", () => { - const Select = selectWithProps({ - options: ["foo", "bar"], - value: "foo", - multiple: true + const Select = mount(VueSelect, { + propsData: { + multiple: true, + options: ['foo', 'bar'], + value: 'foo', + }, }); expect(Select.vm.showClearButton).toEqual(false); + expect(Select.find('.vs__clear').isVisible()).toBe(false); }); - it("should remove selected value when clicked", () => { - const Select = selectWithProps({ - options: ["foo", "bar"], + it("should remove selected value when clicked", async () => { + const Select = mount(VueSelect, { + propsData: { + options: ['foo', 'bar'], + value: 'foo', + }, }); - Select.vm.$data._value = 'foo'; - expect(Select.vm.selectedValue).toEqual(["foo"]); - Select.find("button.vs__clear").trigger("click"); + const spy = jest.spyOn(Select.vm, 'clearSelection'); - expect(Select.emitted().input).toEqual([[null]]); - expect(Select.vm.selectedValue).toEqual([]); + Select.find('button.vs__clear').trigger("click"); + + expect(spy).toHaveBeenCalled(); }); it("should be disabled when component is disabled", () => { - const Select = selectWithProps({ - options: ["foo", "bar"], - value: "foo", - disabled: true + const Select = mount(VueSelect, { + propsData: { + disabled: true, + options: ['foo', 'bar'], + value: 'foo', + }, }); expect(Select.find("button.vs__clear").attributes().disabled).toEqual( diff --git a/tests/unit/Slots.spec.js b/tests/unit/Slots.spec.js index 35c3a32..66d11c2 100644 --- a/tests/unit/Slots.spec.js +++ b/tests/unit/Slots.spec.js @@ -6,7 +6,7 @@ describe('Scoped Slots', () => { {value: 'one'}, { scopedSlots: { - 'selected-option': `{{ option.label }}`, + 'selected-option': `{{ option.label }}`, }, });