ΠΡΠΎΡ ΠΏΡΠΎΠ΅ΠΊΡ ΡΠ΅Π°Π»ΠΈΠ·ΡΠ΅Ρ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ Π½Π° Vue 3 Π΄Π»Ρ ΠΎΡΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΡ ΠΏΡΠ΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΠΉ ΠΈΠ· API, Π²ΠΊΠ»ΡΡΠ°Ρ Π·Π°Π΄Π΅ΡΠΆΠΊΡ Π·Π°ΠΏΡΠΎΡΠΎΠ² (debounce), Π°Π΄Π°ΠΏΡΠΈΠ²Π½ΡΠΉ ΠΈΠ½ΡΠ΅ΡΡΠ΅ΠΉΡ, ΠΎΠ±ΡΠ°Π±ΠΎΡΠΊΡ ΠΎΡΠΈΠ±ΠΎΠΊ ΠΈ Π·Π°Π³ΡΡΠ·ΠΎΠΊ. ΠΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ ΠΏΡΠ΅Π΄ΠΎΡΡΠ°Π²Π»ΡΠ΅Ρ ΠΏΠ΅ΡΠ΅ΠΈΡΠΏΠΎΠ»ΡΠ·ΡΠ΅ΠΌΡΡ Π°ΡΡ ΠΈΡΠ΅ΠΊΡΡΡΡ Π΄Π»Ρ API-Π²Π·Π°ΠΈΠΌΠΎΠ΄Π΅ΠΉΡΡΠ²ΠΈΡ ΠΈ ΠΏΠΎΠ΄Π΄Π΅ΡΠΆΠΈΠ²Π°Π΅Ρ Π΄ΠΈΠ½Π°ΠΌΠΈΡΠ΅ΡΠΊΡΡ Π½Π°ΡΡΡΠΎΠΉΠΊΡ Π΄Π»Ρ ΡΠ°Π·Π½ΡΡ ΠΈΡΡΠΎΡΠ½ΠΈΠΊΠΎΠ² Π΄Π°Π½Π½ΡΡ .
- ΠΠ°Π΄Π΅ΡΠΆΠΊΠ° Π·Π°ΠΏΡΠΎΡΠΎΠ² ΠΊ API: Π£ΠΌΠ΅Π½ΡΡΠ°Π΅Ρ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π·Π°ΠΏΡΠΎΡΠΎΠ², ΠΎΠΆΠΈΠ΄Π°Ρ Π·Π°Π²Π΅ΡΡΠ΅Π½ΠΈΡ Π²Π²ΠΎΠ΄Π°.
- ΠΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°Π½ΠΈΠ΅ composable: ΠΠ΅Π³ΠΊΠΎ ΠΈΠ½ΡΠ΅Π³ΡΠΈΡΡΠ΅ΡΡΡ Π² Π΄ΡΡΠ³ΠΈΠ΅ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½ΡΡ Π΄Π»Ρ ΡΠ°Π±ΠΎΡΡ Ρ API.
- ΠΠΎΠ΄Π΄Π΅ΡΠΆΠΊΠ° ΡΠ°Π·Π½ΡΡ API: ΠΠΎΠ·Π²ΠΎΠ»ΡΠ΅Ρ Π½Π°ΡΡΡΠ°ΠΈΠ²Π°ΡΡ ΡΡΠ°Π½ΡΡΠΎΡΠΌΠ°ΡΠΈΡ Π΄Π°Π½Π½ΡΡ ΠΈ ΠΏΡΠΎΠ²Π΅ΡΠΊΡ ΠΏΠ°ΡΠ°ΠΌΠ΅ΡΡΠΎΠ² Π·Π°ΠΏΡΠΎΡΠ°.
- ΠΠ΄Π°ΠΏΡΠΈΠ²Π½ΡΠΉ ΠΈΠ½ΡΠ΅ΡΡΠ΅ΠΉΡ: UI ΠΊΠΎΡΡΠ΅ΠΊΡΠ½ΠΎ ΠΎΡΠΎΠ±ΡΠ°ΠΆΠ°Π΅ΡΡΡ Π½Π° Π²ΡΠ΅Ρ ΡΡΡΡΠΎΠΉΡΡΠ²Π°Ρ .
- ΠΠ±ΡΠ°Π±ΠΎΡΠΊΠ° ΠΎΡΠΈΠ±ΠΎΠΊ: Π£Π΄ΠΎΠ±Π½ΡΠ΅ ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΡ Π΄Π»Ρ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Π΅ΠΉ.
- Π‘ΠΎΡΡΠΎΡΠ½ΠΈΠ΅ Π·Π°Π³ΡΡΠ·ΠΊΠΈ: ΠΠΎΠΊΠ°Π·ΡΠ²Π°Π΅Ρ ΠΈΠ½Π΄ΠΈΠΊΠ°ΡΠΎΡ Π·Π°Π³ΡΡΠ·ΠΊΠΈ.
- ΠΠ΅ΡΠ΅ΠΈΡΠΏΠΎΠ»ΡΠ·ΡΠ΅ΠΌΠΎΡΡΡ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½ΡΠ°: ΠΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡΡ ΠΏΠΎΠ΄ΡΡΡΠΎΠΈΡΡ ΡΠ°Π΄ΠΆΠ΅ΡΡ ΠΏΠΎΠ΄ ΡΠ²ΠΎΠΈ Π½ΡΠΆΠ΄Ρ
- Π Π΅Π°Π»ΠΈΠ·Π°ΡΠΈΡ Π±Π΅Π· 3rd-party: Π Π΅Π°Π»ΠΈΠ·Π°ΡΠΈΡ ΡΠΎΠ»ΡΠΊΠΎ Π½Π° Vue+TS
project
βββ src
β βββ components
β β βββ VSuggest.vue
β β βββ VSuggestItem.vue
β β βββ VTag.vue
β β βββ VLoader.vue
β β βββ VCompanyEntity.vue
β β βββ VUserEntity.vue
β βββ composables
β β βββ useGetFetchSuggestions.ts
β βββ utils
β β βββ debounce.ts
β β βββ clickOutside.ts
β βββ types
β β βββ index.ts
β βββ App.vue
β βββ main.ts
βββ public
β βββ assets
β βββ images
β βββ noPhoto.png
β βββ styles
β βββ style.css
β βββ suggestItemStyle.css
βββ package.json
βββ README.md
Π‘ΡΠ°ΡΡΠΎΠ²ΡΠΉ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ
<template>
<VSuggest :inputLabel="'ΠΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ ΠΈΠ»ΠΈ ΠΊΠΎΠΌΠΏΠ°Π½ΠΈΡ'"
:tagAmount="1"
:placeholderSuggest="'ΠΠ²Π΅Π΄ΠΈΡΠ΅ Π»ΠΎΠ³ΠΈΠ½'"
:apiUrl="'https://habr.com/kek/v2/publication/suggest-mention'"
/>
</template>- ΠΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡΡ Π½Π°ΡΡΡΠΎΠΉΠΊΠΈ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²Π° ΡΡΡΠ½ΠΎΡΡΠ΅ΠΉ Π΄ΠΎΠΏΡΡΡΠΈΠΌΡΡ Π΄Π»Ρ Π²ΡΠ±ΠΎΡΠ°
- ΠΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡΡ Π½Π°ΡΡΡΠΎΠΉΠΊΠΈ placeholder ΠΈ label
- ΠΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡΡ ΠΏΠ΅ΡΠ΅Π΄Π°ΡΠΈ Π΄ΡΡΠ³ΠΈΡ URl, Π½ΠΎ Ρ ΠΈΠ΄Π΅Π½ΡΠΈΡΠ½ΠΎΠΉ ΡΡΡΡΠΊΡΡΡΠΎΠΉ
ΠΡΠ½ΠΎΠ²Π½ΠΎΠΉ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ, ΠΎΠ±ΡΠ°Π±Π°ΡΡΠ²Π°ΡΡΠΈΠΉ Π²Π²ΠΎΠ΄ ΠΈ ΠΎΡΠΎΠ±ΡΠ°ΠΆΠ°ΡΡΠΈΠΉ ΡΠΏΠΈΡΠΎΠΊ ΠΏΡΠ΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΠΉ.
- ΠΠΎΠ»Π΅ Π²Π²ΠΎΠ΄Π° Ρ Π·Π°Π΄Π΅ΡΠΆΠΊΠΎΠΉ Π·Π°ΠΏΡΠΎΡΠΎΠ² (debounced fetchSuggestions).
- ΠΠ½ΡΠ΅Π³ΡΠ°ΡΠΈΡ Ρ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½ΡΠ°ΠΌΠΈ
VLoaderΠΈVSuggestItem. - ΠΠΈΠ½Π°ΠΌΠΈΡΠ΅ΡΠΊΠ°Ρ ΠΎΠ±ΡΠ°Π±ΠΎΡΠΊΠ° ΠΎΡΠΈΠ±ΠΎΠΊ ΠΈ ΠΏΡΡΡΡΡ ΡΠΎΡΡΠΎΡΠ½ΠΈΠΉ.
<template>
<main>
<section>
<div>
<label for="suggest" class="label_description">
<span>*</span>
ΠΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ ΠΈΠ»ΠΈ ΠΊΠΎΠΌΠΏΠ°Π½ΠΈΡ
</label>
<br />
<div class="input_block">
<VTag @removeItem="removeItem" :listItem="listItem" />
<input
id="suggest"
type="text"
ref="inputUserValue"
v-model="query"
@input="debouncedFetchSuggestions"
:disabled="listItem.length >= 1"
:placeholder="listItem.length >= 1 ? '' : 'ΠΠ²Π΅Π΄ΠΈΡΠ΅ Π»ΠΎΠ³ΠΈΠ½'"
@keydown.enter.prevent="addItemToList(query)"
aria-autocomplete="list"
aria-expanded="true"
/>
<VLoader v-if="isLoading" />
</div>
<p v-if="error" class="error-message">{{ error }}</p>
</div>
<VSuggestItem :responseData="responseData" @selectedItem="addItemToList" v-if="flagActiveList" />
</section>
</main>
</template>ΠΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ Π΄Π»Ρ ΠΎΡΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΡ ΡΠΏΠΈΡΠΊΠ° ΠΏΡΠ΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΠΉ.
responseData: ΠΌΠ°ΡΡΠΈΠ² ΠΏΡΠ΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΠΉ ΠΈΠ· API.
@selectedItem: ΠΎΡΠΏΡΠ°Π²Π»ΡΠ΅Ρ Π²ΡΠ±ΡΠ°Π½Π½ΠΎΠ΅ ΠΏΡΠ΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΠ΅ ΠΎΠ±ΡΠ°ΡΠ½ΠΎ Π² ΡΠΎΠ΄ΠΈΡΠ΅Π»ΡΡΠΊΠΈΠΉ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ.
ΠΡΠΎΡΡΠΎΠΉ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ, ΠΎΡΠΎΠ±ΡΠ°ΠΆΠ°ΡΡΠΈΠΉ ΠΈΠ½Π΄ΠΈΠΊΠ°ΡΠΎΡ Π·Π°Π³ΡΡΠ·ΠΊΠΈ.
ΠΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ Π΄Π»Ρ ΠΎΡΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΡ Π²ΡΠ±ΡΠ°Π½Π½ΡΡ ΡΠ΅Π³ΠΎΠ² Ρ Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡΡΡ ΠΈΡ ΡΠ΄Π°Π»Π΅Π½ΠΈΡ.
ΠΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ Π΄Π»Ρ ΠΎΡΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΡ Π΄Π°Π½Π½ΡΡ ΠΊΠΎΠ³Π΄Π° ΡΠΈΠΏ = ΠΊΠΎΠΌΠΏΠ°Π½ΠΈΡ
ΠΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ Π΄Π»Ρ ΠΎΡΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΡ Π΄Π°Π½Π½ΡΡ ΠΊΠΎΠ³Π΄Π° ΡΠΈΠΏ != ΠΊΠΎΠΌΠΏΠ°Π½ΠΈΡ
Composable Π΄Π»Ρ Π²Π·Π°ΠΈΠΌΠΎΠ΄Π΅ΠΉΡΡΠ²ΠΈΡ Ρ API.
interface FetchSuggestionsOptions<T> {
apiUrl: string; // URL API
transformResponse?: (data: any) => T[]; // ΠΠΎΠ³ΠΈΠΊΠ° ΡΡΠ°Π½ΡΡΠΎΡΠΌΠ°ΡΠΈΠΈ Π΄Π°Π½Π½ΡΡ
validateQueryParams?: (query: string) => boolean; // ΠΠΎΠ³ΠΈΠΊΠ° ΠΏΡΠΎΠ²Π΅ΡΠΊΠΈ Π·Π°ΠΏΡΠΎΡΠ°
}{
isLoading: Ref<boolean>,
error: Ref<string | null>,
responseData: Ref<T[]>,
fetchSuggestions: (query: string) => void
}ΠΡΠΈΠΌΠ΅Ρ ΠΈΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°Π½ΠΈΡ:
const { isLoading, error, responseData, fetchSuggestions } = useGetFetchSuggestions({
apiUrl: "https://api.example.com/suggestions",
transformResponse: (data) => data.items,
validateQueryParams: (query) => query.length >= 3,
});Π€ΡΠ½ΠΊΡΠΈΡ Π΄Π»Ρ Π·Π°Π΄Π΅ΡΠΆΠΊΠΈ Π²ΡΠΏΠΎΠ»Π½Π΅Π½ΠΈΡ Π·Π°ΠΏΡΠΎΡΠΎΠ².
ΠΡΠΈΠΌΠ΅Ρ ΠΊΠΎΠ΄Π°:
export function debounce<T extends (...args: any[]) => void>(fn: T, delay: number): T {
let timeout: ReturnType<typeof setTimeout>;
return ((...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
}) as T;
}.suggest-container {
width: clamp(20%, 35%, 80%);
border-radius: 4px;
box-shadow: rgba(0, 0, 0, 0.25) 0px 0.0625em 0.0625em,
rgba(0, 0, 0, 0.25) 0px 0.125em 0.5em,
rgba(255, 255, 255, 0.1) 0px 0px 0px 1px inset;
overflow-y: auto;
}
.avatar {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
}
@media (max-width: 768px) {
.suggest-container {
width: 80%;
}
}const { isLoading, error, responseData, fetchSuggestions } = useGetFetchSuggestions({
apiUrl: "https://habr.com/kek/v2/publication/suggest-mention",
transformResponse: (data) => data.data,
validateQueryParams: (query) => query.trim().length >= 3,
});- 400 Error: ΠΡΠΎΠ±ΡΠ°ΠΆΠ°Π΅Ρ ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅ "ΠΠ΅ΠΊΠΎΡΡΠ΅ΠΊΡΠ½ΡΠΉ Π·Π°ΠΏΡΠΎΡ. ΠΠΎΠ²ΡΠΎΡΠΈΡΠ΅ ΠΏΠΎΠΏΡΡΠΊΡ."
- 500 Error: ΠΡΠΎΠ±ΡΠ°ΠΆΠ°Π΅Ρ ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅ "ΠΡΠΈΠ±ΠΊΠ° ΡΠ΅ΡΠ²Π΅ΡΠ°. ΠΠΎΠ²ΡΠΎΡΠΈΡΠ΅ ΠΏΠΎΠ·ΠΆΠ΅."
git clone https://github.com/eldenhard/suggest.gitnpm installnpm run dev