Skip to content

SaSelect (FormSelect) ​

Advanced Vue 3 select component dengan async search, cursor pagination, dan keyboard navigation

Versi: 0.2.9
Kategori: UI Component (Vue 3)

npm versionTypeScriptVueTailwind CSS

MVP LIBRARY

Library ini di-extract dari family project. Styling tightly coupled dengan parent project dan mengharuskan class definitions tertentu. Lihat Styling untuk detail.


TL;DR ​

Select component untuk Vue 3 dengan async search capabilities, virtual scrolling untuk ribuan items, cursor-based pagination, dan full keyboard navigation. Component ini handle complex object options, primitive arrays, custom value mapping, dan integrasi dengan Laravel routes via RequestInstance atau regular axios.

Components:

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

Types:

ts
import type {
    RouteConfig,
    OptionValue,
    PrepareDataFn,
    StandardApiResponse
} from '@bpmlib/vue-saselect';

Installation & Setup ​

Requirements ​

PHP Minimum: 8.1+ (untuk backend Laravel routes, optional)

Browser: Modern browsers dengan ES2015+ support

Peer Dependencies ​

Library ini memerlukan peer dependencies berikut:

Wajib:

bash
npm install vue@^3.3.0 axios@^1.6.0
npm install vue-virtual-scroller@^2.0.0-beta.8
npm install @fortawesome/fontawesome-svg-core@^6.0.0
npm install @fortawesome/free-solid-svg-icons@^6.0.0
npm install @fortawesome/vue-fontawesome@^3.0.0
DependencyVersiStatusDeskripsi
vue^3.3.0RequiredVue 3 framework
axios^1.6.0RequiredHTTP client
vue-virtual-scroller^2.0.0-beta.8RequiredVirtual scrolling
@fortawesome/fontawesome-svg-core^6.0.0RequiredFontAwesome core
@fortawesome/free-solid-svg-icons^6.0.0RequiredFontAwesome icons
@fortawesome/vue-fontawesome^3.0.0RequiredFontAwesome Vue
@bpmlib/utils-sarequest^0.1.0 | ^1.0.0OptionalRequestInstance

Package Installation ​

bash
npm install @bpmlib/vue-saselect
bash
yarn add @bpmlib/vue-saselect
bash
pnpm add @bpmlib/vue-saselect
bash
bun install @bpmlib/vue-saselect

Import ​

Component:

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

Types:

ts
import type {
    RouteConfig,
    OptionValue,
    PrepareDataFn
} from '@bpmlib/vue-saselect';

Required CSS:

ts
// WAJIB: Import vue-virtual-scroller CSS
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';

NO COMPONENT CSS

Component ini TIDAK menyediakan CSS sendiri. Hanya perlu import vue-virtual-scroller.css. Styling sepenuhnya bergantung pada parent project classes Lihat selengkapnya.

Plugin Setup ​

Component bisa digunakan langsung atau di-register global:

Direct Usage:

vue
<script setup>
    import { FormSelect } from '@bpmlib/vue-saselect';
</script>

<template>
    <FormSelect id="select" v-model="value" :options="items" />
</template>

Global Registration:

ts
// main.ts
import { createApp } from 'vue';
import { FormSelect } from '@bpmlib/vue-saselect';
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';

const app = createApp(App);
app.component('FormSelect', FormSelect);

Quick Start ​

Synchronous Options ​

vue
<script setup lang="ts">
    import { ref } from 'vue';
    import { FormSelect } from '@bpmlib/vue-saselect';

    const selected = ref(null);

    const cities = [
        { id: 1, name: 'Jakarta' },
        { id: 2, name: 'Bandung' },
        { id: 3, name: 'Surabaya' }
    ];
</script>

<template>
    <FormSelect
        id="city"
        v-model="selected"
        label="Pilih Kota"
        :options="cities"
        option-value="id"
        option-label="name"
    />
</template>

vue
<script setup lang="ts">
    import { ref } from 'vue';
    import { FormSelect } from '@bpmlib/vue-saselect';
    import axios from 'axios';

    const selectedUser = ref(null);
    const axiosInstance = axios.create({
        baseURL: 'https://api.example.com'
    });
