Component Guide
This guide covers how to use and create components in Qelos, including global components, custom components, and best practices.
🧩 Global Components
Qelos provides a rich set of global components that you can use throughout your application. These components are automatically available and don't need to be imported.
Form Components
Form Input
The most versatile input component with built-in validation.
<script setup>
const formData = ref({
name: '',
email: '',
password: ''
})
const validation = {
name: { required: true, minLength: 3 },
email: { required: true, type: 'email' },
password: { required: true, minLength: 8 }
}
</script>
<template>
<form-input
v-model="formData.name"
label="Full Name"
placeholder="Enter your name"
:validation="validation.name"
:debounce="300"
/>
<form-input
v-model="formData.email"
label="Email Address"
type="email"
:validation="validation.email"
error-message="Please enter a valid email"
/>
<form-input
v-model="formData.password"
label="Password"
type="password"
:validation="validation.password"
show-password-toggle
/>
</template>Form Select
Dropdown selection with search and multi-select capabilities.
<script setup>
const selectedCountry = ref('')
const selectedTags = ref([])
const countries = [
{ value: 'us', label: 'United States', code: '+1' },
{ value: 'uk', label: 'United Kingdom', code: '+44' },
{ value: 'fr', label: 'France', code: '+33' }
]
const tags = [
{ value: 'vue', label: 'Vue.js' },
{ value: 'react', label: 'React' },
{ value: 'angular', label: 'Angular' }
]
</script>
<template>
<!-- Single selection -->
<form-select
v-model="selectedCountry"
:options="countries"
label="Country"
placeholder="Select a country"
clearable
/>
<!-- Multiple selection -->
<form-select
v-model="selectedTags"
:options="tags"
label="Technologies"
placeholder="Select technologies"
multiple
:max-tags="5"
/>
</template>Form Date Picker
Date selection with various modes and formats.
<script setup>
const birthDate = ref('')
const dateRange = ref([])
const dateTime = ref('')
const shortcuts = [
{
text: 'Today',
value: new Date()
},
{
text: 'Yesterday',
value: () => {
const date = new Date()
date.setDate(date.getDate() - 1)
return date
}
}
]
</script>
<template>
<!-- Date only -->
<form-date-picker
v-model="birthDate"
label="Birth Date"
placeholder="Select date"
:shortcuts="shortcuts"
:disabled-date="disableFutureDates"
/>
<!-- Date range -->
<form-date-picker
v-model="dateRange"
label="Date Range"
type="daterange"
placeholder="Select range"
/>
<!-- Date and time -->
<form-date-picker
v-model="dateTime"
label="Appointment"
type="datetime"
placeholder="Select date and time"
/>
</template>Form Switch
Toggle switch for boolean values.
<script setup>
const settings = ref({
notifications: true,
darkMode: false,
autoSave: true
})
</script>
<template>
<form-switch
v-model="settings.notifications"
label="Enable Notifications"
description="Receive email notifications for important updates"
/>
<form-switch
v-model="settings.darkMode"
label="Dark Mode"
:disabled="!settings.premium"
/>
<form-switch
v-model="settings.autoSave"
label="Auto Save"
active-text="On"
inactive-text="Off"
/>
</template>Display Components
Quick Table
Powerful data table with sorting, filtering, and pagination.
<script setup>
const tableData = ref([])
const loading = ref(false)
const pagination = ref({
page: 1,
pageSize: 20,
total: 0
})
const columns = [
{
key: 'name',
label: 'Name',
sortable: true,
width: '200px'
},
{
key: 'email',
label: 'Email',
sortable: true
},
{
key: 'role',
label: 'Role',
filterable: true,
filters: [
{ text: 'Admin', value: 'admin' },
{ text: 'User', value: 'user' }
]
},
{
key: 'status',
label: 'Status',
component: 'status-badge'
},
{
key: 'actions',
label: 'Actions',
width: '120px',
fixed: 'right'
}
]
const handleSort = ({ prop, order }) => {
// Handle sorting
loadUsers()
}
const handleFilter = (filters) => {
// Handle filtering
loadUsers()
}
const handleRowClick = (row) => {
// Navigate to user details
router.push(`/users/${row.id}`)
}
</script>
<template>
<quick-table
:data="tableData"
:columns="columns"
:loading="loading"
:pagination="pagination"
stripe
border
@sort-change="handleSort"
@filter-change="handleFilter"
@row-click="handleRowClick"
>
<!-- Custom column slot -->
<template #status="{ row }">
<status-badge :status="row.status" />
</template>
<!-- Actions column -->
<template #actions="{ row }">
<el-button size="small" @click.stop="editUser(row)">
Edit
</el-button>
<el-button size="small" type="danger" @click.stop="deleteUser(row)">
Delete
</el-button>
</template>
</quick-table>
</template>Stat Card
Display key metrics with trends and icons.
<script setup>
const stats = ref([
{
title: 'Total Users',
value: 12543,
trend: '+12.5%',
trendDirection: 'up',
icon: 'users',
color: 'primary'
},
{
title: 'Revenue',
value: 89345,
format: 'currency',
trend: '+8.2%',
trendDirection: 'up',
icon: 'dollar',
color: 'success'
},
{
title: 'Conversion Rate',
value: 3.24,
format: 'percentage',
trend: '-2.1%',
trendDirection: 'down',
icon: 'chart',
color: 'warning'
}
])
</script>
<template>
<grid-layout :cols="{ xs: 1, sm: 2, lg: 4 }" gap="20">
<stat-card
v-for="stat in stats"
:key="stat.title"
:title="stat.title"
:value="stat.value"
:format="stat.format"
:trend="stat.trend"
:trend-direction="stat.trendDirection"
:icon="stat.icon"
:color="stat.color"
/>
</grid-layout>
</template>Data Card
Container component for grouping related content.
<template>
<data-card
title="Product Details"
:loading="loading"
:border="true"
:shadow="'hover'"
>
<!-- Header extra content -->
<template #extra>
<el-button-group>
<el-button size="small" @click="editProduct">
Edit
</el-button>
<el-button size="small" type="danger" @click="deleteProduct">
Delete
</el-button>
</el-button-group>
</template>
<!-- Card content -->
<div class="product-info">
<el-descriptions :column="2" border>
<el-descriptions-item label="Name">
{{ product.name }}
</el-descriptions-item>
<el-descriptions-item label="Price">
{{ formatCurrency(product.price) }}
</el-descriptions-item>
<el-descriptions-item label="Category">
{{ product.category }}
</el-descriptions-item>
<el-descriptions-item label="Status">
<el-tag :type="product.status === 'active' ? 'success' : 'danger'">
{{ product.status }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
</div>
<!-- Footer actions -->
<template #footer>
<el-button @click="viewAnalytics">View Analytics</el-button>
<el-button type="primary">Export Data</el-button>
</template>
</data-card>
</template>Layout Components
Page Container
Main layout wrapper for pages.
<template>
<page-container>
<!-- Page header -->
<template #header>
<div class="page-header">
<page-title :breadcrumb="breadcrumb">
User Management
</page-title>
<div class="header-actions">
<el-button @click="exportUsers">
<export-icon /> Export
</el-button>
<el-button type="primary" @click="showCreateDialog = true">
<plus-icon /> Add User
</el-button>
</div>
</div>
</template>
<!-- Sidebar (optional) -->
<template #sidebar>
<filter-panel
v-model="filters"
:options="filterOptions"
@apply="applyFilters"
/>
</template>
<!-- Main content -->
<div class="page-content">
<quick-table :data="users" :columns="columns" />
</div>
</page-container>
</template>Grid Layout
Responsive grid system for layouts.
<script setup>
const gridCols = {
xs: 1, // Mobile: 1 column
sm: 2, // Small tablets: 2 columns
md: 3, // Tablets: 3 columns
lg: 4, // Desktop: 4 columns
xl: 5 // Large desktop: 5 columns
}
</script>
<template>
<!-- Equal columns -->
<grid-layout :cols="gridCols" gap="20">
<product-card v-for="product in products" :key="product.id" />
</grid-layout>
<!-- Custom column spans -->
<grid-layout :cols="12" gap="20">
<div :span="8">Main content (8/12)</div>
<div :span="4">Sidebar (4/12)</div>
</grid-layout>
<!-- Masonry layout -->
<grid-layout :cols="gridCols" gap="20" masonry>
<div v-for="item in items" :key="item.id" :style="{ height: item.height + 'px' }">
{{ item.content }}
</div>
</grid-layout>
</template>🎨 Creating Custom Components
Component Structure
<!-- components/MyComponent.vue -->
<script setup>
import { computed, ref } from 'vue'
// Props definition
const props = defineProps({
// Basic props
title: {
type: String,
required: true
},
// Prop with default
size: {
type: String,
default: 'medium',
validator: (value) => ['small', 'medium', 'large'].includes(value)
},
// Complex prop
config: {
type: Object,
default: () => ({
showHeader: true,
bordered: false
})
}
})
// Emits definition
const emit = defineEmits([
'update', // For v-model
'click',
'custom-event'
])
// Local state
const isActive = ref(false)
const count = ref(0)
// Computed properties
const sizeClass = computed(() => `my-component--${props.size}`)
const hasHeader = computed(() => props.config.showHeader && props.title)
// Methods
const handleClick = () => {
isActive.value = !isActive.value
emit('click', { active: isActive.value })
}
const increment = () => {
count.value++
emit('update', count.value)
}
// Watchers
watch(count, (newVal) => {
if (newVal > 10) {
emit('custom-event', { count: newVal })
}
})
// Lifecycle
onMounted(() => {
console.log('Component mounted')
})
</script>
<template>
<div
:class="['my-component', sizeClass]"
:class="{ 'is-active': isActive }"
@click="handleClick"
>
<!-- Conditional header -->
<header v-if="hasHeader" class="my-component__header">
<h3>{{ title }}</h3>
<slot name="header-extra" />
</header>
<!-- Default slot -->
<main class="my-component__body">
<slot>
Default content when no slot provided
</slot>
</main>
<!-- Named slots -->
<footer v-if="$slots.footer" class="my-component__footer">
<slot name="footer" />
</footer>
</div>
</template>
<style scoped>
.my-component {
/* Use design tokens */
background: var(--qelos-surface);
border: 1px solid var(--qelos-border);
border-radius: var(--qelos-border-radius);
padding: var(--qelos-spacing-md);
transition: all 0.3s ease;
}
.my-component:hover {
border-color: var(--qelos-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.my-component.is-active {
background: var(--qelos-primary-light);
border-color: var(--qelos-primary);
}
/* Size modifiers */
.my-component--small {
padding: var(--qelos-spacing-sm);
}
.my-component--large {
padding: var(--qelos-spacing-lg);
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.my-component {
background: var(--qelos-surface-dark);
border-color: var(--qelos-border-dark);
}
}
</style>Advanced Component Patterns
Renderless Component
<!-- components/RenderlessList.vue -->
<script setup>
const props = defineProps({
items: Array,
filterKey: String
})
const filteredItems = computed(() => {
if (!props.filterKey) return props.items
return props.items.filter(item =>
item.name.toLowerCase().includes(props.filterKey.toLowerCase())
)
})
const slots = useSlots()
</script>
<template>
<!-- Don't render anything, just provide data -->
<slot
:items="filteredItems"
:count="filteredItems.length"
/>
</template>
<!-- Usage -->
<renderless-list :items="users" filter-key="search">
<template #default="{ items, count }">
<div>
<p>Found {{ count }} users</p>
<user-list :users="items" />
</div>
</template>
</renderless-list>Compound Component
<!-- components/CompoundCard.vue -->
<script setup>
import { provide, ref } from 'vue'
const props = defineProps({
title: String,
bordered: Boolean
)
const isExpanded = ref(false)
const cardId = Symbol('card')
provide('card', {
id: cardId,
title: props.title,
isExpanded,
toggle: () => isExpanded.value = !isExpanded.value
})
</script>
<template>
<div class="compound-card" :class="{ bordered }">
<slot />
</div>
</template>
<!-- components/CompoundCardHeader.vue -->
<script setup>
import { inject } from 'vue'
const card = inject('card')
</script>
<template>
<header class="compound-card__header" @click="card.toggle">
<h3>{{ card.title }}</h3>
<chevron-icon :class="{ 'is-expanded': card.isExpanded }" />
</header>
</template>
<!-- Usage -->
<compound-card title="User Details">
<compound-card-header />
<div v-if="isExpanded" class="compound-card__body">
User content here
</div>
</compound-card>🔌 Plugin Components
Registering Components in Plugins
// plugin.ts
import { definePlugin } from '@qelos/plugin-play'
import MyComponent from './components/MyComponent.vue'
import ProductCard from './components/ProductCard.vue'
export default definePlugin({
name: 'my-plugin',
version: '1.0.0',
// Register components globally
components: {
'my-component': MyComponent,
'product-card': ProductCard
},
// Component-specific configuration
config: {
components: {
'product-card': {
defaultVariant: 'default',
showPrice: true
}
}
}
})Component Overrides
// Override existing components
export default definePlugin({
overrides: {
// Override a global component
'quick-table': CustomTable,
// Override with conditional logic
'form-input': {
component: EnhancedFormInput,
condition: () => user.value?.premium
}
}
})🎯 Best Practices
1. Naming Conventions
- Use PascalCase for component names
- Be descriptive but concise
- Prefix with your plugin name if needed
<!-- Good -->
<ProductCard />
<UserAvatar />
<DatePicker />
<!-- Avoid -->
<PC />
<UA />
<DP />2. Props Design
- Keep props simple and typed
- Provide sensible defaults
- Use validators for constraints
// Good
const props = defineProps({
size: {
type: String,
default: 'medium',
validator: (value) => ['small', 'medium', 'large'].includes(value)
}
})
// Avoid
const props = defineProps({
s: String // Unclear what this means
})3. Event Naming
- Use kebab-case for event names
- Be descriptive about what happened
- Include relevant data
// Good
emit('user-selected', { userId: user.id, userName: user.name })
emit('form-submitted', formData)
// Avoid
emit('click') // Too generic
emit('data', stuff) // Unclear what data4. Slot Design
- Provide default content when useful
- Use named slots for complex layouts
- Document slot props
<!-- Good -->
<slot name="header" :title="title" :subtitle="subtitle">
<h2>{{ title }}</h2>
</slot>
<!-- Avoid -->
<slot /> <!-- No context provided -->5. Styling
- Use CSS custom properties (design tokens)
- Scope styles to avoid conflicts
- Consider dark mode
.my-component {
/* Use design tokens */
background: var(--qelos-surface);
color: var(--qelos-text-primary);
/* Responsive */
@media (max-width: 768px) {
padding: var(--qelos-spacing-sm);
}
/* Dark mode */
@media (prefers-color-scheme: dark) {
background: var(--qelos-surface-dark);
}
}📚 Component Library
Available Components
- Form Components - Input, select, date picker, switch
- Data Components - Table, editor, grid
- Display Components - Card, badge, avatar
- Layout Components - Container, grid, section
- Navigation Components - Breadcrumb, menu, tabs
- Feedback Components - Empty state, loading, toast
- AI Components - Chat, form assist
Component Playground
Visit the component playground to see interactive examples: https://playground.qelos.io
This component system provides everything you need to build consistent, accessible, and beautiful interfaces with Qelos.
