vue-saselect
Advanced Vue 3 select component dengan async search, cursor pagination, dan keyboard navigation
Versi: 0.2.9
Kategori: UI Component (Vue 3)
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:
import { FormSelect } from '@bpmlib/vue-saselect';Types:
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:
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| Dependency | Versi | Status | Deskripsi |
|---|---|---|---|
vue | ^3.3.0 | Required | Vue 3 framework |
axios | ^1.6.0 | Required | HTTP client |
vue-virtual-scroller | ^2.0.0-beta.8 | Required | Virtual scrolling |
@fortawesome/fontawesome-svg-core | ^6.0.0 | Required | FontAwesome core |
@fortawesome/free-solid-svg-icons | ^6.0.0 | Required | FontAwesome icons |
@fortawesome/vue-fontawesome | ^3.0.0 | Required | FontAwesome Vue |
@bpmlib/utils-sarequest | ^0.1.0 | ^1.0.0 | Optional | RequestInstance |
Package Installation
npm install @bpmlib/vue-saselectyarn add @bpmlib/vue-saselectpnpm add @bpmlib/vue-saselectbun install @bpmlib/vue-saselectImport
Component:
import { FormSelect } from '@bpmlib/vue-saselect';Types:
import type {
RouteConfig,
OptionValue,
PrepareDataFn
} from '@bpmlib/vue-saselect';Required CSS:
// 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:
<script setup>
import { FormSelect } from '@bpmlib/vue-saselect';
</script>
<template>
<FormSelect id="select" v-model="value" :options="items" />
</template>Global Registration:
// 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
<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>Async Search
<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
<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:
// ❌ 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:
- User ketik → timer start
- User stop ketik 500ms → request trigger
- Loading indicator muncul
- 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
maxAutoLoadItemslimit 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):
const fruits = ['Apple', 'Banana', 'Cherry'];- Zero config, works out-of-box
- Label/value = option itself
- Duplicate values OK
Contoh:
<FormSelect
id="fruit"
v-model="selected"
:options="['Apple', 'Banana', 'Cherry']"
/>
<!-- selected = 'Apple' -->Complex Objects:
const users = [{ id: 1, name: 'John' }];- Perlu
optionValue+optionLabelprops Lihat selengkapnya - Object reference stability critical
Basic Example:
<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):
- Slot
#customList- Full custom rendering, total control mapLabelfunction - Transform option → custom stringoptionLabelproperty - Bacaoption[optionLabel]- Default -
String(option)(usually[object Object], tidak berguna)
Value Priority (Apa yang di-emit ke v-model):
rawEmitprop - Emit raw option object, bypass semuamapValuefunction - Transform option → custom valueoptionValueproperty - Bacaoption[optionValue]- Default - Option as-is (full object)
Example - Advanced Mapping:
<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/mapValueuntuk custom logic - Full control: Gunakan
#customListslot ataurawEmitprop
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
| Name | Type | Default | Description |
|---|---|---|---|
options | OptionValue<T>[] | [] | Opsi synchronous mode |
asyncUrl | AsyncUrlConfig | - | Endpoint async search Lihat selengkapnya |
asyncParamKey | string | 'q' | Query param key untuk search |
initialUrl | AsyncUrlConfig | - | Endpoint load initial data Lihat selengkapnya |
prepareData | PrepareDataFn | - | Transform API response Lihat selengkapnya |
asyncInstance | any | - | Axios atau RequestInstance Lihat selengkapnya |
asyncUrl
Endpoint configuration untuk async searching. Menerima string URL atau RouteConfig object untuk advanced control.
Type: string | RouteConfig
Contoh:
// 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:
<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:
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:
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
| Name | Type | Default | Description |
|---|---|---|---|
modelValue | any | - | Bound value (single atau array) |
multiselect | boolean | false | Multiple selection mode |
optionKey | string | 'value' | Deprecated - gunakan optionValue |
optionValue | string | - | Property untuk value extraction |
optionLabel | string | 'label' | Property untuk display label |
optionDesc | string | - | Property untuk description text |
mapLabel | (option: T) => string | - | Custom label mapper Lihat selengkapnya |
mapValue | (option: T) => any | - | Custom value mapper Lihat selengkapnya |
rawEmit | boolean | false | Emit raw object, bypass mappers |
mapLabel
Custom function transform option ke display label. Priority lebih tinggi dari optionLabel prop.
Type: (option: OptionValue<T>) => string
Contoh:
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:
const mapValue = (product) => ({
id: product.id,
price: product.currentPrice
});Use Case: Extract nested property, custom value structure, combine multiple properties.
Props: Async Search
| Name | Type | Default | Description |
|---|---|---|---|
asyncCursor | boolean | false | Enable cursor pagination auto-load |
maxAutoLoadItems | number | 150 | Max items auto-load (cursor mode) |
asyncOtherParams | Record<string, any> | {} | Deprecated - gunakan urlParams di RouteConfig |
Props: UI State
| Name | Type | Default | Description |
|---|---|---|---|
id | string | required | Unique ID untuk input & ARIA |
label | string | - | Label text |
placeholder | string | 'Ketik untuk mencari' | Placeholder text |
disabled | boolean | false | Disable component |
readonly | boolean | false | Read-only mode |
required | boolean | false | Required validation |
notClearable | boolean | false | Hide clear button |
disableSearch | boolean | false | Disable typing/search |
hasError | boolean | false | Force error state |
Props: Validation
| Name | Type | Default | Description |
|---|---|---|---|
hideValidation | boolean | false | Hide validation messages |
serverSideError | string | object | - | Server validation error |
Events
| Name | Payload | Description |
|---|---|---|
update:modelValue | any | Selection changed |
reachBottom | - | Scrolled to bottom |
Slots
| Name | Scope | Description |
|---|---|---|
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:
{
data: OptionValue<T> // Current option object
}Contoh:
<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
- RouteConfig
- AsyncUrlConfig
- PrepareDataFn
- AsyncConfig
- SelectionConfig
- UIConfig
- ValidationConfig
- CursorData
- StandardApiResponse
- RequestInstance
- HttpMethod & ResponseType
OptionValue
type OptionValue<T = unknown> = T;Generic type untuk option items. Bisa primitive atau object.
RouteConfig
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
method?: HttpMethodHTTP method ('get' | 'post' | 'put' | 'patch' | 'delete'). Default 'get'.
routeName
routeName: stringRequestInstance: Laravel route name (e.g., 'api.users.search')
Axios: URL path dengan {param} placeholders (e.g., '/api/users/{id}/posts')
routeParams
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
urlParams?: Record<string, any>Query string parameters. Object jadi ?key=value&key2=value2.
headers
headers?: Record<string, any>Custom request headers.
body
body?: anyRequest body untuk POST/PUT/PATCH.
asFormData
asFormData?: booleanConvert body object ke FormData. Berguna untuk file uploads.
AsyncUrlConfig
type AsyncUrlConfig = string | RouteConfig;Union type: simple string URL atau advanced RouteConfig.
PrepareDataFn
type PrepareDataFn = (data: any) => any[];Function signature transform API response ke options array.
AsyncConfig
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
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
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
interface ValidationConfig {
hideValidation: boolean;
serverSideError?: string | object;
}Internal validation config (derived dari props). Rarely used directly.
CursorData
interface CursorData {
next: string | null;
prev: string | null;
hasMore: boolean;
}Cursor pagination state.
Contains:
next
next: string | nullNext page cursor token.
prev
prev: string | nullPrevious page cursor token.
hasMore
hasMore: booleanBoolean flag, true jika ada more pages.
StandardApiResponse
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 itemnext_cursor- Cursor untuk next pagehas_more- Boolean pagination flag
RequestInstance
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
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
- 2. RouteConfig dengan Axios
- 3. Custom Label & Value Mapping
- 4. Initial Data Loading
- 5. Custom List Slot
- 6. Validation & Server Errors
1. RouteConfig dengan RequestInstance
Laravel named routes dengan RequestInstance:
<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):
<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:
routeNameadalah URL path dengan{param}placeholdersrouteParamsreplace placeholders →/api/cities/ID/searchurlParamsjadi query string →?limit=20&sortBy=name
3. Custom Label & Value Mapping
Transform option display dan emitted value:
<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):
<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:
- Component detect
modelValue= 123 - Request ke
/api/products/123 - Response added ke options
- Display label muncul (bukan cuma ID)
5. Custom List Slot
Full custom rendering dengan avatars, badges, status:
<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:
<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:
- Parent project class definitions - Form, input, dropdown classes
- Tailwind CSS utility classes - Spacing, colors, typography
- 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
.form-parent /* Outer container */
.form-group /* Field wrapper */
.form-disabled /* Disabled wrapper */
.select-component /* Select-specific */State modifiers: .err, .has-value, .opened
Input Elements
.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
Dropdown Elements
.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
.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
// WAJIB di main app entry
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';Provides virtual scroller container, scroll behavior, dynamic positioning.
Links
- Repository: [Internal GitHub]
- NPM Registry: https://js.pkg.ppsdmmigas.id/