</script>

<template>
    <FormSelect
        id="user"
        v-model="selectedUser"
        label="Cari User"
        async-url="/api/users/search"
        async-param-key="q"
        option-value="id"
        option-label="name"
        :async-instance="axiosInstance"
    />
</template>

Multiselect ​

vue
<script setup lang="ts">
    import { ref } from 'vue';
    import { FormSelect } from '@bpmlib/vue-saselect';

    const selectedTags = ref<number[]>([]);

    const tags = [
        { id: 1, label: 'Vue' },
        { id: 2, label: 'React' },
        { id: 3, label: 'Angular' }
    ];
</script>

<template>
    <FormSelect
        id="tags"
        v-model="selectedTags"
        label="Pilih Tags"
        :options="tags"
        option-value="id"
        option-label="label"
        multiselect
    />

    <p>Selected: {{ selectedTags }}</p>
</template>

Core Concepts ​

Virtual Scrolling untuk Performance ​

Component menggunakan vue-virtual-scroller untuk render ribuan items efisien. Hanya items visible di viewport yang di-render ke DOM.

Object Identity Stability:

Virtual scroller track options by reference. Jangan recreate option objects:

ts
// ❌ WRONG - Recreate objects
const filtered = computed(() =>
    raw.value.map(x => ({ id: x.id, name: x.name }))
);

// ✅ CORRECT - Preserve references
const filtered = computed(() =>
    raw.value.filter(x => x.active)
);

Safe operations: filter(), slice(), appending
Unsafe operations: map() ke new objects, destructuring

Component warning di dev mode jika detect recreation Lihat selengkapnya.


Async Search dengan Debouncing ​

Search requests di-debounce 500ms untuk efisiensi:

Flow:

  1. User ketik → timer start
  2. User stop ketik 500ms → request trigger
  3. Loading indicator muncul
  4. Response → populate dropdown

Features:

  • Previous requests di-cancel otomatis (AbortController)
  • Error state dengan retry option
  • Loading indicator otomatis

Initial Data: Component auto-load initial option jika initialUrl + modelValue provided saat mount Lihat selengkapnya.


Cursor Pagination Modes ​

Auto-load (asyncCursor: true):

  • Scroll ke bottom → auto-load next page
  • maxAutoLoadItems limit total items (default: 150)
  • Setelah limit → manual button

Manual (asyncCursor: false):

  • "Muat Lebih Banyak" button
  • User control kapan load

API response harus include next_cursor dan has_more Lihat selengkapnya.


Primitive vs Complex Options ​

Component mendukung dua tipe options dengan behavior berbeda.

Primitive (string, number, boolean):

ts
const fruits = ['Apple', 'Banana', 'Cherry'];
  • Zero config, works out-of-box
  • Label/value = option itself
  • Duplicate values OK

Contoh:

vue
<FormSelect
  id="fruit"
  v-model="selected"
  :options="['Apple', 'Banana', 'Cherry']"
/>
<!-- selected = 'Apple' -->

Complex Objects:

ts
const users = [{ id: 1, name: 'John' }];
  • Perlu optionValue + optionLabel props Lihat selengkapnya
  • Object reference stability critical

Basic Example:

vue
<FormSelect
  id="user"
  v-model="selectedId"
  :options="users"
  option-value="id"
  option-label="name"
/>
<!-- selectedId = 1 -->

Label & Value Priority System (Complex Objects):

Component menggunakan sistem priority untuk override default property-based extraction:

Label Priority (Apa yang user lihat):

  1. Slot #customList - Full custom rendering, total control
  2. mapLabel function - Transform option → custom string
  3. optionLabel property - Baca option[optionLabel]
  4. Default - String(option) (usually [object Object], tidak berguna)

Value Priority (Apa yang di-emit ke v-model):

  1. rawEmit prop - Emit raw option object, bypass semua
  2. mapValue function - Transform option → custom value
  3. optionValue property - Baca option[optionValue]
  4. Default - Option as-is (full object)

Example - Advanced Mapping:

vue
<script setup>
const mapLabel = (user) => `${user.name} (${user.email})`;
const mapValue = (user) => ({ id: user.id, name: user.name });
</script>

