Skip to content

vue-saselect

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.