Source: customDatatable/customDatatable.js

import { refreshApex } from '@salesforce/apex';
import getColumns from '@salesforce/apex/CustomDatatableUtil.convertFieldSetToColumns';
import getRecordCount from '@salesforce/apex/CustomDatatableUtil.getRecordCount';
import getRecords from '@salesforce/apex/CustomDatatableUtil.getRecordsWithFieldSet';
import {
  addRowActions,
  buildDatatableProperties,
  deleteSelectedRecords,
  getSelectedRecords,
  handleRowAction,
  handleSave,
  showToast
} from 'c/datatableUtils';
import { NavigationMixin } from 'lightning/navigation';
import { LightningElement, api, track, wire } from 'lwc';

/**
 * A custom datatable with different configuration options.
 * @alias CustomDatatable
 * @extends LightningElement
 * @hideconstructor
 *
 * @example
 * <c-custom-datatable
 *   object-api-name="Case"
 *   field-set-api-name="CaseFieldSet"
 *   where-conditions="Status = 'New'"
 *   hide-checkbox-column
 *   show-row-number-column
 *   enable-pagination
 *   page-size="25"
 * ></c-custom-datatable>
 */
export default class CustomDatatable extends NavigationMixin(LightningElement) {
  /**
   * If show card option is active, the card icon is displayed in the header before the card title.
   * It should contain the SLDS name of the icon.
   * Specify the name in the format 'utility:down' where 'utility' is the category and 'down' the icon to be displayed.
   * @type {string}
   * @default ''
   * @example 'standard:case'
   */
  @api cardIcon = '';

  /**
   * If show card option is active, the card title can include text and is displayed in the header above the table.
   * @type {string}
   * @default ''
   */
  @api cardTitle = '';

  /**
   * Specifies how column widths are calculated. Set to 'fixed' for columns with equal widths.
   * Set to 'auto' for column widths that are based on the width of the column content and the table width.
   * @type {string}
   * @default 'fixed'
   */
  @api columnWidthsMode = 'fixed';

  /**
   * Specifies the default sorting direction on an unsorted column. Valid options include 'asc' and 'desc'.
   * @type {string}
   * @default 'asc'
   */
  @api defaultSortDirection = 'asc';

  /**
   * If present, enables server-side pagination with page navigation controls.
   * @type {boolean}
   * @default false
   */
  @api enablePagination = false;

  /**
   * If present, enables a server-side fuzzy search input that filters records across all text fields.
   * @type {boolean}
   * @default false
   */
  @api enableSearch = false;

  /**
   * API name of the field set that specifies which fields are displayed in the table.
   * @type {string}
   */
  @api fieldSetApiName = '';

  /**
   * If present, the checkbox column for row selection is hidden.
   * @type {boolean}
   * @default false
   */
  @api hideCheckboxColumn = false;

  /**
   * If present, the table header is hidden.
   * @type {boolean}
   * @default false
   */
  @api hideTableHeader = false;

  /**
   * If present, the table is wrapped with the correct page header to fit better into the related list layout.
   * @type {boolean}
   * @default false
   */
  @api isUsedAsRelatedList = false;

  /**
   * Required field for better table performance. Associates each row with a unique Id.
   * @type {string}
   * @default 'Id'
   */
  @api keyField = 'Id';

  /**
   * The maximum width for all columns. The default is 1000px.
   * @type {number}
   * @default 1000
   */
  @api maxColumnWidth = 1000;

  /**
   * The maximum number of rows that can be selected.
   * Checkboxes are used for selection by default, and radio buttons are used when maxRowSelection is 1.
   * @type {number}
   * @default 50
   */
  @api maxRowSelection = 50;

  /**
   * The minimum width for all columns. The default is 50px.
   * @type {number}
   * @default 50
   */
  @api minColumnWidth = 50;

  /**
   * API name of the object that will be displayed in the table.
   * @type {string}
   */
  @api objectApiName = '';

  /**
   * The number of records displayed per page when pagination is enabled.
   * @type {number}
   * @default 10
   */
  @api pageSize = 10;

  /**
   * If present, then all datatable fields are not editable.
   * @type {boolean}
   * @default false
   */
  @api readOnly = false;

  /**
   * If present, column resizing is disabled.
   * @type {boolean}
   * @default false
   */
  @api resizeColumnDisabled = false;

  /**
   * Determines where to start counting the row number.
   * @type {number}
   * @default 0
   */
  @api rowNumberOffset = 0;

  /**
   * If present, the table is wrapped in a lightning card to fit better into the overall page layout.
   * @type {boolean}
   * @default false
   */
  @api showCard = false;

  /**
   * If present, the last column contains a delete record action.
   * @type {boolean}
   * @default false
   */
  @api showDeleteRowAction = false;

  /**
   * If present, the last column contains a edit record action.
   * @type {boolean}
   * @default false
   */
  @api showEditRowAction = false;