<template>
  <FormSelect
    :options="users"
    :map-label="mapLabel"
    :map-value="mapValue"
  />
  <!-- Label: "John (john@example.com)" -->
  <!-- Value: { id: 1, name: "John" } -->
</template>

Takeaway:

  • Primitive options: Zero config
  • Object options: Minimal config (optionValue + optionLabel) usually cukup
  • Advanced cases: Gunakan mapLabel/mapValue untuk custom logic
  • Full control: Gunakan #customList slot atau rawEmit prop

Keyboard Navigation ​

Arrow Keys: ↑/↓ navigate options
Enter: Select highlighted
Escape: Close dropdown

Auto-scroll highlighted ke viewport. ARIA attributes untuk screen readers.


API Reference ​

📖 Components ​

FormSelect ​

Generic Vue component untuk select/dropdown dengan async capabilities.

Cara Penggunaan:

Component generik dengan type parameter T untuk option type. Default usage tidak perlu explicit type.

Accessibility:

  • Keyboard navigation: Arrow keys, Enter, Escape
  • ARIA attributes: role="combobox", aria-expanded, aria-activedescendant
  • Screen reader: Accessible labels dan descriptions

Props

Contains:

Props: Data Source ​
NameTypeDefaultDescription
optionsOptionValue<T>[][]Opsi synchronous mode
asyncUrlAsyncUrlConfig-Endpoint async search Lihat selengkapnya
asyncParamKeystring'q'Query param key untuk search
initialUrlAsyncUrlConfig-Endpoint load initial data Lihat selengkapnya
prepareDataPrepareDataFn-Transform API response Lihat selengkapnya
asyncInstanceany-Axios atau RequestInstance Lihat selengkapnya
asyncUrl ​

Endpoint configuration untuk async searching. Menerima string URL atau RouteConfig object untuk advanced control.

Type: string | RouteConfig

Contoh:

ts
// Simple string
async-url="/api/search"

// RouteConfig
:async-url="{
  method: 'post',
  routeName: 'api.products.search',
  routeParams: { category: 'electronics' }
}"

Use Case: Simple async search (string) atau complex request config (RouteConfig dengan headers, body, params).


initialUrl ​

Endpoint load initial data dari modelValue existing (edit forms). Component auto-request saat mount jika prop ini + modelValue provided.

Type: string | RouteConfig

Contoh:

vue
<FormSelect
  v-model="existingUserId"
  initial-url="/api/users/{id}"
  async-url="/api/users/search"
/>

Use Case: Edit forms dengan existing selection, perlu load display label dari ID tersimpan.


prepareData ​

Transform API response jadi options array. Berguna jika response structure non-standard.

Type: PrepareDataFn

Default: Extract response.content atau return as-is jika array

Contoh:

ts
const prepareData = (res) => res.results; // API returns { results: [...] }

Use Case: API response format custom, bukan standard { content: [...] }.


asyncInstance ​

Axios-compatible instance untuk requests. Bisa axios biasa atau RequestInstance untuk Laravel routes.

Type: AxiosInstance | RequestInstance

Fallback: window.axios jika tidak provided

Contoh:

ts
import axios from 'axios';
import { RequestInstance } from '@bpmlib/utils-sarequest';

// Regular axios
const axiosInstance = axios.create({
  baseURL: 'https://api.example.com'
});

// RequestInstance
const requestInstance = new RequestInstance({
  routes: routeDict,
  baseURL: 'https://api.example.com'
});

Use Case: Custom axios config atau Laravel named routes via RequestInstance.


Props: Selection & Display ​
NameTypeDefaultDescription
modelValueany-Bound value (single atau array)
multiselectbooleanfalseMultiple selection mode
optionKeystring'value'Deprecated - gunakan optionValue
optionValuestring-Property untuk value extraction
optionLabelstring'label'Property untuk display label
optionDescstring-Property untuk description text
mapLabel(option: T) => string-Custom label mapper Lihat selengkapnya
mapValue(option: T) => any-Custom value mapper Lihat selengkapnya
rawEmitbooleanfalseEmit raw object, bypass mappers
mapLabel ​

