Skip to content

Commit

Permalink
feat: header, footer, list-header, list-footer slots (#1085)
Browse files Browse the repository at this point in the history
  • Loading branch information
sagalbot authored Mar 10, 2020
1 parent 3c54634 commit b2f388b
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 1 deletion.
49 changes: 49 additions & 0 deletions docs/.vuepress/components/Paginated.vue
Original file line number Diff line number Diff line change
@@ -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>
1 change: 1 addition & 0 deletions docs/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
63 changes: 63 additions & 0 deletions docs/api/slots.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions docs/guide/pagination.md
Original file line number Diff line number Diff line change
@@ -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
16 changes: 15 additions & 1 deletion src/components/Select.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down Expand Up @@ -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"
Expand All @@ -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>

Expand Down Expand Up @@ -1001,6 +1004,12 @@
* @returns {Object}
*/
scope () {
const listSlot = {
search: this.search,
loading: this.loading,
searching: this.searching,
filteredOptions: this.filteredOptions
};
return {
search: {
attributes: {
Expand Down Expand Up @@ -1032,6 +1041,7 @@
},
noOptions: {
search: this.search,
loading: this.loading,
searching: this.searching,
},
openIndicator: {
Expand All @@ -1041,6 +1051,10 @@
'class': 'vs__open-indicator',
},
},
listHeader: listSlot,
listFooter: listSlot,
header: { ...listSlot, deselect: this.deselect },
footer: { ...listSlot, deselect: this.deselect }
};
},
Expand Down
47 changes: 47 additions & 0 deletions tests/unit/Slots.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
]);
});
});

0 comments on commit b2f388b

Please sign in to comment.