Skip to content

Commit

Permalink
feat: add isLoading prop with slot (#124)
Browse files Browse the repository at this point in the history
* feat(select): add isLoading prop with a loading slot

* docs: add isLoading prop and loading slot documentation
  • Loading branch information
TotomInc authored Sep 30, 2024
1 parent f469f3f commit 49f9c3f
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 2 deletions.
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>

0 comments on commit 49f9c3f

Please sign in to comment.