Skip to content

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.

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

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

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

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

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

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

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

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

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

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

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

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

typescript
// 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 ​

typescript
// 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
vue
<!-- Good -->
<ProductCard />
<UserAvatar />
<DatePicker />

<!-- Avoid -->
<PC />
<UA />
<DP />

2. Props Design ​

  • Keep props simple and typed
  • Provide sensible defaults
  • Use validators for constraints
typescript
// 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
typescript
// Good
emit('user-selected', { userId: user.id, userName: user.name })
emit('form-submitted', formData)

// Avoid
emit('click') // Too generic
emit('data', stuff) // Unclear what data

4. Slot Design ​

  • Provide default content when useful
  • Use named slots for complex layouts
  • Document slot props
vue
<!-- 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
scss
.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 ​

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.

Build SaaS Products Without Limits.