Skip to content

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

ts
import { FormSelect } from '@bpmlib/vue-saform';

Instalasi: lihat Installation & Setup di halaman utama.


Usage

vue
<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

NameTypeDefaultDescription
idstring-Id input, juga dipakai sebagai name. Wajib.
labelstring-Teks floating label
placeholderstring'Ketik untuk mencari' ('Pilih opsi' saat disableSearch)
disabledbooleanfalse
readonlybooleanfalse
requiredbooleanfalseBuilt-in rule required()
hasErrorbooleanfalsePaksa visual error state terlepas dari internal validation state
multiselectbooleanfalseAktifkan multi-select. Inline live-commit — tiap klik langsung update v-model, kecuali dikombinasikan dengan detach Lihat selengkapnya
optionsOptionValue<T>[][]Daftar opsi statis/sync. Diabaikan jika asyncUrl diset
fieldMapOptionFieldMap<T>-Memetakan value/label/desc suatu opsi
rawEmitbooleanfalseEmit raw option object, bukan mapped value
notClearablebooleanfalseCegah clear value lewat UI
disableSearchbooleanfalseNonaktifkan ketik/search — input jadi display-only trigger
detachboolean | 'small'falseDetach panel ke Teleport+backdrop modal Lihat selengkapnya
asyncUrlstring | RouteConfig-Endpoint async search, mengaktifkan async mode (mengganti options) Lihat selengkapnya
initialUrlstring | RouteConfig-Endpoint untuk resolve opsi yang cocok dengan modelValue yang datang sebelum opsinya ter-load
prepareData(data: any) => any[]Baca .content/.data/bare arrayTransform payload response async jadi array opsi Lihat selengkapnya
asyncParamKeystring'q'Query parameter key untuk async search
asyncInstanceanywindow.axios (fallback)Instance axios-compatible (atau RequestInstance) untuk async request
asyncCursorbooleanfalseAktifkan cursor-based pagination (infinite scroll)
maxAutoLoadItemsnumber150Batas item yang auto-load lewat scroll di cursor mode sebelum butuh klik manual "load more"
validationValidationInput<unknown>-Custom validation rule(s), jalan bersama built-in required
messagesValidationMessages-Override pesan validasi default, di-shallow-merge
hideValidationbooleanfalseSkip 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
ts
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
ts
value?: string | ((option: T) => any)
// Default: 'value'

Value yang dipakai untuk identity, comparison, dan emission.

label
ts
label?: string | ((option: T) => string)
// Default: 'label'

Teks display label.

desc
ts
desc?: string | ((option: T) => string)

Teks deskripsi sekunder. Tidak ada default — omit untuk tanpa deskripsi.

Contoh:

ts
const fieldMap = {
  value: 'id',
  label: (opt) => `${opt.firstName} ${opt.lastName}`,
};
prepareData

Transform response payload API jadi array opsi.

Signature:

ts
prepareData: (data: any) => any[]

Parameters:

  • data - Raw response body dari asyncUrl

Contoh:

ts
: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.

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

NameTypeDefaultDescription
v-modelany-Value ter-mapped (kecuali rawEmit). Array saat multiselect

Events

NamePayloadDescription
changeanySama seperti update:modelValue, disediakan sebagai event terpisah untuk kemudahan
reachBottom-Fired saat auto-load cursor pagination mencapai batas maxAutoLoadItems

Slots

NamePropsDescription
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

NameTypeDescription
validate()() => Promise<boolean>Jalankan validasi, return true jika valid

Contoh:

vue
<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

vue
<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

vue
<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')

vue
<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

vue
<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

VariableDefaultDescription
--sa-fm-panel-z40 (inline) / 50 (detached)z-index panel dropdown/modal Lihat selengkapnya
--sa-fm-error-list-styledisclist-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:

css
:root {
  --sa-fm-panel-z: 9999;
}

Catatan: Set lebih tinggi dari z-index modal milik host app jika FormSelect dibuka dari dalam modal tersebut.


Bagian dari family Notch: