feat: add CAutoComplete component with basic functionality and remote search support
- Implemented CAutoComplete.vue with props for modelValue, options, debounceWait, loading, and more. - Added search functionality with debounce for remote city suggestions. - Created AutoComplete.vue page for documentation and examples. - Updated CSelect.vue to support remote searching and caching of selected values. - Enhanced CMultiSelect.vue to handle object comparisons for selected values.main
parent
3a8489d818
commit
18eae8fa23
@ -0,0 +1,254 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, ref } from 'vue'
|
||||
|
||||
import { CAutoComplete, CFormField, CSeparator } from '@/index'
|
||||
import CCodeBlock from '@/documentation/CCodeBlock.vue'
|
||||
|
||||
const cities = [
|
||||
'Bandung',
|
||||
'Bekasi',
|
||||
'Bogor',
|
||||
'Denpasar',
|
||||
'Jakarta',
|
||||
'Makassar',
|
||||
'Medan',
|
||||
'Semarang',
|
||||
'Surabaya',
|
||||
'Yogyakarta',
|
||||
]
|
||||
|
||||
const destination = ref('Jakarta')
|
||||
const department = ref('')
|
||||
const smallValue = ref('Bandung')
|
||||
const largeValue = ref('Surabaya')
|
||||
const remoteCity = ref('')
|
||||
const remoteOptions = ref<string[]>([])
|
||||
const remoteLoading = ref(false)
|
||||
let remoteTimer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
function searchCities(query: string) {
|
||||
if (remoteTimer !== undefined) clearTimeout(remoteTimer)
|
||||
if (!query) {
|
||||
remoteOptions.value = []
|
||||
remoteLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
remoteLoading.value = true
|
||||
remoteTimer = setTimeout(() => {
|
||||
remoteOptions.value = cities.filter((city) =>
|
||||
city.toLocaleLowerCase().includes(query.toLocaleLowerCase()),
|
||||
)
|
||||
remoteLoading.value = false
|
||||
}, 600)
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (remoteTimer !== undefined) clearTimeout(remoteTimer)
|
||||
})
|
||||
|
||||
const basicJavaScript = `const cities = [
|
||||
'Bandung',
|
||||
'Bekasi',
|
||||
'Bogor',
|
||||
'Denpasar',
|
||||
'Jakarta',
|
||||
'Makassar',
|
||||
'Medan',
|
||||
'Semarang',
|
||||
'Surabaya',
|
||||
'Yogyakarta',
|
||||
]
|
||||
|
||||
const destination = ref('Jakarta')`
|
||||
|
||||
const basicUsage = `<CAutoComplete
|
||||
v-model="destination"
|
||||
:options="cities"
|
||||
placeholder="Enter a destination"
|
||||
clearable
|
||||
/>`
|
||||
|
||||
const arbitraryUsage = `<CAutoComplete
|
||||
v-model="department"
|
||||
:options="['Sales', 'Finance', 'Operations']"
|
||||
placeholder="Choose or enter a department"
|
||||
/>
|
||||
|
||||
<p>Current value: {{ department }}</p>`
|
||||
|
||||
const remoteJavaScript = `const suggestions = ref([])
|
||||
const loading = ref(false)
|
||||
let controller
|
||||
|
||||
async function searchCustomers(query) {
|
||||
controller?.abort()
|
||||
|
||||
if (!query) {
|
||||
suggestions.value = []
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const request = new AbortController()
|
||||
controller = request
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
\`/api/customers?q=\${encodeURIComponent(query)}\`,
|
||||
{ signal: request.signal },
|
||||
)
|
||||
suggestions.value = await response.json()
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError') throw error
|
||||
} finally {
|
||||
if (controller === request) loading.value = false
|
||||
}
|
||||
}`
|
||||
|
||||
const remoteUsage = `<CAutoComplete
|
||||
v-model="customer"
|
||||
:options="suggestions"
|
||||
:loading="loading"
|
||||
:debounce-wait="350"
|
||||
:min-search-length="2"
|
||||
@search="searchCustomers"
|
||||
/>`
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article class="form-page">
|
||||
<header class="page-header">
|
||||
<div><p class="category">Forms</p><h1>Auto Complete</h1></div>
|
||||
<p>
|
||||
<code>CAutoComplete</code> is a text input with string suggestions. Users may select a
|
||||
suggestion or keep any arbitrary text they enter.
|
||||
</p>
|
||||
</header>
|
||||
<CSeparator />
|
||||
|
||||
<section class="section">
|
||||
<h2>String suggestions</h2>
|
||||
<p>
|
||||
Pass a simple array of strings through <code>options</code>. Matching is case-insensitive
|
||||
and checks the entire string. Because the options have no object mapping, the API does not
|
||||
need <code>option-label</code>, <code>option-value</code>, or <code>option-key</code>.
|
||||
</p>
|
||||
<div class="preview">
|
||||
<CFormField label="Destination">
|
||||
<CAutoComplete
|
||||
v-model="destination"
|
||||
:options="cities"
|
||||
placeholder="Enter a destination"
|
||||
clearable
|
||||
/>
|
||||
</CFormField>
|
||||
<span>Current value: {{ destination }}</span>
|
||||
</div>
|
||||
<CCodeBlock class="code-sample" :code="basicJavaScript" language="javascript" />
|
||||
<CCodeBlock class="code-sample" :code="basicUsage" />
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Arbitrary values</h2>
|
||||
<p>
|
||||
Typing updates <code>v-model</code> immediately, even when the text is absent from
|
||||
<code>options</code>. Suggestions assist entry but do not constrain it. The
|
||||
<code>select</code> event is emitted only when a listed suggestion is explicitly chosen.
|
||||
</p>
|
||||
<div class="preview">
|
||||
<CFormField label="Department" hint="You may enter a department not shown in the list.">
|
||||
<CAutoComplete
|
||||
v-model="department"
|
||||
:options="['Sales', 'Finance', 'Operations']"
|
||||
placeholder="Choose or enter a department"
|
||||
/>
|
||||
</CFormField>
|
||||
<span>Current value: {{ department }}</span>
|
||||
</div>
|
||||
<CCodeBlock class="code-sample" :code="arbitraryUsage" />
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Keyboard usage</h2>
|
||||
<p>
|
||||
Use Arrow Up or Arrow Down to move through suggestions, Enter to accept the highlighted
|
||||
suggestion, and Escape to close the list without changing the current text. Tab keeps the
|
||||
arbitrary value and moves focus normally.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Remote suggestions</h2>
|
||||
<p>
|
||||
The component does not make network requests itself. After the user pauses typing,
|
||||
<code>search</code> emits the trimmed query and the parent replaces <code>options</code>.
|
||||
<code>debounce-wait</code> controls that pause, while <code>min-search-length</code> prevents
|
||||
short queries from starting a search.
|
||||
</p>
|
||||
<p>
|
||||
When the query drops below the minimum, an empty search is emitted immediately so the
|
||||
parent can cancel work and clear stale results. Set <code>loading</code> while awaiting the
|
||||
response; users can continue typing while the spinner is visible.
|
||||
</p>
|
||||
<div class="preview">
|
||||
<CFormField label="Remote city search" hint="Type at least two characters.">
|
||||
<CAutoComplete
|
||||
v-model="remoteCity"
|
||||
:options="remoteOptions"
|
||||
:loading="remoteLoading"
|
||||
:debounce-wait="350"
|
||||
:min-search-length="2"
|
||||
placeholder="Search cities"
|
||||
clearable
|
||||
@search="searchCities"
|
||||
/>
|
||||
</CFormField>
|
||||
<span>Current value: {{ remoteCity }}</span>
|
||||
</div>
|
||||
<CCodeBlock class="code-sample" :code="remoteJavaScript" language="javascript" />
|
||||
<CCodeBlock class="code-sample" :code="remoteUsage" />
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Sizes and states</h2>
|
||||
<div class="preview">
|
||||
<CFormField label="Small (26px)">
|
||||
<CAutoComplete v-model="smallValue" :options="cities" size="small" />
|
||||
</CFormField>
|
||||
<CFormField label="Large (34px)">
|
||||
<CAutoComplete v-model="largeValue" :options="cities" size="large" />
|
||||
</CFormField>
|
||||
<CFormField label="Disabled">
|
||||
<CAutoComplete model-value="Jakarta" :options="cities" disabled />
|
||||
</CFormField>
|
||||
<CFormField label="Invalid">
|
||||
<CAutoComplete model-value="Unknown office" :options="cities" invalid />
|
||||
</CFormField>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Properties and events</h2>
|
||||
<dl class="property-list">
|
||||
<div><dt><code>model-value</code></dt><dd>Current string, including values absent from the suggestion list.</dd></div>
|
||||
<div><dt><code>options</code></dt><dd>Array of strings used as suggestions.</dd></div>
|
||||
<div><dt><code>debounce-wait</code></dt><dd>Milliseconds to wait before emitting a non-empty <code>search</code>. Defaults to <code>300</code>.</dd></div>
|
||||
<div><dt><code>loading</code></dt><dd>Shows remote loading feedback without disabling text entry.</dd></div>
|
||||
<div><dt><code>min-search-length</code></dt><dd>Minimum trimmed query length required before emitting a non-empty search.</dd></div>
|
||||
<div><dt><code>clearable</code></dt><dd>Adds a compact button that clears the value to an empty string.</dd></div>
|
||||
<div><dt><code>placeholder</code></dt><dd>Text displayed while the value is empty.</dd></div>
|
||||
<div><dt><code>size</code></dt><dd><code>small</code>, <code>medium</code>, or <code>large</code>.</dd></div>
|
||||
<div><dt><code>disabled</code></dt><dd>Disables input and focus.</dd></div>
|
||||
<div><dt><code>required</code></dt><dd>Applies native required validation.</dd></div>
|
||||
<div><dt><code>invalid</code></dt><dd>Applies invalid styling and <code>aria-invalid</code>.</dd></div>
|
||||
<div><dt><code>search</code></dt><dd>Emitted with the debounced query, or immediately with an empty string when below the minimum.</dd></div>
|
||||
<div><dt><code>select</code></dt><dd>Emitted with the string when a suggestion is explicitly selected.</dd></div>
|
||||
<div><dt><code>clear</code></dt><dd>Emitted after the clear button resets the value.</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss" src="./form-demo.scss"></style>
|
||||
Loading…
Reference in New Issue