Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing a Data Table with Vue.js for Create, Read, Update, Delete, Copy, Revert, and Submit Operations

Tech 1

Table Feature Implementation using vxe-table@4.5.21

Implementation Overview

Template Structure

<vxe-table
  ref="dataTableRef"
  :data="displayData"
  keep-source
  :edit-config="{ trigger: 'dblclick', mode: 'cell', showStatus: true, enabled: isEditable }"
  style="width: 100%; user-select: text"
  height="100%"
  class="dynamic-vxe-table"
  :row-class-name="applyRowStyles"
  :cell-class-name="applyCellStyles"
  :row-config="{ useKey: true }"
  :tooltip-config="{ enterable: true, theme: 'light' }"
  v-loading="isLoading"
  border
  :scroll-x="{ enabled: true, gt: 10 }"
  :scroll-y="{ enabled: true, gt: 100 }"
  @cell-click="handleCellSelection"
>
  <vxe-column
    v-for="(colDef, idx) in columnDefinitions"
    :key="idx"
    :field="colDef.fieldName"
    :title="colDef.headerText"
    :show-header-overflow="true"
    min-width="180"
    :show-overflow="true"
    :edit-render="{ enabled: !!colDef.headerText, autofocus: '.el-input__inner' }"
  >
    <template #edit="{ row }">
      <el-input v-model="row[colDef.fieldName]" type="text" :placeholder="generatePlaceholder(row, colDef)"></el-input>
    </template>
  </vxe-column>
</vxe-table>

Template Attribute Configuration

keep-source: Essential for tracking edit states, temporary deletions, and data restoration.

edit-config: Configures cell editing mode activated by double-click; row mode is an alternative. showStatus combined with keep-source enables visual distinction between new rows, modified cels, and original data.

row-class-name: Applies custom CSS classes to selected rows for style modification.

cell-class-name: Applies custom CSS classes to selected cells for style modification.

row-config: useKey sets a unique identifier for each table row.

scroll-x/scroll-y: Configures virtual scrolling for handling large datasets.

Column-Specific Attributes

edit-render: autofocus ensures immediate focus on the input field when editing begins.

placeholder: Defines default hint text for editable cells.

Script Implementation

Reactive State Variables

import { VxeTableInstance, VxeTableEvents } from 'vxe-table';
const originalDataset = ref<any[]>([]); // Baseline data for revert operations
const displayData = ref<any[]>([]); // Data bound to the table
const columnDefinitions = ref<any[]>([]); // Column configuration (headers, placeholders, editability)
const updatePreviewParameters = ref<any>({}); // Parameters for preview operations
const dataTableRef = ref<VxeTableInstance>(); // Table component reference
const activeSelection = ref<any[]>([]); // Currently selected cells/rows

Computed Properties

// Determines if the table is editable
const isEditable = computed(() => {
  // Logic based on API response or state
  return ...;
});

// Checks if copy action is available
const isCopyActionDisabled = computed(() => {
  return !activeSelection.value[0]?.row;
});

// Checks if delete action is available
const isDeleteActionDisabled = computed(() => {
  return !activeSelection.value[0]?.row;
});

// Determines if revert action is available for selected items
const isRevertActionDisabled = computed(() => {
  const tableInstance = dataTableRef.value;
  if (tableInstance && activeSelection.value[0]) {
    for (let idx = 0; idx < activeSelection.value.length; idx++) {
      const selectedItem = activeSelection.value[idx];
      // Check for newly inserted rows
      if (!!tableInstance.isInsertByRow(selectedItem.row)) return false;
      // Check for cell or row updates
      if (selectedItem.field && tableInstance.isUpdateByRow(selectedItem.row, selectedItem.field)) {
        return false;
      }
      if (!selectedItem.field && tableInstance.isUpdateByRow(selectedItem.row)) {
        return false;
      }
      // Check for pending deletion markers
      if (isMarkedForRemoval(selectedItem.row)) {
        return false;
      }
    }
  }
  return true;
});

Table Interaction and Event Handlers

onMounted(() => {
  window.addEventListener('dblclick', clearSelectionOnExternalClick);
});

onUnmounted(() => {
  window.removeEventListener('dblclick', clearSelectionOnExternalClick);
});

// Clears selection when double-clicking outside the table
const clearSelectionOnExternalClick = (event?: any) => {
  activeSelection.value = [];
};

