2
0
mirror of https://github.com/tenrok/vue-select.git synced 2026-06-22 10:30:34 +03:00

update tests to use mount where required, start new slot docs

This commit is contained in:
Jeff
2019-11-06 13:10:51 -08:00
parent 83c1c795db
commit 218a9e5c99
7 changed files with 134 additions and 160 deletions
-65
View File
@@ -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
<slot name="selected-option" v-bind="(typeof option === 'object')?option:{[label]: option}">
{{ getOptionLabel(option) }}
</slot>
```
### `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
<slot v-for="option in valueAsArray" name="selected-option-container"
:option="(typeof option === 'object')?option:{[label]: option}" :deselect="deselect" :multiple="multiple" :disabled="disabled">
<span class="selected-tag" v-bind:key="option.index">
<slot name="selected-option" v-bind="(typeof option === 'object')?option:{[label]: option}">
{{ getOptionLabel(option) }}
</slot>
<button v-if="multiple" :disabled="disabled" @click="deselect(option)" type="button" class="close" aria-label="Remove option">
<span aria-hidden="true">&times;</span>
</button>
</span>
</slot>
```
## Component Actions
### `spinner`
```html
<slot name="spinner">
<div class="spinner" v-show="mutableLoading">Loading...</div>
</slot>
```
## Dropdown
### `option`
#### Scope:
- `option {Object}` - The currently iterated option from `filteredOptions`
```html
<slot name="option" v-bind="(typeof option === 'object')?option:{[label]: option}">
{{ getOptionLabel(option) }}
</slot>
```
+9 -18
View File
@@ -1,22 +1,13 @@
::: tip 🚧 ## Scoped Slots
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 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)
<v-select :options="options" label="title"> `events {Object}` Event handlers for elements within the slot
<template v-slot:option="option">
<span :class="option.icon"></span>
{{ option.title }}
</template>
</v-select>
```
Using the `option` slot with props `"option"` provides the current option variable to the template.
<CodePen url="NXBwYG" height="500"/>
+10 -10
View File
@@ -1,22 +1,22 @@
<script> <script>
export default { export default {
functional: true, functional: true,
render (_, {data}) { render (createElement, {data}) {
const svg = { const path = createElement('path', {
attrs: {
d: 'M6.895455 5l2.842897-2.842898c.348864-.348863.348864-.914488 0-1.263636L9.106534.261648c-.348864-.348864-.914489-.348864-1.263636 0L5 3.104545 2.157102.261648c-.348863-.348864-.914488-.348864-1.263636 0L.261648.893466c-.348864.348864-.348864.914489 0 1.263636L3.104545 5 .261648 7.842898c-.348864.348863-.348864.914488 0 1.263636l.631818.631818c.348864.348864.914773.348864 1.263636 0L5 6.895455l2.842898 2.842897c.348863.348864.914772.348864 1.263636 0l.631818-.631818c.348864-.348864.348864-.914489 0-1.263636L6.895455 5z,',
},
});
const svg = createElement('svg', {
attrs: { attrs: {
xmlns: 'http://www.w3.org/2000/svg', xmlns: 'http://www.w3.org/2000/svg',
width: 10, width: 10,
height: 10, height: 10,
}, },
}; }, [path]);
const path = { return createElement('button', data, [svg]);
attrs: {
d: 'M6.895455 5l2.842897-2.842898c.348864-.348863.348864-.914488 0-1.263636L9.106534.261648c-.348864-.348864-.914489-.348864-1.263636 0L5 3.104545 2.157102.261648c-.348863-.348864-.914488-.348864-1.263636 0L.261648.893466c-.348864.348864-.348864.914489 0 1.263636L3.104545 5 .261648 7.842898c-.348864.348863-.348864.914488 0 1.263636l.631818.631818c.348864.348864.914773.348864 1.263636 0L5 6.895455l2.842898 2.842897c.348863.348864.914772.348864 1.263636 0l.631818-.631818c.348864-.348864.348864-.914489 0-1.263636L6.895455 5z,',
},
};
return _('button', data, [_('svg', svg, [_('path', path)])]);
}, },
}; };
</script> </script>
+74 -43
View File
@@ -3,13 +3,14 @@
<div ref="toggle" @mousedown.prevent="toggleDropdown" class="vs__dropdown-toggle"> <div ref="toggle" @mousedown.prevent="toggleDropdown" class="vs__dropdown-toggle">
<div class="vs__selected-options" ref="selectedOptions"> <div class="vs__selected-options" ref="selectedOptions">
<slot name="selected-option" v-for="option in scopedValues" v-bind="option"> <slot name="selected-option" v-for="selected in scopedValues" v-bind="selected">
<span :class="option.bindings.class"> <span :class="selected.bindings.class">
{{ option.label }} {{ selected.label }}
<component <component
:is="option.deselect.component" :is="selected.deselect.component"
v-bind="option.deselect.bindings" v-if="selected.deselect.bindings.multiple"
v-on="option.deselect.events" v-bind="selected.deselect.bindings"
v-on="selected.deselect.events"
/> />
</span> </span>
</slot> </slot>
@@ -20,23 +21,24 @@
</div> </div>
<div class="vs__actions"> <div class="vs__actions">
<button <slot name="clear">
v-show="showClearButton" <component
:disabled="disabled" :is="scope.clear.component"
@click="clearSelection" v-bind="scope.clear.bindings"
type="button" v-on="scope.clear.events"
class="vs__clear" />
title="Clear selection" </slot>
>
<component :is="childComponents.Deselect" />
</button>
<slot name="open-indicator" v-bind="scope.openIndicator"> <slot name="open-indicator" v-bind="scope.openIndicator">
<component :is="childComponents.OpenIndicator" v-if="!noDrop" v-bind="scope.openIndicator.attributes"/> <component
v-if="!noDrop"
:is="childComponents.OpenIndicator"
v-bind="scope.openIndicator.attributes"
/>
</slot> </slot>
<slot name="spinner" v-bind="scope.spinner"> <slot name="spinner" v-bind="scope.spinner">
<div class="vs__spinner" v-show="mutableLoading">Loading...</div> <div :class="scope.spinner.bindings.class" v-show="mutableLoading">Loading...</div>
</slot> </slot>
</div> </div>
</div> </div>
@@ -50,8 +52,8 @@
@mousedown.prevent="onMousedown" @mousedown.prevent="onMousedown"
@mouseup="onMouseUp" @mouseup="onMouseUp"
> >
<slot name="option" v-for="{attributes, events, option} in scopedOptions" v-bind="{attributes, events, option}"> <slot name="option" v-for="option in scopedOptions" v-bind="option">
<li v-bind="attributes" v-on="events">{{ getOptionLabel(option) }}</li> <li v-bind="option.attributes" v-on="option.events">{{ option.label }}</li>
</slot> </slot>
<li v-if="!filteredOptions.length" class="vs__no-options" @mousedown.stop=""> <li v-if="!filteredOptions.length" class="vs__no-options" @mousedown.stop="">
<slot name="no-options">Sorry, no matching options.</slot> <slot name="no-options">Sorry, no matching options.</slot>
@@ -269,30 +271,32 @@
*/ */
getOptionKey: { getOptionKey: {
type: Function, type: Function,
default(option) { default (option) {
if (typeof option === 'object' && option.id) { if (typeof option === 'object' && option.id) {
return option.id return option.id;
} else { } else {
try { try {
return JSON.stringify(option) return JSON.stringify(option);
} catch(e) { } catch (e) {
return console.warn( return console.warn(
`[vue-select warn]: Could not stringify option ` + `[vue-select warn]: Could not stringify option ` +
`to generate unique key. Please provide'getOptionKey' prop ` + `to generate unique key. Please provide'getOptionKey' prop ` +
`to return a unique key for each option.\n` + `to return a unique key for each option.\n` +
'https://vue-select.org/api/props.html#getoptionkey' 'https://vue-select.org/api/props.html#getoptionkey',
) );
return null
} }
} }
} },
}, },
getDropdownOptionScope: { getOptionScope: {
type: Function, type: Function,
default(option, index) { default (option, index) {
const optionProperties = typeof option === 'object' ? {...option} : {};
return { return {
option, ...optionProperties,
label: this.getOptionLabel(option),
attributes: { attributes: {
key: this.getOptionKey(option), key: this.getOptionKey(option),
class: { class: {
@@ -308,7 +312,7 @@
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
return this.selectable(option) ? this.select(option) : null; return this.selectable(option) ? this.select(option) : null;
} },
}, },
}; };
}, },
@@ -316,7 +320,7 @@
getSelectedOptionScope: { getSelectedOptionScope: {
type: Function, type: Function,
default(option, index) { default (option, index) {
return { return {
label: this.getOptionLabel(option), label: this.getOptionLabel(option),
deselect: this.getOptionDeselectScope(option), deselect: this.getOptionDeselectScope(option),
@@ -325,7 +329,7 @@
option: this.normalizeOptionForSlot(option), option: this.normalizeOptionForSlot(option),
deselect: this.deselect, deselect: this.deselect,
multiple: this.multiple, multiple: this.multiple,
class: "vs__selected", class: 'vs__selected',
}, },
events: { events: {
'mouseover': () => this.selectable(option) ? this.typeAheadPointer = index : null, 'mouseover': () => this.selectable(option) ? this.typeAheadPointer = index : null,
@@ -333,7 +337,7 @@
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
return this.selectable(option) ? this.select(option) : null; return this.selectable(option) ? this.select(option) : null;
} },
}, },
}; };
}, },
@@ -341,7 +345,7 @@
getOptionDeselectScope: { getOptionDeselectScope: {
type: Function, type: Function,
default(option) { default (option) {
return { return {
component: childComponents.Deselect, component: childComponents.Deselect,
bindings: { bindings: {
@@ -353,9 +357,9 @@
}, },
events: { events: {
'click': () => this.deselect(option), 'click': () => this.deselect(option),
} },
} };
} },
}, },
/** /**
@@ -1017,11 +1021,38 @@
'keydown': this.onSearchKeyDown, 'keydown': this.onSearchKeyDown,
'blur': this.onSearchBlur, 'blur': this.onSearchBlur,
'focus': this.onSearchFocus, '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: { spinner: {
loading: this.mutableLoading loading: this.mutableLoading,
bindings: {
'class': 'vs__spinner',
},
}, },
openIndicator: { openIndicator: {
attributes: { attributes: {
@@ -1123,7 +1154,7 @@
}, },
scopedOptions() { scopedOptions() {
return this.filteredOptions.map((option, index) => this.getDropdownOptionScope(this.normalizeOptionForSlot(option), index)); return this.filteredOptions.map((option, index) => this.getOptionScope(option, index));
}, },
/** /**
+1 -1
View File
@@ -6,7 +6,7 @@ describe('Components API', () => {
it('swap the Deselect component', () => { it('swap the Deselect component', () => {
const Deselect = Vue.component('Deselect', { const Deselect = Vue.component('Deselect', {
render (createElement) { render (createElement) {
return createElement('button', 'remove'); return createElement('span', 'remove');
}, },
}); });
+39 -22
View File
@@ -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", () => { describe("Removing values", () => {
it("can remove the given tag when its close icon is clicked", () => { it("can remove the given tag when its close icon is clicked", () => {
const Select = selectWithProps({ multiple: true }); const Select = mount(VueSelect, {
Select.vm.$data._value = 'one'; propsData: {
multiple: true,
options: ['foo', 'bar'],
value: 'foo',
},
});
Select.find(".vs__deselect").trigger("click"); const deselect = jest.spyOn(Select.vm, 'deselect');
expect(Select.emitted().input).toEqual([[[]]]);
expect(Select.vm.selectedValue).toEqual([]); Select.find("button.vs__deselect").trigger("click");
expect(deselect).toHaveBeenCalled();
}); });
it("should not remove tag when close icon is clicked and component is disabled", () => { 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.vm.showClearButton).toEqual(true);
expect(Select.find('.vs__clear').isVisible()).toBe(true);
}); });
it("should not be displayed on multiple select", () => { it("should not be displayed on multiple select", () => {
const Select = selectWithProps({ const Select = mount(VueSelect, {
options: ["foo", "bar"], propsData: {
value: "foo", multiple: true,
multiple: true options: ['foo', 'bar'],
value: 'foo',
},
}); });
expect(Select.vm.showClearButton).toEqual(false); expect(Select.vm.showClearButton).toEqual(false);
expect(Select.find('.vs__clear').isVisible()).toBe(false);
}); });
it("should remove selected value when clicked", () => { it("should remove selected value when clicked", async () => {
const Select = selectWithProps({ const Select = mount(VueSelect, {
options: ["foo", "bar"], propsData: {
options: ['foo', 'bar'],
value: 'foo',
},
}); });
Select.vm.$data._value = 'foo';
expect(Select.vm.selectedValue).toEqual(["foo"]); const spy = jest.spyOn(Select.vm, 'clearSelection');
Select.find("button.vs__clear").trigger("click");
expect(Select.emitted().input).toEqual([[null]]); Select.find('button.vs__clear').trigger("click");
expect(Select.vm.selectedValue).toEqual([]);
expect(spy).toHaveBeenCalled();
}); });
it("should be disabled when component is disabled", () => { it("should be disabled when component is disabled", () => {
const Select = selectWithProps({ const Select = mount(VueSelect, {
options: ["foo", "bar"], propsData: {
value: "foo", disabled: true,
disabled: true options: ['foo', 'bar'],
value: 'foo',
},
}); });
expect(Select.find("button.vs__clear").attributes().disabled).toEqual( expect(Select.find("button.vs__clear").attributes().disabled).toEqual(
+1 -1
View File
@@ -6,7 +6,7 @@ describe('Scoped Slots', () => {
{value: 'one'}, {value: 'one'},
{ {
scopedSlots: { scopedSlots: {
'selected-option': `<span slot="selected-option" slot-scope="option">{{ option.label }}</span>`, 'selected-option': `<span class="vs__selected" slot="selected-option" slot-scope="option">{{ option.label }}</span>`,
}, },
}); });