unovue / shadcn-vue

Vue port of shadcn-ui
https://www.shadcn-vue.com/
MIT License
5.14k stars 301 forks source link

[Feature]: Multi Select Component #851

Open Uhasith opened 1 week ago

Uhasith commented 1 week ago

Describe the feature

Currently, the Select component only supports single-item selection. Could we add a variant for selecting multiple items?

imfaizanyousaf commented 1 week ago

yes similar to this one but in vue https://shadcn-extension.vercel.app/docs/multi-select

sadeghbarati commented 3 days ago

Related https://github.com/unovue/shadcn-vue/issues/688

@Uhasith I saw your comment can you create a Stackblitz playground so we could see the problem, thanks

Uhasith commented 3 days ago

This is the Demo @sadeghbarati

https://stackblitz.com/edit/vitejs-vite-7cpzyx?file=src%2Fcomponents%2FHelloWorld.vue

sadeghbarati commented 3 days ago

@Uhasith Check the Stackblitz URL again it's 404: Not Found

Uhasith commented 3 days ago

@Uhasith Check the Stackblitz URL again it's 404: Not Found

I'm not a paid user. I guess they don't let me share it. This is the Github Repo Link @sadeghbarati

https://github.com/Uhasith/Shadcn-MultiSelect

Uhasith commented 3 days ago

@Uhasith Check the Stackblitz URL again it's 404: Not Found

This should Work @sadeghbarati