const handleCellSelection: VxeTableEvents.CellClick = ({ row, rowIndex, column, columnIndex, $event }) => {
  if (!isEditable.value) return;

  // Toggle selection if clicking the same cell/row
  if (activeSelection.value.length === 1) {
    const firstSelected = activeSelection.value[0];
    if (rowIndex === firstSelected.rowIndex && (columnIndex === firstSelected.columnIndex || (!columnIndex && !firstSelected.columnIndex))) {
      activeSelection.value = [];
      return;
    }
  }

  // Define the selection target (row or cell)
  const selectionTarget = { row, rowIndex };
  if (columnIndex != 0) {
    Object.assign(selectionTarget, { field: column.field, columnIndex });
  }

  // Handle modifier keys for multi-selection
  if ($event.ctrlKey) {
    handleCtrlMultiSelect(row, rowIndex);
    return;
  }
  if ($event.shiftKey) {
    handleShiftMultiSelect(rowIndex);
    return;
  }

  // Default single selection
  activeSelection.value = [selectionTarget];
};

const applyRowStyles = ({ rowIndex }: any) => {
  const matchedRowSelection = activeSelection.value.filter((item: any) => item.rowIndex === rowIndex);
  if (!matchedRowSelection[0]?.columnIndex && matchedRowSelection[0]?.rowIndex == rowIndex) {
    return 'selected-row-class'; // CSS class for selected rows
  }
  return '';
};

const applyCellStyles = ({ rowIndex, columnIndex }: any) => {
  const matchedCellSelection = activeSelection.value.filter((item: any) => item.rowIndex === rowIndex);
  if (matchedCellSelection[0]?.rowIndex == rowIndex && matchedCellSelection[0]?.columnIndex == columnIndex) {
    return 'selected-cell-class'; // CSS class for selected cells
  }
  return '';
};

const generatePlaceholder = (rowData: any, columnDef: any) => {
  let hintText = '';
  const tableInstance = dataTableRef.value;
  if (tableInstance) {
    // Logic to determine placeholder text
    // ...
  }
  return hintText;
};

Multi-Row Selection Logic

// CTRL-click for adding/removing individual rows from selection
const handleCtrlMultiSelect = (rowData: any, rowIdx: number) => {
  // Standardize selection to rows only
  activeSelection.value = activeSelection.value.map((item: any) => ({
    row: item.row,
    rowIndex: item.rowIndex
  }));

  const selectedIndices = activeSelection.value.map((item: any) => item.rowIndex);
  if (selectedIndices.includes(rowIdx)) {
    activeSelection.value = activeSelection.value.filter((item: any) => item.rowIndex !== rowIdx);
  } else {
    activeSelection.value.push({ row: rowData, rowIndex: rowIdx });
    activeSelection.value.sort((a: any, b: any) => a.rowIndex - b.rowIndex);
  }
};

// SHIFT-click for selecting a range of rows
const handleShiftMultiSelect = (targetRowIdx: number) => {
  // Standardize selection to rows only
  activeSelection.value = activeSelection.value.map((item: any) => ({
    row: item.row,
    rowIndex: item.rowIndex
  }));

  const selectedIndices = activeSelection.value.map((item: any) => item.rowIndex);
  const anchorIndex = selectedIndices[selectedIndices.length - 1] || targetRowIdx;
  const tableInstance = dataTableRef.value;

  if (tableInstance) {
    activeSelection.value = [];
    const allRows = tableInstance.getTableData().fullData || [];
    const startIdx = Math.min(anchorIndex, targetRowIdx);
    const endIdx = Math.max(anchorIndex, targetRowIdx);

    for (let idx = startIdx; idx <= endIdx; idx++) {
      activeSelection.value.push({ row: allRows[idx], rowIndex: idx });
    }
  }
};

Data Management Methods

/**
 * Populates the table with data.
 * @param headers Column header definitions
 * @param rawData Array of data objects
 */
const loadTableData = (headers: any[], rawData: any[]) => {
  const propertyMap: Record<string, string> = {};
  headers.forEach((header, index) => {
    propertyMap[index] = header.prop;
  });

  const processedData = rawData.map(dataRow => {
    const mappedRow: Record<string, any> = {};
    Object.keys(dataRow).forEach(key => {
      const mappedKey = propertyMap[key];
      if (mappedKey) {
        mappedRow[mappedKey] = dataRow[key];
      }
    });
    return mappedRow;
  });

  displayData.value = processedData;
  originalDataset.value = JSON.parse(JSON.stringify(processedData));
};

/**
 * Checks for unsaved changes (inserts, updates, pending deletions).
 */
