DataTable
Data table component with pagination, filters, multi-selection, and built-in export.
Description
DataTable is a wrapper around vue3-easy-data-table that provides advanced features such as filters, server-side pagination, multi-selection, data export, and URL synchronization. It is designed to work with server data via an async load function.
Location: @/components/interface/Table/DataTable.vue
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
loadData | Function | ✅ Yes | - | Async function that loads data from the server |
columns | Array | ✅ Yes | - | Array of objects defining the table columns |
options | Object | ❌ No | {} | Table configuration options |
filters | Object | ❌ No | {} | Basic filters configuration object |
advanceFilters | Object | ❌ No | {} | Advanced filters configuration object |
itemsSelected | Boolean | ❌ No | undefined | Enables multi-row selection |
itemsSelectedFilter | Function | ❌ No | () => true | Function to filter which items can be selected |
bulkButtons | Object | ❌ No | {} | Bulk action buttons for selected items |
buttons | Object | ❌ No | {} | Additional table buttons |
exportFormat | String | Boolean | ❌ No | false | Export format: 'xlsx', 'csv' or false |
exportName | String | ❌ No | 'exported-data' | Export file name |
disableUrlUpdate | Boolean | ❌ No | false | Disables URL update with filters |
Column Structure
The columns prop must be an array of objects, each defining a table column.
Column Properties
{
text: 'Column Name', // Column title (required)
value: 'field_name', // Data object field (required)
sortable: true, // Allow sorting (optional, default: true)
exportable: true, // Include in export (optional, default: true)
visible: true, // Show/hide column (optional, default: true)
width: 100, // Column width in pixels (optional)
exportFormat: 'field_name', // Alternative field for export (optional)
scape: false, // Escape HTML in export (optional)
}Column Example
const columns = computed(() => [
{
text: t('datatable.column.name'),
value: 'name',
sortable: true,
},
{
text: t('datatable.column.email'),
value: 'email',
sortable: true,
},
{
text: t('datatable.column.status'),
value: 'status',
sortable: false,
},
{
text: t('datatable.column.actions'),
value: 'actions',
exportable: false,
sortable: false,
width: 100,
},
]);loadData Function
The loadData function is responsible for loading data from the server. It receives an object with pagination and filter options and must return a Promise that resolves with an object of the following shape:
{
data: Array, // Array of items to display in the table
recordsTotal: Number // Total number of records (for pagination)
}loadData Example
const loadData = async (filters) => {
const response = await api.users.get(filters);
return {
data: response.data,
recordsTotal: response.recordsTotal,
};
};loadData Parameters
The filters object passed to loadData contains:
{
page: 1, // Current page
rowsPerPage: 100, // Items per page
sortBy: 'name', // Sort field
sortType: 'ASC', // Sort type: 'ASC' | 'DESC'
filterLocale: false, // Filter by locale
localeId: 1, // Locale ID (if filterLocale is true)
// ... custom filters
search: 'query',
status: 'active',
// ... other filters defined in filters and advanceFilters
}Configuration Options
The options prop configures the table’s initial behavior:
const options = {
rowsPerPage: 50, // Items per page (default: 100)
sortBy: 'name', // Initial sort field
sortType: 'ASC', // Initial sort type: 'ASC' | 'DESC'
filterLocale: true, // Filter by user locale
page: 1, // Initial page
};Basic Filters
The filters prop defines the filters shown in the table’s top bar.
Filter Types
1. Input (el: 'input')
Simple text field.
const filters = computed(() => ({
name: {
className: 'col-12 col-lg-6',
label: t('datatable.column.name'),
el: 'input',
placeholder: 'Enter name',
value: '', // Initial value (optional)
},
}));2. Select (el: 'select')
Simple dropdown.
const filters = computed(() => ({
status: {
className: 'col-12 col-lg-6',
label: t('datatable.column.status'),
el: 'select',
options: [
{ value: 'active', label: 'Active' },
{ value: 'inactive', label: 'Inactive' },
],
value: '', // Initial value (optional)
},
}));3. Select2 (el: 'select2')
Advanced selector with multiple options.
const filters = computed(() => ({
carrier: {
className: 'col-12 col-lg-6',
label: t('datatable.column.carrier'),
el: 'select2',
options: carriers.value,
placeholder: t('header.all'),
mode: 'tags', // 'single' | 'tags' | 'multiple'
value: null, // Initial value (optional)
},
}));4. DatePicker (el: 'datePicker')
Date range selector.
const filters = computed(() => ({
dateRange: {
className: 'col-12 col-lg-6',
label: t('filters.dateRange'),
el: 'datePicker',
mode: 'range', // 'range' | 'month'
value: { // Initial value (optional)
startDate: new Date(),
endDate: new Date(),
},
names: { // Query field names (optional)
startDate: 'startDate',
endDate: 'endDate',
},
},
}));5. SearchMultiple (el: 'searchMultiple')
Multiple search with options.
const filters = computed(() => ({
search: {
className: 'col-12 col-lg-6',
el: 'searchMultiple',
options: [
{ key: 'tracking', label: t('filters.tracking') },
{ key: 'company', label: t('filters.companyId') },
{ key: 'ticket', label: t('filters.ticket') },
],
value: '', // Initial value (optional)
},
}));Common Filter Properties
className: Bootstrap CSS classes for layout (e.g.'col-12 col-lg-6')label: Filter labelel: Filter element typevalue: Initial filter valueplaceholder: Placeholder text (for inputs and selects)
Advanced Filters
The advanceFilters prop defines filters shown in a sliding panel (offcanvas). It uses the same structure as filters but is displayed in a separate panel.
const advanceFilters = computed(() => ({
created_at: {
className: 'col-12 col-lg-6',
label: t('datatable.column.createdAt'),
el: 'datePicker',
value: {
startDate: new Date(),
endDate: new Date(),
},
},
// ... more filters
}));To show the advanced filters panel, emit the event:
eventBus.emit('showAdvanceFilters');Multi-Selection
To enable multi-row selection, set itemsSelected to true:
<DataTable
:itemsSelected="true"
:itemsSelectedFilter="canSelectItem"
:bulkButtons="bulkActions"
/>itemsSelectedFilter
Function that determines whether an item can be selected:
const canSelectItem = (item) => {
// Return true if the item can be selected
return !item.deleted_at && item.status === 'active';
};bulkButtons
Object with bulk action buttons shown when items are selected:
const bulkActions = {
delete: {
text: t('actions.delete'),
icon: 'fa fa-trash',
class: 'btn btn-danger',
action: () => {
// Action to run with selected items
const selected = table.value.itemsSelected.value;
console.log('Selected items:', selected);
},
},
export: {
text: t('actions.export'),
icon: 'fa fa-download',
class: 'btn btn-primary',
action: () => {
// Another action
},
},
};Slots
Column Slots
You can customize any column’s content with the #item-{value} slot:
<DataTable :columns="columns" :loadData="loadData">
<template #item-name="item">
<div class="d-flex align-items-center gap-2">
<img :src="item.avatar" alt="" />
<span>{{ item.name }}</span>
</div>
</template>
<template #item-status="item">
<span :class="`badge bg-${item.status === 'active' ? 'success' : 'danger'}`">
{{ item.status }}
</span>
</template>
<template #item-actions="item">
<button @click="editItem(item)">Edit</button>
<button @click="deleteItem(item)">Delete</button>
</template>
</DataTable>Checkbox Header Slot
Customize the selection checkbox header:
<DataTable :itemsSelected="true">
<template #header-check>
<div class="custom-checkbox-header">
<!-- Custom content -->
</div>
</template>
</DataTable>Item Checkbox Slot
Customize each row’s checkbox:
<DataTable :itemsSelected="true">
<template #item-check="item">
<div class="custom-checkbox">
<!-- Custom content -->
</div>
</template>
</DataTable>Loading Slot
Customize the loading indicator:
<DataTable :loadData="loadData">
<template #loading>
<div class="custom-loader">
<em class="fa-solid fa-spinner fa-spin" />
<span>Loading data...</span>
</div>
</template>
</DataTable>Empty Message Slot
Customize the message when there is no data:
<DataTable :loadData="loadData">
<template #empty-message>
<div class="text-center">
<h3>No data available</h3>
<button @click="clearFilters">Clear Filters</button>
</div>
</template>
</DataTable>Extra Fields Slot
Add extra content above the table:
<DataTable :loadData="loadData">
<template #extra-fields="{ items, loading }">
<div class="mb-3">
<button @click="refreshData">Refresh</button>
<span>Total: {{ items.length }}</span>
</div>
</template>
</DataTable>Exposed Methods
The component exposes the following methods via ref:
reload(callback)
Reloads the table data.
const table = ref(null);
// Reload without callback
table.value.reload();
// Reload with callback
table.value.reload(() => {
console.log('Data reloaded');
});itemsSelected
Reactive ref with the selected items.
const table = ref(null);
// Access selected items
const selected = table.value.itemsSelected.value;
console.log('Selected items:', selected);cleanSelecteds()
Clears the item selection.
table.value.cleanSelecteds();loading(status)
Sets the loading state manually.
table.value.loading(true); // Enable loading
table.value.loading(false); // Disable loadingfilters()
Returns an object with all active filters.
const activeFilters = table.value.filters();
console.log('Active filters:', activeFilters);Data Export
To enable export, set exportFormat:
<DataTable
:exportFormat="'xlsx'"
:exportName="'users-report'"
:loadData="loadData"
/>Supported Formats
'xlsx': Export to Excel'csv': Export to CSVfalse: Disable export
Manual Export
To export manually, emit the event:
eventBus.emit('exportTable');EventBus Events
The component listens for the following EventBus events:
changeLocale
Changes the data locale:
eventBus.emit('changeLocale', {
detail: { action: 1 } // Locale ID
});exportTable
Exports the table with current filters:
eventBus.emit('exportTable');showAdvanceFilters
Shows the advanced filters panel:
eventBus.emit('showAdvanceFilters');onClearFilters
Clears all filters:
eventBus.emit('onClearFilters');URL Synchronization
By default, the component syncs filters with the URL query parameters. This allows:
- Sharing links with applied filters
- Reloading the page while keeping filters
- Back/forward navigation while preserving state
To disable this:
<DataTable
:disableUrlUpdate="true"
:loadData="loadData"
/>Usage Examples
Basic Example
<template>
<DataTable
ref="table"
:columns="columns"
:loadData="loadData"
/>
</template>
<script setup>
import { ref, computed } from 'vue';
import DataTable from '@/components/interface/Table/DataTable.vue';
import api from '@/services';
const { t } = useI18n();
const table = ref(null);
const columns = computed(() => [
{
text: t('datatable.column.name'),
value: 'name',
},
{
text: t('datatable.column.email'),
value: 'email',
},
{
text: t('datatable.column.status'),
value: 'status',
},
]);
const loadData = async (filters) => {
const response = await api.users.get(filters);
return {
data: response.data,
recordsTotal: response.recordsTotal,
};
};
</script>Example with Filters
<template>
<DataTable
ref="table"
:columns="columns"
:filters="filtersTable"
:loadData="loadData"
/>
</template>
<script setup>
import { ref, computed } from 'vue';
import DataTable from '@/components/interface/Table/DataTable.vue';
import api from '@/services';
const { t } = useI18n();
const table = ref(null);
const columns = computed(() => [
{
text: t('datatable.column.name'),
value: 'name',
},
{
text: t('datatable.column.email'),
value: 'email',
},
]);
const filtersTable = computed(() => ({
name: {
className: 'col-12 col-lg-6',
label: t('datatable.column.name'),
el: 'input',
placeholder: 'Search by name',
},
status: {
className: 'col-12 col-lg-6',
label: t('datatable.column.status'),
el: 'select',
options: [
{ value: 'active', label: 'Active' },
{ value: 'inactive', label: 'Inactive' },
],
},
}));
const loadData = async (filters) => {
const response = await api.users.get(filters);
return {
data: response.data,
recordsTotal: response.recordsTotal,
};
};
</script>Example with Multi-Selection
<template>
<DataTable
ref="table"
:columns="columns"
:itemsSelected="true"
:itemsSelectedFilter="canSelectItem"
:bulkButtons="bulkActions"
:loadData="loadData"
/>
</template>
<script setup>
import { ref, computed } from 'vue';
import DataTable from '@/components/interface/Table/DataTable.vue';
import api from '@/services';
const { t } = useI18n();
const table = ref(null);
const columns = computed(() => [
{
text: t('datatable.column.name'),
value: 'name',
},
{
text: t('datatable.column.email'),
value: 'email',
},
]);
const canSelectItem = (item) => {
return !item.deleted_at;
};
const bulkActions = {
delete: {
text: t('actions.delete'),
icon: 'fa fa-trash',
class: 'btn btn-danger',
action: async () => {
const selected = table.value.itemsSelected.value;
if (selected.length > 0) {
await api.users.bulkDelete(selected.map(item => item.id));
table.value.reload();
}
},
},
};
const loadData = async (filters) => {
const response = await api.users.get(filters);
return {
data: response.data,
recordsTotal: response.recordsTotal,
};
};
</script>Example with Custom Slots
<template>
<DataTable
ref="table"
:columns="columns"
:loadData="loadData"
>
<template #item-name="item">
<div class="d-flex align-items-center gap-2">
<img
:src="item.avatar"
:alt="item.name"
class="rounded-circle"
width="32"
height="32"
/>
<span>{{ item.name }}</span>
</div>
</template>
<template #item-status="item">
<span :class="`badge bg-${item.status === 'active' ? 'success' : 'danger'}`">
{{ item.status }}
</span>
</template>
<template #item-actions="item">
<div class="d-flex gap-2">
<button
class="btn btn-sm btn-primary"
@click="editItem(item)"
>
Edit
</button>
<button
class="btn btn-sm btn-danger"
@click="deleteItem(item)"
>
Delete
</button>
</div>
</template>
</DataTable>
</template>
<script setup>
import { ref, computed } from 'vue';
import DataTable from '@/components/interface/Table/DataTable.vue';
import api from '@/services';
const { t } = useI18n();
const table = ref(null);
const columns = computed(() => [
{
text: t('datatable.column.name'),
value: 'name',
},
{
text: t('datatable.column.status'),
value: 'status',
},
{
text: t('datatable.column.actions'),
value: 'actions',
exportable: false,
width: 150,
},
]);
const loadData = async (filters) => {
const response = await api.users.get(filters);
return {
data: response.data,
recordsTotal: response.recordsTotal,
};
};
const editItem = (item) => {
console.log('Edit item:', item);
};
const deleteItem = (item) => {
console.log('Delete item:', item);
};
</script>Example with Export
<template>
<DataTable
ref="table"
:columns="columns"
:exportFormat="'xlsx'"
:exportName="'users-report'"
:loadData="loadData"
/>
</template>
<script setup>
import { ref, computed } from 'vue';
import DataTable from '@/components/interface/Table/DataTable.vue';
import api from '@/services';
const { t } = useI18n();
const table = ref(null);
const columns = computed(() => [
{
text: t('datatable.column.name'),
value: 'name',
},
{
text: t('datatable.column.email'),
value: 'email',
},
]);
const loadData = async (filters) => {
const response = await api.users.get(filters);
return {
data: response.data,
recordsTotal: response.recordsTotal,
};
};
// Export manually
const exportData = () => {
eventBus.emit('exportTable');
};
</script>Important Notes
loadData function: It must always return an object with
dataandrecordsTotal, even on error.Server-side pagination: The component is designed for server-side pagination. Do not try to paginate data on the client.
Reactive filters: Filters should be
computedso they update reactively when options change.Translations: All text uses translation keys. Ensure translations are defined in your i18n files.
Exportable columns: Columns with
exportable: falseare not included in the export.Disabled rows: Rows with
deleted_atorutc_deleted_atare automatically shown with a disabled style.EventBus: The component uses EventBus for global communication. Ensure you import and use EventBus correctly.
URL sync: URL synchronization can cause issues with multiple tables on the same page. Consider using
disableUrlUpdatein that case.
