Skip to content

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:

ComponentLocationPurpose
useTablePreferences composable@/composables/useTablePreferences.jsState management, persistence, merge logic
TablePreferences component@/components/interface/Table/Preferences/TablePreferences.offcanvas.vueUI panel for configuring preferences

Architecture

Persistence Flow

useTablePreferences Composable

Import and Usage

javascript
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

ParameterTypeRequiredDescription
tableIdstringYesUnique identifier for the table (e.g. 'crm-prospects'). Used as localStorage key and API identifier.
options.allColumnsRef<Array> | ArrayYesAll available column definitions. Each column must have at least text and value.
options.allFiltersRef<Object> | ObjectNoAll available filter definitions, keyed by filter name.
options.defaultsObjectNoDefault preferences { columns: [...], filters: [...] }. If not provided, all columns/filters are visible by default.

Returned Values

ReturnTypeDescription
mergedColumnsComputedRef<Array>All columns merged with saved preferences (includes hidden ones)
visibleColumnsComputedRef<Array>Only visible columns, sorted by order. Pass this to DataTable's :columns prop.
mergedFiltersComputedRef<Object>All filters merged with saved preferences
visibleFiltersComputedRef<Object>Only visible filters. Pass this to DataTable's :filters prop.
draftColumnsComputedRef<Array>Draft columns for the preferences UI
draftFiltersComputedRef<Object>Draft filters for the preferences UI
toggleColumnFunction(value)Toggle a column's visibility in draft state
reorderColumnsFunction(string[])Set new column order in draft state
toggleFilterFunction(key)Toggle a filter's visibility in draft state
resetDefaultsFunctionReset draft state to defaults
initDraftFunctionSync draft state from saved state (call before opening the preferences panel)
saveFunctionPersist draft to localStorage and API
savingRef<boolean>Loading flag during save

Draft vs Saved State

The composable uses a two-phase commit pattern:

  1. Draft state (draftColumns, draftFilters): In-memory working copy. Modified by toggleColumn, reorderColumns, toggleFilter.
  2. Saved state (mergedColumns, visibleColumns, etc.): Persisted state. Only updated when save() 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

javascript
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

PropertyTypeDescription
valuestringMust match the value property in allColumns
visiblebooleanWhether the column is visible by default
ordernumberDisplay order (0-based)

Filter Defaults

PropertyTypeDescription
keystringMust match the key in the allFilters object
visiblebooleanWhether the filter is visible by default

Real Example: CRM Table

javascript
// 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

javascript
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

javascript
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

PropTypeDefaultDescription
columnsArray[]Column definitions with value, text, visible, locked, order
filtersObjectrequiredFilter configs with label, visible, locked
savingBooleanfalseDisables save button while saving
showColumnsBooleantrueWhether to show the column configurator

Events

EventPayloadDescription
toggleColumncolumnValue: stringUser toggled a column checkbox
reorderColumnsstring[]User reordered columns via drag-and-drop
toggleFilterfilterKey: stringUser toggled a filter checkbox
resetnoneUser clicked "Reset to defaults"
savenoneUser clicked "Save"

Exposed Methods

MethodDescription
show()Open the offcanvas panel
hide()Close the offcanvas panel

Step-by-Step Integration Guide

1. Create a Defaults File

javascript
// 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

javascript
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

javascript
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

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

javascript
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

javascript
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:

  1. Add to allColumns with the column definition:
javascript
{
    text: t('crm.columns.lastRechargeDate'),
    value: 'last_recharge_date',
    sortable: false,
},
  1. Add to defaults file (determines initial visibility for new users):
javascript
{ value: 'last_recharge_date', visible: false, order: 20 },
  1. Add template slot if custom rendering is needed:
vue
<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">&mdash;</span>
</template>
  1. 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

  1. Add to allFilters with the filter definition:
javascript
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') },
    ],
},
  1. Add to defaults file:
javascript
{ key: 'hasRecharge', visible: false },
  1. 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-table

Stored Payload Shape

json
{
    "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

MethodEndpointDescription
GET/preferences/tables/:tableIdFetch saved preferences
POST/preferences/tables/:tableIdSave preferences

Envia Admin