mirror of
https://github.com/tenrok/vue-select.git
synced 2026-06-07 07:12:23 +03:00
feat: header, footer, list-header, list-footer slots (#1085)
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<v-select :options="paginated" @search="query => search = query" filterable="false">
|
||||
<li slot="list-footer" class="pagination">
|
||||
<button @click="offset -= 10" :disabled="!hasPrevPage">Prev</button>
|
||||
<button @click="offset += 10" :disabled="!hasNextPage">Next</button>
|
||||
</li>
|
||||
</v-select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import countries from '../data/countries';
|
||||
export default {
|
||||
data: () => ({
|
||||
countries,
|
||||
search: '',
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
}),
|
||||
computed: {
|
||||
filtered () {
|
||||
return this.countries.filter(country => country.includes(this.search));
|
||||
},
|
||||
paginated () {
|
||||
return this.filtered.slice(this.offset, this.limit + this.offset);
|
||||
},
|
||||
hasNextPage () {
|
||||
const nextOffset = this.offset + 10;
|
||||
return Boolean(this.filtered.slice(nextOffset, this.limit + nextOffset).length);
|
||||
},
|
||||
hasPrevPage () {
|
||||
const prevOffset = this.offset - 10;
|
||||
return Boolean(this.filtered.slice(prevOffset, this.limit + prevOffset).length);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pagination {
|
||||
display: flex;
|
||||
margin: .25rem .25rem 0;
|
||||
}
|
||||
.pagination button {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.pagination button:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -119,6 +119,7 @@ module.exports = {
|
||||
children: [
|
||||
['guide/validation', 'Validation'],
|
||||
['guide/selectable', 'Limiting Selections'],
|
||||
['guide/pagination', 'Pagination'],
|
||||
['guide/vuex', 'Vuex'],
|
||||
['guide/ajax', 'AJAX'],
|
||||
['guide/loops', 'Using in Loops'],
|
||||
|
||||
@@ -3,6 +3,39 @@ Vue Select leverages scoped slots to allow for total customization of the presen
|
||||
Slots can be used to change the look and feel of the UI, or to simply swap out text.
|
||||
:::
|
||||
|
||||
## Wrapper
|
||||
|
||||
### `header` <Badge text="3.8.0+" />
|
||||
|
||||
Displayed at the top of the component, above `.vs__dropdown-toggle`.
|
||||
|
||||
- `search {string}` - the current search query
|
||||
- `loading {boolean}` - is the component loading
|
||||
- `searching {boolean}` - is the component searching
|
||||
- `filteredOptions {array}` - options filtered by the search text
|
||||
- `deselect {function}` - function to deselect an option
|
||||
|
||||
```html
|
||||
<slot name="header" v-bind="scope.header" />
|
||||
```
|
||||
|
||||
### `footer` <Badge text="3.8.0+" />
|
||||
|
||||
Displayed at the bottom of the component, below `.vs__dropdown-toggle`.
|
||||
|
||||
When implementing this slot, you'll likely need to use `appendToBody` to position the dropdown.
|
||||
Otherwise content in this slot will affect it's positioning.
|
||||
|
||||
- `search {string}` - the current search query
|
||||
- `loading {boolean}` - is the component loading
|
||||
- `searching {boolean}` - is the component searching
|
||||
- `filteredOptions {array}` - options filtered by the search text
|
||||
- `deselect {function}` - function to deselect an option
|
||||
|
||||
```html
|
||||
<slot name="footer" v-bind="scope.footer" />
|
||||
```
|
||||
|
||||
## Selected Option(s)
|
||||
|
||||
### `selected-option`
|
||||
@@ -91,8 +124,38 @@ attributes : {
|
||||
|
||||
## Dropdown
|
||||
|
||||
### `list-header` <Badge text="3.8.0+" />
|
||||
|
||||
Displayed as the first item in the dropdown. No content by default. Parent element is the `<ul>`,
|
||||
so this slot should contain a root `<li>`.
|
||||
|
||||
- `search {string}` - the current search query
|
||||
- `loading {boolean}` - is the component loading
|
||||
- `searching {boolean}` - is the component searching
|
||||
- `filteredOptions {array}` - options filtered by the search text
|
||||
|
||||
```html
|
||||
<slot name="list-header" v-bind="scope.listHeader" />
|
||||
```
|
||||
|
||||
### `list-footer` <Badge text="3.8.0+" />
|
||||
|
||||
Displayed as the last item in the dropdown. No content by default. Parent element is the `<ul>`,
|
||||
so this slot should contain a root `<li>`.
|
||||
|
||||
- `search {string}` - the current search query
|
||||
- `loading {boolean}` - is the component loading
|
||||
- `searching {boolean}` - is the component searching
|
||||
- `filteredOptions {array}` - options filtered by the search text
|
||||
|
||||
```html
|
||||
<slot name="footer" v-bind="scope.listFooter" />
|
||||
```
|
||||
|
||||
### `option`
|
||||
|
||||
The current option within the dropdown, contained within `<li>`.
|
||||
|
||||
- `option {Object}` - The currently iterated option from `filteredOptions`
|
||||
|
||||
```html
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
::: tip <Badge text="3.8.0+" />
|
||||
Pagination is supported using slots available with Vue Select 3.8 and above.
|
||||
:::
|
||||
|
||||
Pagination can be a super helpful tool when working with large sets of data. If you have 1,000
|
||||
options, the component is going to render 1,000 DOM nodes. That's a lot of nodes to insert/remove,
|
||||
and chances are your user is only interested in a few of them anyways.
|
||||
|
||||
To implement pagination with Vue Select, you can take advantage of the `list-footer` slot. It
|
||||
appears below all other options in the drop down list.
|
||||
|
||||
To make pagination work properly with filtering, you'll have to handle it yourself in the parent.
|
||||
You can use the `filterable` boolean to turn off Vue Select's filtering, and then hook into the
|
||||
`search` event to use the current search query in the parent component.
|
||||
|
||||
<Paginated />
|
||||
|
||||
<<< @/.vuepress/components/Paginated.vue
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
<template>
|
||||
<div :dir="dir" class="v-select" :class="stateClasses">
|
||||
<slot name="header" v-bind="scope.header" />
|
||||
<div :id="`vs${uid}__combobox`" ref="toggle" @mousedown.prevent="toggleDropdown" class="vs__dropdown-toggle" role="combobox" :aria-expanded="dropdownOpen.toString()" :aria-owns="`vs${uid}__listbox`" aria-label="Search for option">
|
||||
|
||||
<div class="vs__selected-options" ref="selectedOptions">
|
||||
@@ -51,9 +52,9 @@
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<transition :name="transition">
|
||||
<ul ref="dropdownMenu" v-if="dropdownOpen" :id="`vs${uid}__listbox`" class="vs__dropdown-menu" role="listbox" @mousedown.prevent="onMousedown" @mouseup="onMouseUp" v-append-to-body>
|
||||
<slot name="list-header" v-bind="scope.listHeader" />
|
||||
<li
|
||||
role="option"
|
||||
v-for="(option, index) in filteredOptions"
|
||||
@@ -72,9 +73,11 @@
|
||||
<li v-if="filteredOptions.length === 0" class="vs__no-options" @mousedown.stop="">
|
||||
<slot name="no-options" v-bind="scope.noOptions">Sorry, no matching options.</slot>
|
||||
</li>
|
||||
<slot name="list-footer" v-bind="scope.listFooter" />
|
||||
</ul>
|
||||
<ul v-else :id="`vs${uid}__listbox`" role="listbox" style="display: none; visibility: hidden;"></ul>
|
||||
</transition>
|
||||
<slot name="footer" v-bind="scope.footer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1001,6 +1004,12 @@
|
||||
* @returns {Object}
|
||||
*/
|
||||
scope () {
|
||||
const listSlot = {
|
||||
search: this.search,
|
||||
loading: this.loading,
|
||||
searching: this.searching,
|
||||
filteredOptions: this.filteredOptions
|
||||
};
|
||||
return {
|
||||
search: {
|
||||
attributes: {
|
||||
@@ -1032,6 +1041,7 @@
|
||||
},
|
||||
noOptions: {
|
||||
search: this.search,
|
||||
loading: this.loading,
|
||||
searching: this.searching,
|
||||
},
|
||||
openIndicator: {
|
||||
@@ -1041,6 +1051,10 @@
|
||||
'class': 'vs__open-indicator',
|
||||
},
|
||||
},
|
||||
listHeader: listSlot,
|
||||
listFooter: listSlot,
|
||||
header: { ...listSlot, deselect: this.deselect },
|
||||
footer: { ...listSlot, deselect: this.deselect }
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -68,8 +68,55 @@ describe('Scoped Slots', () => {
|
||||
await Select.vm.$nextTick();
|
||||
|
||||
expect(noOptions).toHaveBeenCalledWith({
|
||||
loading: false,
|
||||
search: 'something not there',
|
||||
searching: true,
|
||||
})
|
||||
});
|
||||
|
||||
test('header slot props', async () => {
|
||||
const header = jest.fn();
|
||||
const Select = mountDefault({}, {
|
||||
scopedSlots: {header: header},
|
||||
});
|
||||
await Select.vm.$nextTick();
|
||||
expect(Object.keys(header.mock.calls[0][0])).toEqual([
|
||||
'search', 'loading', 'searching', 'filteredOptions', 'deselect',
|
||||
]);
|
||||
});
|
||||
|
||||
test('footer slot props', async () => {
|
||||
const footer = jest.fn();
|
||||
const Select = mountDefault({}, {
|
||||
scopedSlots: {footer: footer},
|
||||
});
|
||||
await Select.vm.$nextTick();
|
||||
expect(Object.keys(footer.mock.calls[0][0])).toEqual([
|
||||
'search', 'loading', 'searching', 'filteredOptions', 'deselect',
|
||||
]);
|
||||
});
|
||||
|
||||
test('list-header slot props', async () => {
|
||||
const header = jest.fn();
|
||||
const Select = mountDefault({}, {
|
||||
scopedSlots: {'list-header': header},
|
||||
});
|
||||
Select.vm.open = true;
|
||||
await Select.vm.$nextTick();
|
||||
expect(Object.keys(header.mock.calls[0][0])).toEqual([
|
||||
'search', 'loading', 'searching', 'filteredOptions',
|
||||
]);
|
||||
});
|
||||
|
||||
test('list-footer slot props', async () => {
|
||||
const footer = jest.fn();
|
||||
const Select = mountDefault({}, {
|
||||
scopedSlots: {'list-footer': footer},
|
||||
});
|
||||
Select.vm.open = true;
|
||||
await Select.vm.$nextTick();
|
||||
expect(Object.keys(footer.mock.calls[0][0])).toEqual([
|
||||
'search', 'loading', 'searching', 'filteredOptions',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user