FormSelect
Single/multi-select dengan floating label, sync & async search (cursor pagination), inline & detached panel.
Bagian dari: vue-saform · Family: Notch
FormSelect adalah dropdown select yang bisa jalan sepenuhnya sync (array options statis) atau async (search ke server dengan cursor pagination untuk infinite scroll), single maupun multi-select. Panelnya bisa inline (absolute-positioned, anchored ke field) atau detached jadi modal Teleport+backdrop — kombinasi multiselect + detached otomatis dapat staged/draft-commit workflow (Cancel/Reset/Submit) alih-alih live-commit tiap klik. fieldMap memetakan value/label/desc opsi secara fleksibel (property-name atau function), jadi bisa langsung pakai array object dari API tanpa transformasi manual. Ada juga disableSearch untuk jadikan field display-only trigger, dan scoped slot customList untuk override tampilan tiap baris opsi.
Import
import { FormSelect } from '@bpmlib/vue-saform';Instalasi: lihat Installation & Setup di halaman utama.
Usage
<script setup lang="ts">
import { ref } from 'vue';
import { FormSelect, required } from '@bpmlib/vue-saform';
const country = ref(null);
const options = [
{ value: 'id', label: 'Indonesia' },
{ value: 'my', label: 'Malaysia' },
{ value: 'sg', label: 'Singapura' },
];
</script>
<template>
<FormSelect id="country" v-model="country" label="Negara" :options="options" :validation="required()" />
</template>API Reference
NOTE
Dropdown/keyboard/floating-position state dibangun di atas beberapa composable internal (useDropdownOverlay, useKeyboardNav, useFloatingPosition, useSelection, useStagedSelection, useOptionFormatting), tidak diekspos secara publik. Lihat Core Concepts di halaman utama untuk arsitektur detach mode & staged commit.
FormSelect
Cara Penggunaan:
options menerima array bertipe apapun (T[], genuinely unconstrained) — pemetaan value/label/desc-nya diatur lewat fieldMap. Untuk async search, set asyncUrl (mengganti options).
Props
| Name | Type | Default | Description |
|---|---|---|---|
id | string | - | Id input, juga dipakai sebagai name. Wajib. |
label | string | - | Teks floating label |
placeholder | string | 'Ketik untuk mencari' ('Pilih opsi' saat disableSearch) | |
disabled | boolean | false | |
readonly | boolean | false | |
required | boolean | false | Built-in rule required() |
hasError | boolean | false | Paksa visual error state terlepas dari internal validation state |
multiselect | boolean | false | Aktifkan multi-select. Inline live-commit — tiap klik langsung update v-model, kecuali dikombinasikan dengan detach Lihat selengkapnya |
options | OptionValue<T>[] | [] | Daftar opsi statis/sync. Diabaikan jika asyncUrl diset |
fieldMap | OptionFieldMap<T> | - | Memetakan value/label/desc suatu opsi |
rawEmit | boolean | false | Emit raw option object, bukan mapped value |
notClearable | boolean | false | Cegah clear value lewat UI |
disableSearch | boolean | false | Nonaktifkan ketik/search — input jadi display-only trigger |
detach | boolean | 'small' | false | Detach panel ke Teleport+backdrop modal Lihat selengkapnya |
asyncUrl | string | RouteConfig | - | Endpoint async search, mengaktifkan async mode (mengganti options) Lihat selengkapnya |
initialUrl | string | RouteConfig | - | Endpoint untuk resolve opsi yang cocok dengan modelValue yang datang sebelum opsinya ter-load |
prepareData | (data: any) => any[] | Baca .content/.data/bare array | Transform payload response async jadi array opsi Lihat selengkapnya |
asyncParamKey | string | 'q' | Query parameter key untuk async search |
asyncInstance | any | window.axios (fallback) | Instance axios-compatible (atau RequestInstance) untuk async request |
asyncCursor | boolean | false | Aktifkan cursor-based pagination (infinite scroll) |
maxAutoLoadItems | number | 150 | Batas item yang auto-load lewat scroll di cursor mode sebelum butuh klik manual "load more" |
validation | ValidationInput<unknown> | - | Custom validation rule(s), jalan bersama built-in required |
messages | ValidationMessages | - | Override pesan validasi default, di-shallow-merge |
hideValidation | boolean | false | Skip seluruh perhitungan pesan validasi internal |
NOTE
Enam prop optionKey/optionValue/optionLabel/optionDesc/mapValue/mapLabel masih berfungsi (deprecated, memicu console.warn) — gunakan fieldMap sebagai gantinya, lihat OptionFieldMap.
multiselect
Saat multiselect dikombinasikan dengan detach (baik true maupun 'small' di bawah breakpoint), FormSelect otomatis beralih ke staged/draft-commit workflow — pilihan tidak langsung ter-commit ke v-model, melainkan menunggu tombol Submit di footer panel (dengan Cancel/Reset). Kombinasi lain (single-select, atau multiselect tanpa detach) tetap live-commit seperti biasa.
OptionFieldMap
interface OptionFieldMap<T> {
value?: string | ((option: T) => any);
label?: string | ((option: T) => string);
desc?: string | ((option: T) => string);
}Memetakan sebuah opsi ke value/label/desc-nya — tiap field berupa property-name string (rename field) atau mapper function (computed value), independen per field.
Contains:
value
value?: string | ((option: T) => any)
// Default: 'value'Value yang dipakai untuk identity, comparison, dan emission.
label
label?: string | ((option: T) => string)
// Default: 'label'Teks display label.
desc
desc?: string | ((option: T) => string)Teks deskripsi sekunder. Tidak ada default — omit untuk tanpa deskripsi.
Contoh:
const fieldMap = {
value: 'id',
label: (opt) => `${opt.firstName} ${opt.lastName}`,
};prepareData
Transform response payload API jadi array opsi.
Signature:
prepareData: (data: any) => any[]Parameters:
data- Raw response body dariasyncUrl
Contoh:
:prepare-data="(res) => res.data.items"Use Case: Default behavior sudah membaca .content/.data/bare array — override hanya kalau shape response API berbeda dari ketiga pola itu.
Async Search
Set asyncUrl untuk mengganti options statis dengan pencarian server-side — tiap ketikan (debounced) memicu request baru ke endpoint tersebut, memakai asyncParamKey sebagai nama query parameter. Kombinasikan dengan asyncCursor untuk infinite-scroll pagination di dalam panel; maxAutoLoadItems membatasi berapa banyak yang auto-load sebelum event reachBottom di-emit dan user harus klik "load more" manual.
Model
| Name | Type | Default | Description |
|---|---|---|---|
v-model | any | - | Value ter-mapped (kecuali rawEmit). Array saat multiselect |
Events
| Name | Payload | Description |
|---|---|---|
change | any | Sama seperti update:modelValue, disediakan sebagai event terpisah untuk kemudahan |
reachBottom | - | Fired saat auto-load cursor pagination mencapai batas maxAutoLoadItems |
Slots
| Name | Props | Description |
|---|---|---|
customList | { data: T } | Override tampilan tiap baris opsi. data adalah object opsi mentah (belum di-map) |
description | - | Baris deskripsi di bawah field |
errors | - | Baris pesan error custom, menggantikan pesan validasi bawaan |
Exposed
| Name | Type | Description |
|---|---|---|
validate() | () => Promise<boolean> | Jalankan validasi, return true jika valid |
Contoh:
<FormSelect
id="user"
v-model="selectedUser"
label="User"
:async-url="{ routeName: '/api/users' }"
:async-cursor="true"
:field-map="{ value: 'id', label: (u) => u.name }"
>
<template #customList="{ data }">
<div class="flex items-center gap-2">
<img :src="data.avatar" class="w-6 h-6 rounded-full" />
{{ data.name }}
</div>
</template>
</FormSelect>Examples
1. Multiselect dengan Detach
<FormSelect
id="tags"
v-model="tags"
label="Tags"
multiselect
:options="tagOptions"
detach
/>
<!-- Membuka modal Teleport+backdrop dengan footer Cancel/Reset/Submit -->2. Async Search dengan Cursor Pagination
<FormSelect
id="city"
v-model="city"
label="Kota"
:async-url="'/api/cities'"
:async-cursor="true"
:max-auto-load-items="100"
@reach-bottom="showLoadMoreHint = true"
/>3. Detach Responsif ('small')
<FormSelect id="category" v-model="category" label="Kategori" :options="categories" detach="small" />
<!-- Inline di layar >= 768px, jadi modal centered di bawahnya -->4. disableSearch — Display-Only Trigger
<FormSelect id="status" v-model="status" label="Status" :options="statusOptions" disable-search />
<!-- Input jadi cursor-default + dimmed, tapi tetap bisa dibuka & dipilih via klik/keyboard -->Styling
Styling sepenuhnya self-contained — lihat Import CSS di halaman utama. Component akan tampil fully styled langsung setelah import, tanpa setup tambahan.
CSS Variables
| Variable | Default | Description |
|---|---|---|
--sa-fm-panel-z | 40 (inline) / 50 (detached) | z-index panel dropdown/modal Lihat selengkapnya |
--sa-fm-error-list-style | disc | list-style-type untuk error list. Sama seperti FormInput |
--sa-fm-panel-z
Panel dropdown/modal Teleport ke document.body, menjadi sibling dari Teleported content lain milik host app (mis. modal sistem mereka sendiri) — tidak ada cara menang lewat CSS specificity terhadap sibling di luar kontrol component ini.
Default: 40 (panel inline), 50 (detached modal)
Usage:
:root {
--sa-fm-panel-z: 9999;
}Catatan: Set lebih tinggi dari z-index modal milik host app jika FormSelect dibuka dari dalam modal tersebut.
Related Components
Bagian dari family Notch:
- FormInput — text/number/email/password/currency input
- FormTextarea — versi
<textarea> - FormDatetime — date/time picker, berbagi konsep
detachyang sama