Component Development Guide
Overview
This guide covers how to develop components in OATERS, from generic Vue components to business-specific Blade components.
Component Documentation
Detailed documentation for all generic Vue components is available:
Simple Components
- Alert - Dismissible notification alerts
- Autocomplete - AJAX-powered autocomplete input
- Avatar - User avatar with image/initials fallback
- Card - Card container with header and footer
- Chart - Chart.js integration with date filtering
- Counter - Dashboard statistics counter
- Loader - Hybrid loading spinner (Blade + Vue)
- Modal - Bootstrap 4 modal dialogs
- OrgChart - Organization hierarchy visualization
Component Bundles
- Breadcrumb - Breadcrumb navigation trail
- DataTable - Complete table component suite with filtering and pagination
- Form - Comprehensive form building system with validation and field management
- List - Bootstrap-styled list with collapsible items
- Navbar - Navigation bar component
- Tab - Tabbed interface with fade animations
- Table - Simple HTML table wrapper
- Timeline - Visual timeline for chronological events
Generic Vue Components
Generic Vue components are reusable UI components located in resources/components/. They should have no module-specific logic and be usable across all modules.
Creating a Generic Component
File: resources/components/Button.vue
<template>
<button
:class="['btn', `btn-${variant}`, { disabled: isDisabled }]"
:disabled="isDisabled"
@click="handleClick"
>
<slot />
</button>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
variant: {
type: String,
default: 'primary',
validator: (v) => ['primary', 'secondary', 'danger', 'success'].includes(v)
},
disabled: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['click'])
const isDisabled = computed(() => props.disabled || props.loading)
function handleClick(event) {
if (!isDisabled.value) {
emit('click', event)
}
}
</script>
<style scoped>
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-size: 1rem;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover:not(:disabled) {
background-color: #0056b3;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-success {
background-color: #28a745;
color: white;
}
</style>Component Best Practices
1. Use Composition API
<script setup>
import { ref, computed, onMounted } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
</script>2. Define Props with Validation
<script setup>
const props = defineProps({
items: {
type: Array,
required: true,
validator: (arr) => Array.isArray(arr)
},
loading: {
type: Boolean,
default: false
}
})
</script>3. Emit Events Explicitly
<script setup>
const emit = defineEmits(['update', 'delete', 'select'])
function updateItem(item) {
emit('update', item)
}
</script>4. Use Slots for Flexibility
<template>
<div class="card">
<div class="card-header">
<slot name="header" :title="title" />
</div>
<div class="card-body">
<slot :data="data" />
</div>
<div class="card-footer">
<slot name="footer" />
</div>
</div>
</template>5. Keep Styling Scoped
<style scoped>
/* This CSS only applies to this component */
.card {
border: 1px solid #ddd;
}
</style>Business Components (Blade Components)
Business components are Blade components that combine generic Vue components with business logic. They're module-specific and can access server-side data.
Example: Department Form Component
Here's a real example from OATERS that demonstrates how business components combine generic Vue components (Form, Field, Autocomplete, Select2) with module-specific logic:
File: Modules/Ruby/resources/views/components/modals/department-form.blade.php
@php
$edit ??= false;
@endphp
<modal id="{{$id}}" ref="{{$ref}}" static size="lg" color="{{$color}}">
<template #header>{{$title}}@if($edit) - @{{ openDepartment.name }}@endif</template>
<vue-form id="{{$id}}-form" ref="{{$ref}}Form" large="3" ajax action="{{url('r/departments/'.($edit? 'update' : 'create'))}}">
@if($edit)
<input type="hidden" name="id" :value="openDepartment.id">
@endif
<vue-field name="en[name]" type="text" id="{{($edit)? 'e_' : ''}}name-en">{{trans('common::words.name')}} ({{trans('common::words.english')}})</vue-field>
<vue-field name="ar[name]" type="text" id="{{($edit)? 'e_' : ''}}name-ar">{{trans('common::words.name')}} ({{trans('common::words.arabic')}})</vue-field>
<vue-field name="manager_id" type="autocomplete" id="{{($edit)? 'e_' : ''}}manager-id" url="{{route('ruby::contacts.search')}}">{{trans('ruby::departments.head')}}</vue-field>
<vue-field name="contact_id" type="select2" multiple id="{{($edit)? 'e_' : ''}}contact-id" url="{{route('ruby::contacts.search')}}">
{{trans('ruby::words.staff')}}
</vue-field>
</vue-form>
<template #footer>
<div class="btn btn-outline-secondary" data-dismiss="modal">Cancel</div>
<div class="btn btn-{{$color}} text-white" @click="submit('{{$ref}}')">Save</div>
</template>
</modal>Using Business Components in Views
This component is used in the departments list page:
File: Modules/Ruby/resources/views/departments.blade.php
<x-ruby::modals.department-form ref="createDepartment" id="add-department" color="green-2" title="{{trans('ruby::departments.new')}}"/>
<x-ruby::modals.department-form ref="updateDepartment" id="edit-department" color="blue-3" title="{{trans('ruby::departments.edit')}}" :edit="true"/>Key Features Demonstrated
This real example shows:
- Combining Generic Components: Uses
VueForm,VueField,Modal,Autocomplete, andSelect2components - Dynamic Properties: Props like
$edit,$id,$ref,$color, and$titleallow component reuse for create/edit operations - AJAX Integration: Form submission via AJAX to Laravel routes
- Server-Side Data: Autocomplete and select2 fields fetch data from server endpoints
- Internationalization: Uses
trans()helpers for multi-language support - Blade Syntax: Mixes Blade directives with Vue template syntax seamlessly
Module-Specific JavaScript Entry Points
Each page needs a JavaScript entry point to register Vue components. These files are automatically discovered by Vite using glob patterns.
File Structure
Location: resources/js/{moduleName}/{pageName}.js
Example: resources/js/ruby/employees.js
import { createApp } from 'vue'
import EmployeeTable from '@/components/EmployeeTable.vue'
import FilterPanel from '@/components/FilterPanel.vue'
import PaginationControl from '@/components/PaginationControl.vue'
// Create app instance
const app = createApp({})
// Register components globally for this page
app.component('EmployeeTable', EmployeeTable)
app.component('FilterPanel', FilterPanel)
app.component('PaginationControl', PaginationControl)
// Mount to #app element
app.mount('#app')Note: No changes to
vite.config.jsare needed. The glob pattern automatically discovers all files matchingresources/js/*.jsandresources/js/*/*.js.
Styling
Global Styles
File: resources/css/app.css
/* Global variables */
:root {
--color-primary: #007bff;
--color-secondary: #6c757d;
--color-danger: #dc3545;
--spacing-unit: 0.5rem;
}
/* Global utilities */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 1rem;
}
.d-flex {
display: flex;
}
.gap-1 {
gap: var(--spacing-unit);
}Module Styles
File: Modules/Ruby/resources/css/module.css
/* Ruby-specific styles */
.employee-form {
max-width: 600px;
}
.department-selector {
border: 1px solid var(--color-primary);
}Component Scoped Styles
Always use scoped styles in Vue components:
<style scoped>
.button {
/* Only applies to this component */
padding: 0.5rem 1rem;
}
</style>