const hasUnsavedChanges = () => {
  const tableInstance = dataTableRef.value;
  let modified = false;
  if (tableInstance) {
    const changeSet = tableInstance.getRecordset();
    const { insertRecords, updateRecords, pendingRecords } = changeSet;
    modified = [...insertRecords, ...updateRecords, ...pendingRecords].length > 0;
  }
  return modified;
};

/**
 * Adds a new row.
 * @param position Insertion index (null for top, -1 for bottom, specific index)
 */
const addNewRow = async (position?: number) => {
  const tableInstance = dataTableRef.value;
  if (tableInstance) {
    const newRows = tableInstance.getInsertRecords();
    const newRecord: Record<string, any> = {};
    columnDefinitions.value.forEach(col => {
      newRecord[col.fieldName] = null;
    });
    newRecord['RowID'] = displayData.value[displayData.value.length - 1]['RowID'] * 1 + newRows.length + 1;
    const { row: insertedRow } = await tableInstance.insertAt(newRecord, position);
    await tableInstance.setEditCell(insertedRow, columnDefinitions.value[1].fieldName);
  }
};

/**
 * Duplicates selected rows.
 */
const duplicateRows = async () => {
  const tableInstance = dataTableRef.value;
  if (tableInstance && activeSelection.value[0]?.row) {
    activeSelection.value.forEach(async (selectedItem) => {
      const existingNewRows = tableInstance.getInsertRecords();
      const duplicateRecord = {
        ...selectedItem.row,
        'RowID': displayData.value[displayData.value.length - 1]['RowID'] * 1 + existingNewRows.length + 1,
      };
      delete duplicateRecord['_X_ROW_KEY'];
      const { row: newRow } = await tableInstance.insertAt(duplicateRecord, -1);
      await tableInstance.setEditCell(newRow, columnDefinitions.value[1].fieldName);
    });
  }
};

/**
 * Removes rows (soft delete for original data, hard delete for new rows).
 * @param markForDeletion true to mark for deletion, false to restore
 */
const removeRows = (markForDeletion: boolean) => {
  const tableInstance = dataTableRef.value;
  if (tableInstance) {
    activeSelection.value.forEach((selectedItem) => {
      if (!!tableInstance.isInsertByRow(selectedItem.row)) {
        tableInstance.remove(selectedItem.row); // Remove newly added rows
      } else {
        tableInstance.revertData(selectedItem.row); // Revert any edits first
        tableInstance.setPendingRow(selectedItem.row, markForDeletion); // Mark/unmark for deletion
      }
    });
  }
};

/**
 * Reverts changes for selected items.
 */
const revertSelectedChanges = () => {
  const tableInstance = dataTableRef.value;
  if (tableInstance) {
    activeSelection.value.forEach((selectedItem) => {
      if (isMarkedForRemoval(selectedItem.row)) {
        removeRows(false); // Restore a soft-deleted row
      } else if (!!tableInstance.isInsertByRow(selectedItem.row)) {
        tableInstance.remove(selectedItem.row); // Remove a newly added row
      } else if (selectedItem.field) {
        tableInstance.revertData(selectedItem.row, selectedItem.field); // Revert a cell
      } else {
        tableInstance.revertData(selectedItem.row); // Revert an entire row
      }
    });
  }
};

/**
 * Generates a preview of pending changes.
 */
const previewPendingChanges = () => {
  if (!hasUnsavedChanges()) return;
  const tableInstance = dataTableRef.value;
  if (tableInstance) {
    const changeSet = tableInstance.getRecordset();
    const { insertRecords, pendingRecords, updateRecords } = changeSet;
    // Process and display changes for preview
    // ...
  }
};

/**
 * Submits all changes to the backend.
 */
const submitAllChanges = () => {
  if (!hasUnsavedChanges()) return;
  // Submission logic
  // ...
};

Style Configuration

Reference styles. Only selection styles are mandatory; others can use defaults.

.dynamic-vxe-table {
  .selected-row-class {
    .vxe-body--column {
      /* Selected row styling */
      box-shadow: inset 0 -0.5px 0 2px #007bff;
      user-select: none;
    }
  }
  .selected-cell-class {
    /* Selected cell styling */
    box-shadow: inset 0 0 0 2px #007bff;
  }
  .col--dirty {
    background-color: /* color for edited cells */;
    &::before {
      display: none;
    }
  }
  .row--new {
    background-color: /* color for new/copied rows */;
    .vxe-body--column {
      &::before {
        display: none;
      }
    }
  }
  .row--pending {
    background-color: /* color for rows marked for deletion */;
    color: inherit;
    text-decoration: none;
    cursor: default;
    .vxe-body--column::after {
      display: none;
    }
  }
}

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.