Custom function transform option ke display label. Priority lebih tinggi dari optionLabel prop.

Type: (option: OptionValue<T>) => string

Contoh:

ts
const mapLabel = (user) => `${user.firstName} ${user.lastName} (${user.role})`;

Use Case: Complex label composition, computed labels dari multiple properties.


mapValue ​

Custom function transform option ke emitted value. Priority lebih tinggi dari optionValue prop.

Type: (option: OptionValue<T>) => any

Contoh:

ts
const mapValue = (product) => ({
  id: product.id,
  price: product.currentPrice
});

Use Case: Extract nested property, custom value structure, combine multiple properties.


NameTypeDefaultDescription
asyncCursorbooleanfalseEnable cursor pagination auto-load
maxAutoLoadItemsnumber150Max items auto-load (cursor mode)
asyncOtherParamsRecord<string, any>{}Deprecated - gunakan urlParams di RouteConfig

Props: UI State ​
NameTypeDefaultDescription
idstringrequiredUnique ID untuk input & ARIA
labelstring-Label text
placeholderstring'Ketik untuk mencari'Placeholder text
disabledbooleanfalseDisable component
readonlybooleanfalseRead-only mode
requiredbooleanfalseRequired validation
notClearablebooleanfalseHide clear button
disableSearchbooleanfalseDisable typing/search
hasErrorbooleanfalseForce error state

Props: Validation ​
NameTypeDefaultDescription
hideValidationbooleanfalseHide validation messages
serverSideErrorstring | object-Server validation error

Events

NamePayloadDescription
update:modelValueanySelection changed
reachBottom-Scrolled to bottom

Slots

NameScopeDescription
customList{ data: T }Custom option rendering Lihat selengkapnya
description-Bottom helper text
customList ​

Full custom rendering untuk setiap option di dropdown. Total control atas tampilan.

Scope:

ts
{
  data: OptionValue<T> // Current option object
}

Contoh:

vue
<FormSelect :options="users">
  <template #customList="{ data }">
    <div class="flex items-center gap-3">
      <img :src="data.avatar" class="w-8 h-8 rounded-full" />
      <div>
        <p class="font-bold">{{ data.name }}</p>
        <p class="text-sm text-gray-500">{{ data.email }}</p>
      </div>
    </div>
  </template>
</FormSelect>

Use Case: Complex option layouts, avatars, badges, icons, custom formatting.


📖 Types ​

Contains:


OptionValue ​

ts
type OptionValue<T = unknown> = T;

Generic type untuk option items. Bisa primitive atau object.


RouteConfig ​

ts
interface RouteConfig {
  method?: HttpMethod;
  routeName: string;
  routeParams?: any[] | Record<string, any>;
  urlParams?: Record<string, any>;
  headers?: Record<string, any>;
  body?: any;
  asFormData?: boolean;
}

Configuration object untuk advanced async requests. Support Laravel-style route parameters, headers, body, FormData.

Contains:

method ​
ts
method?: HttpMethod

HTTP method ('get' | 'post' | 'put' | 'patch' | 'delete'). Default 'get'.


routeName ​
ts
routeName: string

RequestInstance: Laravel route name (e.g., 'api.users.search')
Axios: URL path dengan {param} placeholders (e.g., '/api/users/{id}/posts')


routeParams ​
ts
routeParams?: any[] | Record<string, any>

Route parameters untuk {param} replacement di URL.

Object format: { id: 123 } → /api/users/123
Array format: [123] → /api/users/123 (order-based)


urlParams ​
ts
urlParams?: Record<string, any>

Query string parameters. Object jadi ?key=value&key2=value2.


headers ​
ts
headers?: Record<string, any>

Custom request headers.


body ​
ts
body?: any

Request body untuk POST/PUT/PATCH.


asFormData ​
ts
asFormData?: boolean

Convert body object ke FormData. Berguna untuk file uploads.


AsyncUrlConfig ​

ts
type AsyncUrlConfig = string | RouteConfig;

Union type: simple string URL atau advanced RouteConfig.


PrepareDataFn ​

ts
type PrepareDataFn = (data: any) => any[];