[stackblitz.com/edit/vitejs-vite-1qlnyc](https://stackblitz.com/edit/vitejs-vite-1qlnyc)

sadeghbarati commented 3 days ago

@Uhasith Check this playground

https://stackblitz.com/edit/github-rxs6gk?file=src%2Fcomponents%2FDialogModal.vue

Your issue was using two Portal components together

Uhasith commented 3 days ago

@Uhasith Check this playground

https://stackblitz.com/edit/github-rxs6gk?file=src%2Fcomponents%2FDialogModal.vue

Your issue was using two Portal components together

Wow, mate, that was quick! Could you please explain the fix for this issue? Also, could you push the changes to my repo? @sadeghbarati

Uhasith commented 3 days ago

@Uhasith Check this playground

https://stackblitz.com/edit/github-rxs6gk?file=src%2Fcomponents%2FDialogModal.vue

Your issue was using two Portal components together

I’m new to StackBlitz, so I’m not sure which part you fixed in the codebase, mate. If you don’t mind, could you give me a quick overview? Really appreciate your response ❤️‍🔥 @sadeghbarati

sadeghbarati commented 3 days ago

Use this tool to find out the difference, I changed MultiSelect.vue and DialogModal.vue

https://it-tools.tech/text-diff

On the left-side pastes your code and right-side paste modified code

Uhasith commented 3 days ago

Thank you so much! I hope this component will help others who have issues with multiple select as well. ❤️ @sadeghbarati

imfaizanyousaf commented 3 days ago

I created MultiSelect component by modifying the FacetedFilter. It can also fetch the results from the database using an optionsUrl prop

image

Hope this helps!

MultiSelect.vue

<script setup>
import { ref, onMounted } from "vue";
import { ChevronDown, Check } from "lucide-vue-next";
import { debounce } from "@/lib/utils";
import { Badge } from "@/Components/ui/badge";
import { Button } from "@/Components/ui/button";
import {
    Command,
    CommandEmpty,
    CommandGroup,
    CommandInput,
    CommandItem,
    CommandList,
    CommandSeparator,
} from "@/Components/ui/command";
import {
    Popover,
    PopoverContent,
    PopoverTrigger,
} from "@/Components/ui/popover";
import axios from "axios";
import Label from "@/Components/ui/label/Label.vue";

const model = defineModel();

const props = defineProps({
    title: {
        type: String,
        required: true,
    },
    options: {
        type: Array,
        default: () => [],
    },
    optionsUrl: {
        type: String,
        default: null,
    },
});

const selectedValues = ref(new Set(model.value));
const optionsList = ref([]);
const q = ref("");

onMounted(() => {
    updateOptionList("");
});

const filterFunction = (e) => {
    updateOptionList(e.target.value);
};

const updateOptionList = debounce(async (query) => {
    if (!props.optionsUrl) return;

    q.value = query;
    const { data } = await axios.get(props.optionsUrl, {
        params: { q: query },
    });
    optionsList.value = data;
});

const toggleSelection = (optionValue) => {
    if (selectedValues.value.has(optionValue)) {
        selectedValues.value.delete(optionValue);
        model.value = model.value.filter((value) => value !== optionValue);
    } else {
        selectedValues.value.add(optionValue);
        model.value.push(optionValue);
    }
};

const clearSelections = () => {
    selectedValues.value.clear();
};
</script>

<template>
    <div class="flex flex-col gap-2">
        <Label>{{ title }}</Label>
        <Popover>
            <PopoverTrigger as-child>
                <Button
                    variant="outline"
                    class="h-10 flex justify-between text-muted-foreground font-normal"
                >
                    <span v-if="selectedValues.size < 1">{{ title }}</span>
                    <template v-if="selectedValues.size > 0">
                        <Badge
                            variant="secondary"
                            class="rounded-sm px-1 font-normal lg:hidden"
                        >
                            {{ selectedValues.size }} selected
                        </Badge>
                        <div class="hidden space-x-1 lg:flex">
                            <Badge
                                v-if="selectedValues.size > 3"
                                variant="secondary"
                                class="rounded-sm px-1 font-normal"
                            >
                                {{ selectedValues.size }} selected
                            </Badge>
                            <template v-else>
                                <Badge
                                    v-for="option in optionsList.filter(
                                        (option) =>
                                            selectedValues.has(option.value)
                                    )"
                                    :key="option.value"
                                    variant="secondary"
                                    class="rounded-sm px-1 font-normal"
                                >
                                    {{ option.label }}
                                </Badge>
                            </template>
                        </div>
                    </template>
                    <ChevronDown class="ml-2 h-4 w-4" />
                </Button>
            </PopoverTrigger>
            <PopoverContent class="w-[200px] p-0" align="start">
                <Command
                    :should-filter="props.optionsUrl == null"
                    :filter-function="
                        props.optionsUrl
                            ? null
                            : (list, term) =>
                                  list.filter((i) =>
                                      i.label
                                          .toLowerCase()
                                          .includes(term.toLowerCase())
                                  )
                    "
                >
                    <CommandInput
                        :placeholder="title"
                        v-model="q"
                        @input="filterFunction"
                    />
                    <CommandList>
                        <CommandEmpty>
                            {{
                                optionsUrl && q == ""
                                    ? "Start Typing to search"
                                    : "No results found."
                            }}
                        </CommandEmpty>
                        <CommandGroup>
                            <CommandItem
                                v-for="option in optionsList"
                                :key="option.value"
                                :value="option"
                                @select="toggleSelection(option.value)"
                            >
                                <div
                                    :class="[
                                        'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
                                        selectedValues.has(option.value)
                                            ? 'bg-primary text-primary-foreground'
                                            : 'opacity-50 [&_svg]:invisible',
                                    ]"
                                >
                                    <Check class="h-4 w-4" />
                                </div>
                                <span>{{ option.label }}</span>
                            </CommandItem>
                        </CommandGroup>
                        <template v-if="selectedValues.size > 0">
                            <CommandSeparator />
                            <CommandGroup>
                                <CommandItem
                                    class="justify-center text-center"
                                    @select="clearSelections"
                                >
                                    Clear Selection
                                </CommandItem>
                            </CommandGroup>
                        </template>
                    </CommandList>
                </Command>
            </PopoverContent>
        </Popover>
    </div>
</template>