typecode
VueSearch Filter/SearchFilter.vue
1<script setup lang="ts">
2import { ref, computed, watch, onMounted } from "vue"
3
4interface Item {
5 id: number
6 title: string
7 category: string
8 tags: string[]
9}
10
11const props = defineProps<{
12 items: Item[]
13 placeholder?: string
14}>()
15
16const emit = defineEmits<{
17 select: [item: Item]
18 search: [query: string]
19}>()
20
21const query = ref("")
22const selectedCategory = ref<string | null>(null)
23const isLoading = ref(false)
24const inputRef = ref<HTMLInputElement | null>(null)
25
26const categories = computed(() => {
27 const unique = new Set(props.items.map((item) => item.category))
28 return ["All", ...Array.from(unique).sort()]
29})
30
31const filteredItems = computed(() => {
32 let results = props.items
33
34 if (selectedCategory.value && selectedCategory.value !== "All") {
35 results = results.filter(
36 (item) => item.category === selectedCategory.value
37 )
38 }
39
40 if (query.value.trim()) {
41 const search = query.value.toLowerCase()
42 results = results.filter(
43 (item) =>
44 item.title.toLowerCase().includes(search) ||
45 item.tags.some((tag) => tag.toLowerCase().includes(search))
46 )
47 }
48
49 return results
50})
51
52const resultCount = computed(() => filteredItems.value.length)
53
54let debounceTimer: ReturnType<typeof setTimeout>
55
56watch(query, (newQuery) => {
57 clearTimeout(debounceTimer)
58 isLoading.value = true
59 debounceTimer = setTimeout(() => {
60 emit("search", newQuery)
61 isLoading.value = false
62 }, 300)
63})
64
65function handleSelect(item: Item): void {
66 emit("select", item)
67}
68
69function clearFilters(): void {
70 query.value = ""
71 selectedCategory.value = null
72 inputRef.value?.focus()
73}
74
75onMounted(() => {
76 inputRef.value?.focus()
77})
78</script>
79
80<template>
81 <div class="search-filter">
82 <div class="controls">
83 <input
84 ref="inputRef"
85 v-model="query"
86 type="text"
87 :placeholder="placeholder ?? 'Search...'"
88 class="search-input"
89 />
90 <select v-model="selectedCategory" class="category-select">
91 <option
92 v-for="cat in categories"
93 :key="cat"
94 :value="cat === 'All' ? null : cat"
95 >
96 {{ cat }}
97 </option>
98 </select>
99 <button v-if="query || selectedCategory" @click="clearFilters">
100 Clear
101 </button>
102 </div>
103
104 <p class="result-count">{{ resultCount }} results</p>
105
106 <TransitionGroup name="list" tag="ul" class="item-list">
107 <li
108 v-for="item in filteredItems"
109 :key="item.id"
110 class="item"
111 @click="handleSelect(item)"
112 >
113 <span class="title">{{ item.title }}</span>
114 <span class="category">{{ item.category }}</span>
115 <span v-for="tag in item.tags" :key="tag" class="tag">
116 {{ tag }}
117 </span>
118 </li>
119 </TransitionGroup>
120
121 <p v-if="resultCount === 0" class="empty">No items match your search.</p>
122 </div>
123</template>
0WPM
100%Accuracy
00:00Time
0%
Progress