Function signature transform API response ke options array.


AsyncConfig ​

ts
interface AsyncConfig {
  url: AsyncUrlConfig;
  paramKey: string;
  otherParams: Record<string, any>; // deprecated
  cursor?: boolean;
  maxAutoLoadItems?: number;
}

Internal async config (derived dari props). Rarely used directly.


SelectionConfig ​

ts
interface SelectionConfig<T = unknown> {
  multiselect: boolean;
  rawEmit: boolean;
  optionKey: string;
  optionLabel: string;
  optionDesc?: string;
  mapLabel?: (option: OptionValue<T>) => string;
  mapValue?: (option: OptionValue<T>) => any;
  optionValue?: string;
  customLabel?: (option: OptionValue<T>, type: 'label' | 'value' | 'desc') => string; // deprecated
}

Internal selection config (derived dari props). Rarely used directly.


UIConfig ​

ts
interface UIConfig {
  label?: string;
  id: string;
  placeholder?: string;
  disabled: boolean;
  readonly: boolean;
  required: boolean;
  hasError: boolean;
  notClearable: boolean;
  disableSearch: boolean;
}

Internal UI config (derived dari props). Rarely used directly.


ValidationConfig ​

ts
interface ValidationConfig {
  hideValidation: boolean;
  serverSideError?: string | object;
}

Internal validation config (derived dari props). Rarely used directly.


CursorData ​

ts
interface CursorData {
  next: string | null;
  prev: string | null;
  hasMore: boolean;
}

Cursor pagination state.

Contains:

next ​
ts
next: string | null

Next page cursor token.


prev ​
ts
prev: string | null

Previous page cursor token.


hasMore ​
ts
hasMore: boolean

Boolean flag, true jika ada more pages.


StandardApiResponse ​

ts
interface StandardApiResponse<TData = any, TAdditional = any> {
  success?: boolean;
  message?: string;
  code?: string | number;
  content?: TData[] | TData | unknown;
  appended?: TAdditional;
  max_page?: number | null;
  current_page?: number | null;
  total?: number | null;
  next_cursor?: string | null;
  previous_cursor?: string | null;
  per_page?: number | null;
  has_more?: boolean | null;
}

Standard API response format untuk RequestInstance dari @bpmlib/utils-sarequest.

Key Fields:

  • content - Data array atau single item
  • next_cursor - Cursor untuk next page
  • has_more - Boolean pagination flag

RequestInstance ​

ts
interface RequestInstance {
  setUrlParams(params: Record<string, any>): this;
  setHeaders(headers: Record<string, any>): this;
  setBody(body: any, asFormData?: boolean): this;
  get(route: string, params?: any[], config?: any): Promise<any>;
  post(route: string, params?: any[], config?: any): Promise<any>;
  put(route: string, params?: any[], config?: any): Promise<any>;
  patch(route: string, params?: any[], config?: any): Promise<any>;
  delete(route: string, params?: any[], config?: any): Promise<any>;
}

Interface untuk RequestInstance class dari @bpmlib/utils-sarequest. Component detect jika asyncInstance implement interface ini untuk Laravel route handling.

Note: RequestInstance adalah class, bukan composable. Instantiate dengan new RequestInstance(config).


HttpMethod & ResponseType ​

ts
type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';
type ResponseType = 'json' | 'blob' | 'arraybuffer' | 'document' | 'text' | 'stream';

HTTP method dan response type enums.


Examples ​

Contains:


1. RouteConfig dengan RequestInstance ​

Laravel named routes dengan RequestInstance:

vue
<script setup lang="ts">
import { ref } from 'vue';
import { FormSelect } from '@bpmlib/vue-saselect';
import type { RouteConfig } from '@bpmlib/vue-saselect';
import { RequestInstance } from '@bpmlib/utils-sarequest';

const selectedProduct = ref(null);

const requestInstance = new RequestInstance({
  routes: routeDict,
  baseURL: 'https://api.example.com'
});

const searchConfig: RouteConfig = {
  method: 'post',
  routeName: 'api.products.search',
  routeParams: { category: 'electronics' },
  headers: { 'X-Custom': 'value' },
  body: { filters: { inStock: true } }
};
</script>

