mirror of
https://github.com/tenrok/vue-select.git
synced 2026-06-16 09:10:33 +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 🚧
|
||||
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
|
||||
<v-select :options="options" label="title">
|
||||
<template v-slot:option="option">
|
||||
<span :class="option.icon"></span>
|
||||
{{ option.title }}
|
||||
</template>
|
||||
</v-select>
|
||||
```
|
||||
`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.
|
||||
|
||||
<CodePen url="NXBwYG" height="500"/>
|
||||
|
||||
+10
-10
@@ -1,22 +1,22 @@
|
||||
<script>
|
||||
export default {
|
||||
functional: true,
|
||||
render (_, {data}) {
|
||||
const svg = {
|
||||
render (createElement, {data}) {
|
||||
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: {
|
||||
xmlns: 'http://www.w3.org/2000/svg',
|
||||
width: 10,
|
||||
height: 10,
|
||||
},
|
||||
};
|
||||
}, [path]);
|
||||
|
||||
const 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,',
|
||||
},
|
||||
};
|
||||
|
||||
return _('button', data, [_('svg', svg, [_('path', path)])]);
|
||||
return createElement('button', data, [svg]);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
+74
-43
@@ -3,13 +3,14 @@
|
||||
<div ref="toggle" @mousedown.prevent="toggleDropdown" class="vs__dropdown-toggle">
|
||||
|
||||
<div class="vs__selected-options" ref="selectedOptions">
|
||||
<slot name="selected-option" v-for="option in scopedValues" v-bind="option">
|
||||
<span :class="option.bindings.class">
|
||||
{{ option.label }}
|
||||
<slot name="selected-option" v-for="selected in scopedValues" v-bind="selected">
|
||||
<span :class="selected.bindings.class">
|
||||
{{ selected.label }}
|
||||
<component
|
||||
:is="option.deselect.component"
|
||||
v-bind="option.deselect.bindings"
|
||||
v-on="option.deselect.events"
|
||||
:is="selected.deselect.component"
|
||||
v-if="selected.deselect.bindings.multiple"
|
||||
v-bind="selected.deselect.bindings"
|
||||
v-on="selected.deselect.events"
|
||||
/>
|
||||
</span>
|
||||
</slot>
|
||||
@@ -20,23 +21,24 @@
|
||||
</div>
|
||||
|
||||
<div class="vs__actions">
|
||||
<button
|
||||
v-show="showClearButton"
|
||||
:disabled="disabled"
|
||||
@click="clearSelection"
|
||||
type="button"
|
||||
class="vs__clear"
|
||||
title="Clear selection"
|
||||
>
|
||||
<component :is="childComponents.Deselect" />
|
||||
</button>
|
||||
<slot name="clear">
|
||||
<component
|
||||
:is="scope.clear.component"
|
||||
v-bind="scope.clear.bindings"
|
||||
v-on="scope.clear.events"
|
||||
/>
|
||||
</slot>
|
||||
|
||||
<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 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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -50,8 +52,8 @@
|
||||
@mousedown.prevent="onMousedown"
|
||||
@mouseup="onMouseUp"
|
||||
>
|
||||
<slot name="option" v-for="{attributes, events, option} in scopedOptions" v-bind="{attributes, events, option}">
|
||||
<li v-bind="attributes" v-on="events">{{ getOptionLabel(option) }}</li>
|
||||
<slot name="option" v-for="option in scopedOptions" v-bind="option">
|
||||
<li v-bind="option.attributes" v-on="option.events">{{ option.label }}</li>
|
||||
</slot>
|
||||
<li v-if="!filteredOptions.length" class="vs__no-options" @mousedown.stop="">
|
||||
<slot name="no-options">Sorry, no matching options.</slot>
|
||||
@@ -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));
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('Scoped Slots', () => {
|
||||
{value: 'one'},
|
||||
{
|
||||
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