Component Design
This guide outlines the principles and patterns for creating components in the Employ platform. Following these guidelines will help ensure that your components are reusable, maintainable, and consistent with the rest of the codebase.
Component Hierarchy
Our components follow a clear hierarchy:
- Page Components: Found in
/pages, these use layout and module components - Layout Components: Define the overall structure of a page
- Module Components: Feature-specific components in
/modules/{feature}/components - UI Components: Reusable, generic UI elements
Component Structure
Every component should follow this basic structure:
<script setup lang="ts">
// 1. Imports (if needed)
import { SomeIcon } from 'lucide-vue-next';
// 2. Component props with TypeScript interface
interface Props {
title: string;
description?: string;
items: any[];
loading?: boolean;
}
// 3. Props with defaults
const props = withDefaults(defineProps<Props>(), {
description: '',
loading: false,
});
// 4. Emits
const emit = defineEmits(['itemSelected', 'refresh']);
// 5. Local state and computeds
const selectedItem = ref(null);
// 6. Methods/Functions
function handleItemClick(item) {
selectedItem.value = item;
emit('itemSelected', item);
}
</script>
<template>
<div>
<!-- Component template with clear structure -->
<div v-if="loading">Loading...</div>
<div v-else>
<h2>{{ title }}</h2>
<p v-if="description">{{ description }}</p>
<!-- List rendering with events -->
<ul>
<li
v-for="item in items"
:key="item.id"
@click="handleItemClick(item)"
>
{{ item.name }}
</li>
</ul>
</div>
</div>
</template>
Best Practices
1. Props Design
- Use TypeScript interfaces for all props
- Provide default values for optional props
- Document complex props
interface ApplicationListProps {
// Required props
applications: Application[];
// Optional props with comments
/** Controls loading state display */
loading?: boolean;
/** Error object to display error states */
error?: Error | null;
/** Title displayed at the top of the component */
title?: string;
}
2. Events and Emits
- Define all emits the component can trigger
- Use descriptive event names
- Pass relevant data with events
const emit = defineEmits([
'viewApplication', // When user clicks to view an application
'changeStatus', // When application status is changed
'refresh', // When data needs to be refreshed
]);
function viewApplication(id: string) {
emit('viewApplication', id);
}
3. Component Composition
Break large components into smaller, focused components:
<!-- Parent component -->
<template>
<div>
<ApplicationHeader :title="title" />
<ApplicationFilters @filter="applyFilters" />
<ApplicationTable
:items="filteredApplications"
@viewApplication="viewApplication"
/>
<ApplicationPagination :totalPages="totalPages" />
</div>
</template>
4. Loading and Error States
Always handle loading and error states:
<template>
<div>
<!-- Loading state -->
<div v-if="loading" class="loading-container">
<Spinner />
<p>Loading applications...</p>
</div>
<!-- Error state -->
<div v-else-if="error" class="error-container">
<AlertCircle class="text-red-500" />
<p>{{ error.message || 'Failed to load applications' }}</p>
<Button @click="retry">Retry</Button>
</div>
<!-- Content -->
<div v-else>
<!-- Main content here -->
</div>
</div>
</template>
Real-World Example: ApplicationList
Let's examine the ApplicationList component as an example:
<script setup lang="ts">
// Props with TypeScript interface
interface Props {
applications: any[];
loading?: boolean;
error?: Error | null;
searchTerm?: string;
filterStatus?: string;
// Additional props...
}
// Default props
const props = withDefaults(defineProps<Props>(), {
title: "Applications",
description: "View and manage all job applications",
emptyTitle: "No applications found",
// Additional defaults...
});
// Events
const emit = defineEmits(["viewApplication"]);
// Helper functions
function formatDate(date: Date | string | undefined) {
if (!date) return "";
return new Date(date).toLocaleDateString();
}
function getStatusColor(status: string) {
switch (status) {
case "APPLIED":
return props.compact ? "blue" : "bg-blue-500";
case "REVIEWED":
return props.compact ? "yellow" : "bg-yellow-500";
// Additional cases...
}
}
// Event handlers
function viewApplication(applicationId: string) {
emit("viewApplication", applicationId);
}
</script>
<template>
<ListingContainer
:title="title"
:icon="Users"
:description="description"
:isLoading="loading"
:error="error"
:isEmpty="applications.length === 0"
:emptyTitle="emptyTitle"
:emptyDescription="emptyDescription"
:emptyIcon="FileQuestion">
<!-- Filters section -->
<template v-if="!compact && !hideFilters" #filters>
<Filters
ref="filters"
:items="{ items: applications }"
@update:filteredItems="filteredData = $event"
@update:paginatedItems="paginatedData = $event"
:filterFields="{ /* filter configurations */ }"
:advancedOptions="{ /* options */ }"
:sortOptions="[ /* sort options */ ]"
:searchPlaceholder="'Search applications...'"
class="w-full" />
</template>
<!-- Pagination -->
<template #pagination>
<LoadMore
:total-count="filteredData.length"
:item-count="paginatedData.length"
:batch-size="3"
:filters="filters"
loadedText="All applications loaded"
buttonClass="min-w-[240px] font-medium"
class="w-full mt-4" />
</template>
<!-- Application items rendering -->
<!-- Content implementation... -->
</ListingContainer>
</template>
Component Documentation
For complex components, include documentation in comments:
<script setup lang="ts">
/**
* ApplicationList Component
*
* Displays a list of job applications with filtering, sorting, and pagination.
*
* @component
* @example
* <ApplicationList
* :applications="applications"
* :loading="isLoading"
* @viewApplication="handleViewApplication"
* />
*/
</script>
Component Directory Structure
Organize your components in feature-specific directories:
modules/
platform/
application/
components/
ApplicationList.vue # Main listing component
ApplicationListItem.vue # Individual item component
ApplicationDetails.vue # Details view
ApplicationFilters.vue # Filtering component
ApplicationStatusBadge.vue # Status display component
By following these guidelines, you'll create components that are easy to use, maintain, and integrate into the Employ platform.