Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/props.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@ Whether the select should have a search input to filter the options.

Whether the select should allow multiple selections. If `true`, the `v-model` should be an array of string `string[]`.

## isLoading

**Type**: `boolean`

**Default**: `false`

Whether the select should display a loading state. When `true`, the select will show a loading spinner or custom loading content provided via the `loading` slot.

## closeOnSelect

**Type**: `boolean`
Expand Down
20 changes: 20 additions & 0 deletions docs/slots.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,23 @@ Customize the rendered HTML for the clear icon. Please note that the slot is pla
</VueSelect>
</template>
```

## loading

**Type**: `slotProps: {}`

Customize the rendered HTML when the select component is in a loading state. By default, it displays a `<Spinner />` component.

```vue
<template>
<VueSelect
v-model="option"
:options="options"
:is-loading="true"
>
<template #loading>
<MyCustomLoadingComponent />
</template>
</VueSelect>
</template>
```
3 changes: 3 additions & 0 deletions docs/styling.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ List of available CSS variables (pulled from the demo):
--vs-icon-size: 20px;
--vs-icon-color: var(--vs-text-color);

--vs-spinner-color: var(--vs-text-color);
--vs-spinner-size: 20px;

--vs-dropdown-transition: transform 0.25s ease-out;
}
```
Expand Down
5 changes: 4 additions & 1 deletion playground/Playground.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import VueSelect from "../src/Select.vue";
type BookOption = Option<string>;
type UserOption = Option<number> & { username: string };

const activeBook = ref<string>();
const activeBook = ref<string | null>(null);
const activeUsers = ref<number[]>([1, 3]);
const isLoading = ref(false);

const bookOptions: BookOption[] = [
{ label: "Alice's Adventures in Wonderland", value: "alice" },
Expand All @@ -35,6 +36,7 @@ const userOptions: UserOption[] = [
v-model="activeBook"
:options="bookOptions"
:is-multi="false"
:is-loading="isLoading"
placeholder="Pick a book"
/>

Expand All @@ -46,6 +48,7 @@ const userOptions: UserOption[] = [
v-model="activeUsers"
:options="userOptions"
:is-multi="true"
:is-loading="isLoading"
placeholder="Pick users"
/>

Expand Down
17 changes: 16 additions & 1 deletion src/Select.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import ChevronDownIcon from "./icons/ChevronDownIcon.vue";
import XMarkIcon from "./icons/XMarkIcon.vue";
import MenuOption from "./MenuOption.vue";
import Spinner from "./Spinner.vue";

const props = withDefaults(
defineProps<{
Expand Down Expand Up @@ -36,6 +37,11 @@ const props = withDefaults(
* `v-model` directive when using this prop.
*/
isMulti?: boolean;
/**
* When set to true, show a loading spinner inside the select component. This is useful
* when fetching the options asynchronously.
*/
isLoading?: boolean;
/**
* When set to true, clear the search input when an option is selected.
*/
Expand Down Expand Up @@ -91,6 +97,7 @@ const props = withDefaults(
isDisabled: false,
isSearchable: true,
isMulti: false,
isLoading: false,
closeOnSelect: true,
teleport: undefined,
inputId: undefined,
Expand Down Expand Up @@ -428,7 +435,7 @@ onBeforeUnmount(() => {

<div class="indicators-container">
<button
v-if="selectedOptions.length > 0 && isClearable"
v-if="selectedOptions.length > 0 && isClearable && !isLoading"
type="button"
class="clear-button"
tabindex="-1"
Expand All @@ -441,6 +448,7 @@ onBeforeUnmount(() => {
</button>

<button
v-if="!isLoading"
type="button"
class="dropdown-icon"
tabindex="-1"
Expand All @@ -451,6 +459,10 @@ onBeforeUnmount(() => {
<ChevronDownIcon />
</slot>
</button>

<slot name="loading">
<Spinner v-if="isLoading" />
</slot>
</div>
</div>

Expand Down Expand Up @@ -551,6 +563,9 @@ onBeforeUnmount(() => {
--vs-icon-size: 20px;
--vs-icon-color: var(--vs-text-color);

--vs-spinner-color: var(--vs-text-color);
--vs-spinner-size: 20px;

--vs-dropdown-transition: transform 0.25s ease-out;
}
</style>
Expand Down
137 changes: 137 additions & 0 deletions src/Spinner.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<template>
<div className="spinner">
<div
v-for="i in 12"
:key="i"
class="spinner-circle"
/>
</div>
</template>

<style lang="css" scoped>
@keyframes spinner-circle-animation {
0%, 39%, 100% {
opacity: 0;
}

40% {
opacity: 1;
}
}

.spinner {
position: relative;
width: var(--vs-spinner-size);
height: var(--vs-spinner-size);
margin: 0;
padding: 0;
}

.spinner-circle {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}

.spinner-circle:before {
content: '';
display: block;
margin: 0 auto;
width: 15%;
height: 15%;
background-color: var(--vs-spinner-color);
border-radius: 100%;
-webkit-animation: spinner-circle-animation 1.2s infinite ease-in-out both;
animation: spinner-circle-animation 1.2s infinite ease-in-out both;
}

.spinner-circle:nth-child(2) {
transform: rotate(30deg);
}

.spinner-circle:nth-child(3) {
transform: rotate(60deg);
}

.spinner-circle:nth-child(4) {
transform: rotate(90deg);
}

.spinner-circle:nth-child(5) {
transform: rotate(120deg);
}

.spinner-circle:nth-child(6) {
transform: rotate(150deg);
}

.spinner-circle:nth-child(7) {
transform: rotate(180deg);
}

.spinner-circle:nth-child(8) {
transform: rotate(210deg);
}

.spinner-circle:nth-child(9) {
transform: rotate(240deg);
}

.spinner-circle:nth-child(10) {
transform: rotate(270deg);
}

.spinner-circle:nth-child(11) {
transform: rotate(300deg);
}

.spinner-circle:nth-child(12) {
transform: rotate(330deg);
}

.spinner-circle:nth-child(2):before {
animation-delay: -1.1s;
}

.spinner-circle:nth-child(3):before {
animation-delay: -1s;
}

.spinner-circle:nth-child(4):before {
animation-delay: -0.9s;
}

.spinner-circle:nth-child(5):before {
animation-delay: -0.8s;
}

.spinner-circle:nth-child(6):before {
animation-delay: -0.7s;
}

.spinner-circle:nth-child(7):before {
animation-delay: -0.6s;
}

.spinner-circle:nth-child(8):before {
animation-delay: -0.5s;
}

.spinner-circle:nth-child(9):before {
animation-delay: -0.4s;
}

.spinner-circle:nth-child(10):before {
animation-delay: -0.3s;
}

.spinner-circle:nth-child(11):before {
animation-delay: -0.2s;
}

.spinner-circle:nth-child(12):before {
animation-delay: -0.1s;
}
</style>