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:
@@ -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">×</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
@@ -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
@@ -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
@@ -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));
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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>`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user