Skip to content

Commit 49f9c3f

Browse files
authored
feat: add isLoading prop with slot (#124)
* feat(select): add isLoading prop with a loading slot * docs: add isLoading prop and loading slot documentation
1 parent f469f3f commit 49f9c3f

File tree

6 files changed

+188
-2
lines changed

6 files changed

+188
-2
lines changed

docs/props.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,14 @@ Whether the select should have a search input to filter the options.
114114

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

117+
## isLoading
118+
119+
**Type**: `boolean`
120+
121+
**Default**: `false`
122+
123+
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.
124+
117125
## closeOnSelect
118126

119127
**Type**: `boolean`

docs/slots.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,23 @@ Customize the rendered HTML for the clear icon. Please note that the slot is pla
107107
</VueSelect>
108108
</template>
109109
```
110+
111+
## loading
112+
113+
**Type**: `slotProps: {}`
114+
115+
Customize the rendered HTML when the select component is in a loading state. By default, it displays a `<Spinner />` component.
116+
117+
```vue
118+
<template>
119+
<VueSelect
120+
v-model="option"
121+
:options="options"
122+
:is-loading="true"
123+
>
124+
<template #loading>
125+
<MyCustomLoadingComponent />
126+
</template>
127+
</VueSelect>
128+
</template>
129+
```

docs/styling.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ List of available CSS variables (pulled from the demo):
6767
--vs-icon-size: 20px;
6868
--vs-icon-color: var(--vs-text-color);
6969

70+
--vs-spinner-color: var(--vs-text-color);
71+
--vs-spinner-size: 20px;
72+
7073
--vs-dropdown-transition: transform 0.25s ease-out;
7174
}
7275
```

playground/Playground.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import VueSelect from "../src/Select.vue";
77
type BookOption = Option<string>;
88
type UserOption = Option<number> & { username: string };
99
10-
const activeBook = ref<string>();
10+
const activeBook = ref<string | null>(null);
1111
const activeUsers = ref<number[]>([1, 3]);
12+
const isLoading = ref(false);
1213
1314
const bookOptions: BookOption[] = [
1415
{ label: "Alice's Adventures in Wonderland", value: "alice" },
@@ -35,6 +36,7 @@ const userOptions: UserOption[] = [
3536
v-model="activeBook"
3637
:options="bookOptions"
3738
:is-multi="false"
39+
:is-loading="isLoading"
3840
placeholder="Pick a book"
3941
/>
4042

@@ -46,6 +48,7 @@ const userOptions: UserOption[] = [
4648
v-model="activeUsers"
4749
:options="userOptions"
4850
:is-multi="true"
51+
:is-loading="isLoading"
4952
placeholder="Pick users"
5053
/>
5154

src/Select.vue

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
55
import ChevronDownIcon from "./icons/ChevronDownIcon.vue";
66
import XMarkIcon from "./icons/XMarkIcon.vue";
77
import MenuOption from "./MenuOption.vue";
8+
import Spinner from "./Spinner.vue";
89
910
const props = withDefaults(
1011
defineProps<{
@@ -36,6 +37,11 @@ const props = withDefaults(
3637
* `v-model` directive when using this prop.
3738
*/
3839
isMulti?: boolean;
40+
/**
41+
* When set to true, show a loading spinner inside the select component. This is useful
42+
* when fetching the options asynchronously.
43+
*/
44+
isLoading?: boolean;
3945
/**
4046
* When set to true, clear the search input when an option is selected.
4147
*/
@@ -91,6 +97,7 @@ const props = withDefaults(
9197
isDisabled: false,
9298
isSearchable: true,
9399
isMulti: false,
100+
isLoading: false,
94101
closeOnSelect: true,
95102
teleport: undefined,
96103
inputId: undefined,
@@ -428,7 +435,7 @@ onBeforeUnmount(() => {
428435

429436
<div class="indicators-container">
430437
<button
431-
v-if="selectedOptions.length > 0 && isClearable"
438+
v-if="selectedOptions.length > 0 && isClearable && !isLoading"
432439
type="button"
433440
class="clear-button"
434441
tabindex="-1"
@@ -441,6 +448,7 @@ onBeforeUnmount(() => {
441448
</button>
442449

443450
<button
451+
v-if="!isLoading"
444452
type="button"
445453
class="dropdown-icon"
446454
tabindex="-1"
@@ -451,6 +459,10 @@ onBeforeUnmount(() => {
451459
<ChevronDownIcon />
452460
</slot>
453461
</button>
462+
463+
<slot name="loading">
464+
<Spinner v-if="isLoading" />
465+
</slot>
454466
</div>
455467
</div>
456468

@@ -551,6 +563,9 @@ onBeforeUnmount(() => {
551563
--vs-icon-size: 20px;
552564
--vs-icon-color: var(--vs-text-color);
553565
566+
--vs-spinner-color: var(--vs-text-color);
567+
--vs-spinner-size: 20px;
568+
554569
--vs-dropdown-transition: transform 0.25s ease-out;
555570
}
556571
</style>

src/Spinner.vue

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<template>
2+
<div className="spinner">
3+
<div
4+
v-for="i in 12"
5+
:key="i"
6+
class="spinner-circle"
7+
/>
8+
</div>
9+
</template>
10+
11+
<style lang="css" scoped>
12+
@keyframes spinner-circle-animation {
13+
0%, 39%, 100% {
14+
opacity: 0;
15+
}
16+
17+
40% {
18+
opacity: 1;
19+
}
20+
}
21+
22+
.spinner {
23+
position: relative;
24+
width: var(--vs-spinner-size);
25+
height: var(--vs-spinner-size);
26+
margin: 0;
27+
padding: 0;
28+
}
29+
30+
.spinner-circle {
31+
width: 100%;
32+
height: 100%;
33+
position: absolute;
34+
left: 0;
35+
top: 0;
36+
}
37+
38+
.spinner-circle:before {
39+
content: '';
40+
display: block;
41+
margin: 0 auto;
42+
width: 15%;
43+
height: 15%;
44+
background-color: var(--vs-spinner-color);
45+
border-radius: 100%;
46+
-webkit-animation: spinner-circle-animation 1.2s infinite ease-in-out both;
47+
animation: spinner-circle-animation 1.2s infinite ease-in-out both;
48+
}
49+
50+
.spinner-circle:nth-child(2) {
51+
transform: rotate(30deg);
52+
}
53+
54+
.spinner-circle:nth-child(3) {
55+
transform: rotate(60deg);
56+
}
57+
58+
.spinner-circle:nth-child(4) {
59+
transform: rotate(90deg);
60+
}
61+
62+
.spinner-circle:nth-child(5) {
63+
transform: rotate(120deg);
64+
}
65+
66+
.spinner-circle:nth-child(6) {
67+
transform: rotate(150deg);
68+
}
69+
70+
.spinner-circle:nth-child(7) {
71+
transform: rotate(180deg);
72+
}
73+
74+
.spinner-circle:nth-child(8) {
75+
transform: rotate(210deg);
76+
}
77+
78+
.spinner-circle:nth-child(9) {
79+
transform: rotate(240deg);
80+
}
81+
82+
.spinner-circle:nth-child(10) {
83+
transform: rotate(270deg);
84+
}
85+
86+
.spinner-circle:nth-child(11) {
87+
transform: rotate(300deg);
88+
}
89+
90+
.spinner-circle:nth-child(12) {
91+
transform: rotate(330deg);
92+
}
93+
94+
.spinner-circle:nth-child(2):before {
95+
animation-delay: -1.1s;
96+
}
97+
98+
.spinner-circle:nth-child(3):before {
99+
animation-delay: -1s;
100+
}
101+
102+
.spinner-circle:nth-child(4):before {
103+
animation-delay: -0.9s;
104+
}
105+
106+
.spinner-circle:nth-child(5):before {
107+
animation-delay: -0.8s;
108+
}
109+
110+
.spinner-circle:nth-child(6):before {
111+
animation-delay: -0.7s;
112+
}
113+
114+
.spinner-circle:nth-child(7):before {
115+
animation-delay: -0.6s;
116+
}
117+
118+
.spinner-circle:nth-child(8):before {
119+
animation-delay: -0.5s;
120+
}
121+
122+
.spinner-circle:nth-child(9):before {
123+
animation-delay: -0.4s;
124+
}
125+
126+
.spinner-circle:nth-child(10):before {
127+
animation-delay: -0.3s;
128+
}
129+
130+
.spinner-circle:nth-child(11):before {
131+
animation-delay: -0.2s;
132+
}
133+
134+
.spinner-circle:nth-child(12):before {
135+
animation-delay: -0.1s;
136+
}
137+
</style>

0 commit comments

Comments
 (0)