<template>
  <FormSelect
    id="product"
    v-model="selectedProduct"
    label="Cari Produk"
    :async-url="searchConfig"
    async-param-key="keyword"
    option-value="id"
    option-label="name"
    :async-instance="requestInstance"
  />
</template>

2. RouteConfig dengan Axios ​

RouteConfig dengan axios standar (tanpa RequestInstance):

vue
<script setup lang="ts">
import { ref } from 'vue';
import { FormSelect } from '@bpmlib/vue-saselect';
import type { RouteConfig } from '@bpmlib/vue-saselect';
import axios from 'axios';

const selectedCity = ref(null);
const axiosInstance = axios.create({
  baseURL: 'https://api.example.com'
});

const searchConfig: RouteConfig = {
  method: 'get',
  routeName: '/api/cities/{country}/search',
  routeParams: { country: 'ID' },
  urlParams: { limit: 20, sortBy: 'name' },
  headers: { 'Authorization': 'Bearer token' }
};
</script>

<template>
  <FormSelect
    id="city"
    v-model="selectedCity"
    :async-url="searchConfig"
    option-value="id"
    option-label="name"
    :async-instance="axiosInstance"
  />
</template>

Key Points:

  • routeName adalah URL path dengan {param} placeholders
  • routeParams replace placeholders → /api/cities/ID/search
  • urlParams jadi query string → ?limit=20&sortBy=name

3. Custom Label & Value Mapping ​

Transform option display dan emitted value:

vue
<script setup lang="ts">
import { ref } from 'vue';
import { FormSelect } from '@bpmlib/vue-saselect';

interface Employee {
  employeeId: string;
  firstName: string;
  lastName: string;
  department: string;
  email: string;
}

const selectedEmployee = ref<{ id: string; name: string } | null>(null);

const employees: Employee[] = [
  {
    employeeId: 'E001',
    firstName: 'John',
    lastName: 'Doe',
    department: 'Engineering',
    email: 'john@company.com'
  }
];

const mapLabel = (emp: Employee) => 
  `${emp.firstName} ${emp.lastName} (${emp.department})`;

const mapValue = (emp: Employee) => ({
  id: emp.employeeId,
  name: `${emp.firstName} ${emp.lastName}`
});
</script>

<template>
  <FormSelect
    id="employee"
    v-model="selectedEmployee"
    label="Pilih Karyawan"
    :options="employees"
    :map-label="mapLabel"
    :map-value="mapValue"
    option-desc="email"
  />
  
  <p v-if="selectedEmployee">
    Selected: {{ selectedEmployee.name }} (ID: {{ selectedEmployee.id }})
  </p>
</template>

4. Initial Data Loading ​

Load initial selected option dari ID existing (edit forms):

vue
<script setup lang="ts">
import { ref } from 'vue';
import { FormSelect } from '@bpmlib/vue-saselect';
import axios from 'axios';

// Dari database: existing product ID 123
const selectedProductId = ref(123);

const axiosInstance = axios.create({
  baseURL: 'https://api.example.com'
});
</script>

<template>
  <FormSelect
    id="product"
    v-model="selectedProductId"
    label="Produk"
    async-url="/api/products/search"
    initial-url="/api/products/{id}"
    option-value="id"
    option-label="name"
    :async-instance="axiosInstance"
  />
</template>

How It Works:

  1. Component detect modelValue = 123
  2. Request ke /api/products/123
  3. Response added ke options
  4. Display label muncul (bukan cuma ID)

5. Custom List Slot ​

Full custom rendering dengan avatars, badges, status:

vue
<script setup lang="ts">
import { ref } from 'vue';
import { FormSelect } from '@bpmlib/vue-saselect';

interface User {
  id: number;
  username: string;
  fullName: string;
  avatar: string;
  status: 'online' | 'offline';
  role: string;
}

const selectedUser = ref(null);

const users: User[] = [
  {
    id: 1,
    username: 'johndoe',
    fullName: 'John Doe',
    avatar: 'https://i.pravatar.cc/150?img=1',
    status: 'online',
    role: 'Admin'
  }
];
</script>