  /**
   * If present, a delete action button is available when multiple records are selected.
   * This is only available if the checkbox column is visible and the table is either displayed with a Lightning Card
   * or as a Related List.
   * @type {boolean}
   * @default false
   */
  @api showMultipleRowDeleteAction = false;

  /**
   * If present, the row numbers are shown in the first column.
   * @type {boolean}
   * @default false
   */
  @api showRowNumberColumn = false;

  /**
   * If present, the last column contains a view record action.
   * @type {boolean}
   * @default false
   */
  @api showViewRowAction = false;

  /**
   * If present, the footer that displays the Save and Cancel buttons is hidden during inline editing.
   * @type {boolean}
   * @default false
   */
  @api suppressBottomBar = false;

  /**
   * Optional where clause conditions for loaded data records.
   * @type {string}
   * @default ''
   * @example Status = 'New'
   */
  @api whereConditions = '';

  @track columns = [];
  @track draftValues = [];
  @track records = [];
  @track selectedRecords = [];
  @track wiredRecords = [];

  isLoading = true;
  hasSelectedRecords = false;
  _currentPage = 1;
  _totalRecordCount = 0;
  _searchTerm = '';

  @wire(getColumns, { objectName: '$objectApiName', fieldSetName: '$fieldSetApiName', readOnly: '$readOnly' })
  wiredGetColumns({ data }) {
    if (data) {
      this.isLoading = false;
      this.columns = data.slice();
      this.addRowActions();
    }
  }

  @wire(getRecordCount, {
    objectName: '$objectApiName',
    fieldSetName: '$fieldSetApiName',
    whereConditions: '$whereConditions',
    searchTerm: '$currentSearchTerm'
  })
  wiredGetRecordCount({ data }) {
    if (data !== undefined) {
      this._totalRecordCount = data;
    }
  }

  @wire(getRecords, {
    objectName: '$objectApiName',
    fieldSetName: '$fieldSetApiName',
    whereConditions: '$whereConditions',
    pageSize: '$currentPageSize',
    pageNumber: '$currentPageNumber',
    searchTerm: '$currentSearchTerm'
  })
  wiredGetRecords(result) {
    this.wiredRecords = result;
    if (result.data) {
      this.records = result.data;
    } else if (result.error) {
      this.showToast('Error loading records', result.error.body?.message || 'Unknown error', 'error');
    }
  }

  get currentSearchTerm() {
    return this.enableSearch && this._searchTerm ? this._searchTerm : null;
  }

  get showSearch() {
    return this.enableSearch;
  }

  get currentPageSize() {
    return this.enablePagination ? this.pageSize : null;
  }

  get currentPageNumber() {
    return this.enablePagination ? this._currentPage : null;
  }

  get totalPages() {
    return Math.ceil(this._totalRecordCount / this.pageSize) || 1;
  }

  get isFirstPage() {
    return this._currentPage <= 1;
  }

  get isLastPage() {
    return this._currentPage >= this.totalPages;
  }

  get showPagination() {
    return this.enablePagination && this.totalPages > 1;
  }

  get paginationLabel() {
    return `Page ${this._currentPage} of ${this.totalPages}`;
  }

  get recordCountLabel() {
    const start = (this._currentPage - 1) * this.pageSize + 1;
    const end = Math.min(this._currentPage * this.pageSize, this._totalRecordCount);
    return `Showing ${start}\u2013${end} of ${this._totalRecordCount} records`;
  }

  get computedRowNumberOffset() {
    if (this.enablePagination) {
      return (this._currentPage - 1) * this.pageSize;
    }
    return this.rowNumberOffset;
  }

  get datatableProperties() {
    return buildDatatableProperties(this);
  }

  handleFirst() {
    this._currentPage = 1;
  }

  handlePrevious() {
    if (this._currentPage > 1) {
      this._currentPage--;
    }
  }

  handleNext() {
    if (this._currentPage < this.totalPages) {
      this._currentPage++;
    }
  }

  handleLast() {
    this._currentPage = this.totalPages;
  }

  handleSearchChange(event) {
    this._searchTerm = event.target.value;
    this._currentPage = 1;
  }

  addRowActions() {
    addRowActions(this.columns, this.showViewRowAction, this.showEditRowAction, this.showDeleteRowAction);
  }

  handleRowAction(event) {
    handleRowAction(this, event, () => refreshApex(this.wiredRecords));
  }

  handleSave(event) {
    handleSave(this, event.detail.draftValues, () => refreshApex(this.wiredRecords));
  }

  getSelectedRecords(event) {
    getSelectedRecords(this, event);
  }

  deleteSelectedRecords() {
    deleteSelectedRecords(this, this.selectedRecords, () => refreshApex(this.wiredRecords));
  }

  showToast(title, message, variant) {
    showToast(this, title, message, variant);
  }
}