Table Preferences
User-customizable column visibility, column ordering, and filter visibility for DataTable instances.
Overview
The Table Preferences system allows end users to personalize their table experience by:
- Toggling column visibility (show/hide columns)
- Reordering columns via drag-and-drop
- Toggling filter visibility (show/hide filters)
- Persisting preferences across sessions (localStorage + API backup)
- Resetting to defaults when needed
The system consists of two parts:
| Component | Location | Purpose |
|---|---|---|
useTablePreferences composable | @/composables/useTablePreferences.js | State management, persistence, merge logic |
TablePreferences component | @/components/interface/Table/Preferences/TablePreferences.offcanvas.vue | UI panel for configuring preferences |
Architecture
Persistence Flow
useTablePreferences Composable
Import and Usage
import { useTablePreferences } from '@/composables/useTablePreferences';
const {
visibleColumns,
visibleFilters,
draftColumns,
draftFilters,
toggleColumn,
reorderColumns,
toggleFilter,
resetDefaults,
initDraft,
save,
saving,
} = useTablePreferences('my-table-id', {
allColumns,
allFilters,
defaults: MY_TABLE_DEFAULTS,
});Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
tableId | string | Yes | Unique identifier for the table (e.g. 'crm-prospects'). Used as localStorage key and API identifier. |
options.allColumns | Ref<Array> | Array | Yes | All available column definitions. Each column must have at least text and value. |
options.allFilters | Ref<Object> | Object | No | All available filter definitions, keyed by filter name. |
options.defaults | Object | No | Default preferences { columns: [...], filters: [...] }. If not provided, all columns/filters are visible by default. |
Returned Values
| Return | Type | Description |
|---|---|---|
mergedColumns | ComputedRef<Array> | All columns merged with saved preferences (includes hidden ones) |
visibleColumns | ComputedRef<Array> | Only visible columns, sorted by order. Pass this to DataTable's :columns prop. |
mergedFilters | ComputedRef<Object> | All filters merged with saved preferences |
visibleFilters | ComputedRef<Object> | Only visible filters. Pass this to DataTable's :filters prop. |
draftColumns | ComputedRef<Array> | Draft columns for the preferences UI |
draftFilters | ComputedRef<Object> | Draft filters for the preferences UI |
toggleColumn | Function(value) | Toggle a column's visibility in draft state |
reorderColumns | Function(string[]) | Set new column order in draft state |
toggleFilter | Function(key) | Toggle a filter's visibility in draft state |
resetDefaults | Function | Reset draft state to defaults |
initDraft | Function | Sync draft state from saved state (call before opening the preferences panel) |
save | Function | Persist draft to localStorage and API |
saving | Ref<boolean> | Loading flag during save |
Draft vs Saved State
The composable uses a two-phase commit pattern:
- Draft state (
draftColumns,draftFilters): In-memory working copy. Modified bytoggleColumn,reorderColumns,toggleFilter. - Saved state (
mergedColumns,visibleColumns, etc.): Persisted state. Only updated whensave()is called.
This means users can toggle columns/filters in the preferences panel without affecting the table until they explicitly save.
Defaults File
Define a defaults file to control the initial column visibility/order and filter visibility for new users.
Structure
export const MY_TABLE_DEFAULTS = {
columns: [
{ value: 'name', visible: true, order: 0 },
{ value: 'email', visible: true, order: 1 },
{ value: 'status', visible: true, order: 2 },
{ value: 'created_at', visible: false, order: 3 },
{ value: 'notes', visible: false, order: 4 },
],
filters: [
{ key: 'search', visible: true },
{ key: 'status', visible: true },
{ key: 'dateRange', visible: false },
],
};Column Defaults
| Property | Type | Description |
|---|---|---|
value | string | Must match the value property in allColumns |
visible | boolean | Whether the column is visible by default |
order | number | Display order (0-based) |
Filter Defaults
| Property | Type | Description |
|---|---|---|
key | string | Must match the key in the allFilters object |
visible | boolean | Whether the filter is visible by default |
Real Example: CRM Table
// frontend/client/src/views/crm/constants/table.defaults.js
export const CRM_TABLE_DEFAULTS = {
columns: [
{ value: 'company_name', visible: true, order: 0 },
{ value: 'follow_name', visible: true, order: 1 },
{ value: 'lead_status', visible: true, order: 2 },
{ value: 'salesman_name', visible: false, order: 3 },
{ value: 'balance', visible: true, order: 4 },
{ value: 'account_value', visible: true, order: 5 },
// ... more columns
],
filters: [
{ key: 'search', visible: true },
{ key: 'follow', visible: true },
{ key: 'onboarding', visible: true },
{ key: 'ecommerce', visible: true },
{ key: 'type', visible: true },
{ key: 'createdAt', visible: true },
{ key: 'campaing', visible: false },
// ... more filters
],
};Locked Columns and Filters
Columns and filters can be marked as locked to prevent users from hiding them.
Locked Column
const allColumns = computed(() => [
{
text: t('datatable.column.company'),
value: 'company_name',
sortable: true,
locked: true, // Cannot be hidden or reordered
},
// ...
{
text: t('datatable.column.actions'),
value: 'actions',
sortable: false,
locked: true, // Cannot be hidden
exportable: false,
width: 100,
},
]);Locked Filter
const allFilters = computed(() => ({
search: {
className: 'col-12 col-lg-2 mb-2',
label: t('filters.search'),
el: 'input',
locked: true, // Cannot be hidden
},
// ...
}));TablePreferences Component
The UI component that renders the offcanvas panel with column and filter configurators.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
columns | Array | [] | Column definitions with value, text, visible, locked, order |
filters | Object | required | Filter configs with label, visible, locked |
saving | Boolean | false | Disables save button while saving |
showColumns | Boolean | true | Whether to show the column configurator |
Events
| Event | Payload | Description |
|---|---|---|
toggleColumn | columnValue: string | User toggled a column checkbox |
reorderColumns | string[] | User reordered columns via drag-and-drop |
toggleFilter | filterKey: string | User toggled a filter checkbox |
reset | none | User clicked "Reset to defaults" |
save | none | User clicked "Save" |
Exposed Methods
| Method | Description |
|---|---|
show() | Open the offcanvas panel |
hide() | Close the offcanvas panel |
Step-by-Step Integration Guide
1. Create a Defaults File
// views/my-module/constants/table.defaults.js
export const MY_TABLE_DEFAULTS = {
columns: [
{ value: 'name', visible: true, order: 0 },
{ value: 'status', visible: true, order: 1 },
{ value: 'created_at', visible: false, order: 2 },
],
filters: [
{ key: 'search', visible: true },
{ key: 'status', visible: true },
{ key: 'dateRange', visible: false },
],
};2. Define All Columns and Filters
const allColumns = computed(() => [
{
text: t('datatable.column.name'),
value: 'name',
sortable: true,
locked: true,
},
{
text: t('datatable.column.status'),
value: 'status',
sortable: false,
},
{
text: t('datatable.column.createdAt'),
value: 'created_at',
sortable: true,
},
{
text: t('datatable.column.actions'),
value: 'actions',
sortable: false,
locked: true,
exportable: false,
},
]);
const allFilters = computed(() => ({
search: {
className: 'col-12 col-lg-2 mb-2',
label: t('filters.search'),
el: 'input',
placeholder: t('filters.search.placeholder'),
locked: true,
},
status: {
className: 'col-12 col-lg-2 mb-2',
label: t('datatable.column.status'),
el: 'select',
options: [
{ value: 'active', text: t('labels.active') },
{ value: 'inactive', text: t('labels.inactive') },
],
},
dateRange: {
className: 'col-12 col-lg-2 mb-2',
label: t('filters.dateRange'),
el: 'datePicker',
},
}));3. Initialize the Composable
import { useTablePreferences } from '@/composables/useTablePreferences';
import { MY_TABLE_DEFAULTS } from '../constants/table.defaults';
const {
visibleColumns,
visibleFilters,
draftColumns,
draftFilters,
toggleColumn,
reorderColumns,
toggleFilter,
resetDefaults,
initDraft,
save,
saving,
} = useTablePreferences('my-module-table', {
allColumns,
allFilters,
defaults: MY_TABLE_DEFAULTS,
});4. Wire Up the Template
<template>
<TablePreferences
ref="preferencesPanel"
:columns="draftColumns"
:filters="draftFilters"
:saving="saving"
@toggleColumn="toggleColumn"
@reorderColumns="reorderColumns"
@toggleFilter="toggleFilter"
@reset="onReset"
@save="onSave"
/>
<DataTable
ref="table"
:columns="visibleColumns"
:options="options"
:filters="visibleFilters"
:loadData="loadData"
>
<!-- Column slots here -->
</DataTable>
</template>5. Handle Save and Reset
const preferencesPanel = ref();
const onSave = async () => {
await save();
preferencesPanel.value?.hide();
$toast.success(t('alert.success.update'));
};
const onReset = async () => {
resetDefaults();
await save();
preferencesPanel.value?.hide();
$toast.success(t('alert.success.update'));
};6. Expose Settings Trigger
defineExpose({
reload: () => table.value?.reload?.(),
showSettings: () => {
initDraft(); // Sync draft from saved before opening
preferencesPanel.value?.show();
},
});Adding a New Column to an Existing Table
To add a new column to a table that already uses preferences:
- Add to
allColumnswith the column definition:
{
text: t('crm.columns.lastRechargeDate'),
value: 'last_recharge_date',
sortable: false,
},- Add to defaults file (determines initial visibility for new users):
{ value: 'last_recharge_date', visible: false, order: 20 },- Add template slot if custom rendering is needed:
<template #item-last_recharge_date="item">
<span v-if="item.last_recharge_date">
{{ utils.convertDate(item.last_recharge_date, 'short') }}
</span>
<span v-else class="text-muted">—</span>
</template>- Add the data source in the backend query (see Table Backend Filters).
TIP
Existing users who already have saved preferences will not see the new column automatically. The column will appear in their preferences panel where they can toggle it on. Only new users (no saved preferences) will get the defaults.
Adding a New Filter to an Existing Table
- Add to
allFilterswith the filter definition:
hasRecharge: {
className: 'col-12 col-lg-2 mb-2',
label: t('crm.columns.hasRecharge'),
el: 'select',
options: [
{ value: '1', text: t('labels.yes') },
{ value: '0', text: t('labels.no') },
],
},- Add to defaults file:
{ key: 'hasRecharge', visible: false },- Add backend handling (see Table Backend Filters).
Persistence Details
localStorage Key Format
Keys follow the pattern table-prefs-{tableId}:
table-prefs-crm-prospects
table-prefs-shipments-tableStored Payload Shape
{
"columns": [
{ "value": "company_name", "visible": true, "order": 0 },
{ "value": "balance", "visible": true, "order": 1 },
{ "value": "created_at", "visible": false, "order": 2 }
],
"filters": [
{ "key": "search", "visible": true },
{ "key": "status", "visible": true },
{ "key": "dateRange", "visible": false }
]
}API Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /preferences/tables/:tableId | Fetch saved preferences |
| POST | /preferences/tables/:tableId | Save preferences |
Related Documentation
- DataTable Component - Core DataTable props, filters, slots, and examples
- Table Backend Filters - How to connect frontend filters to backend queries