<template>
  <FormSelect
    id="user"
    v-model="selectedUser"
    label="Pilih User"
    :options="users"
    option-value="id"
  >
    <template #customList="{ data }">
      <div class="flex items-center gap-3 py-1.5 px-3">
        <div class="relative">
          <img :src="data.avatar" :alt="data.fullName" class="w-10 h-10 rounded-full" />
          <span
            class="absolute bottom-0 right-0 w-3 h-3 rounded-full border-2 border-white"
            :class="{
              'bg-green-500': data.status === 'online',
              'bg-gray-400': data.status === 'offline'
            }"
          />
        </div>
        
        <div class="flex-1">
          <p class="font-bold text-sm">{{ data.fullName }}</p>
          <p class="text-xs text-gray-500">@{{ data.username }}</p>
        </div>
        
        <span class="px-2 py-1 text-xs rounded bg-blue-100 text-blue-700">
          {{ data.role }}
        </span>
      </div>
    </template>
  </FormSelect>
</template>

6. Validation & Server Errors ​

Required validation dan server-side errors:

vue
<script setup lang="ts">
import { ref } from 'vue';
import { FormSelect } from '@bpmlib/vue-saselect';
import axios from 'axios';

const selectedCategory = ref(null);
const serverErrors = ref<Record<string, string>>({});

const categories = [
  { id: 1, name: 'Technology' },
  { id: 2, name: 'Business' }
];

async function submitForm() {
  try {
    await axios.post('/api/articles', {
      category_id: selectedCategory.value
    });
    serverErrors.value = {};
  } catch (error: any) {
    if (error.response?.data?.errors) {
      serverErrors.value = error.response.data.errors;
    }
  }
}
</script>

<template>
  <form @submit.prevent="submitForm">
    <FormSelect
      id="category"
      v-model="selectedCategory"
      label="Kategori Artikel"
      placeholder="Pilih kategori"
      :options="categories"
      option-value="id"
      option-label="name"
      required
      :server-side-error="serverErrors.category_id"
    >
      <template #description>
        Pilih kategori yang sesuai dengan artikel
      </template>
    </FormSelect>
    
    <button type="submit" class="btn btn-primary mt-4">
      Submit
    </button>
  </form>
</template>

Styling ​

Component Styling Reality ​

Component ini TIDAK menyediakan CSS apapun. Semua styling bergantung pada:

  1. Parent project class definitions - Form, input, dropdown classes
  2. Tailwind CSS utility classes - Spacing, colors, typography
  3. vue-virtual-scroller CSS - Virtual scrolling (external)

NO STYLING

Tanpa parent project class definitions, component render completely unstyled. Designed untuk family project ecosystem.


Expected Classes dari Parent Project ​

Component menggunakan class names berikut yang HARUS ada:

Form Structure ​

css
.form-parent          /* Outer container */
.form-group           /* Field wrapper */
.form-disabled        /* Disabled wrapper */
.select-component     /* Select-specific */

State modifiers: .err, .has-value, .opened


Input Elements ​

css
.input                /* Input field */
.control-label        /* Floating label */
.required-star        /* Required asterisk */
.right-form           /* Buttons container */
.input-attr-btn       /* Icon buttons */

Input states: .error, .opacity-70, .has-value


css
.options-absolute     /* Dropdown container */
.options-container    /* Scrollable list */
.option-header        /* Multiselect header */
.option               /* Option item */
.option-loading       /* Loading state */

Option states: .highlight, .selected, .select-indicator


Bottom Section ​

css
.bottom-form-items    /* Bottom info/errors */

Tailwind Utilities ​

Component extensively uses Tailwind:

Layout: flex, items-center, justify-between, gap-*, w-*, h-*
Spacing: py-*, px-*, mt-*, mb-*, mr-*, ml-*
Typography: text-*, font-*, italic
Visual: rounded, opacity-*, cursor-*
Interactive: hover:*

TAILWIND REQUIRED

Tailwind CSS adalah WAJIB, bukan optional.


Required External CSS ​

ts
// WAJIB di main app entry
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';

Provides virtual scroller container, scroll behavior, dynamic positioning.