import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ContentChild, TemplateRef } from '@angular/core'; import { CommonModule } from '@angular/common'; export type TableVariant = 'default' | 'striped' | 'bordered' | 'minimal'; export type TableSize = 'compact' | 'default' | 'comfortable'; export type TableColumnAlign = 'left' | 'center' | 'right'; export interface TableColumn { key: string; label: string; sortable?: boolean; align?: TableColumnAlign; width?: string; sticky?: boolean; } export interface TableSortEvent { column: string; direction: 'asc' | 'desc' | null; } @Component({ selector: 'ui-table', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
@if (showHeader && columns.length > 0) { @for (column of columns; track column.key) { } } @if (data.length === 0 && showEmptyState) { } @else { @for (row of data; track trackByFn ? trackByFn($index, row) : $index; let rowIndex = $index) { @for (column of columns; track column.key) { } } }
{{ column.label }} @if (column.sortable && sortColumn === column.key) { @if (sortDirection === 'asc') { ↑ } @else if (sortDirection === 'desc') { ↓ } }
@if (emptyTemplate) { } @else {
{{ emptyMessage }}
}
@if (getCellTemplate(column.key)) { } @else { {{ getCellValue(row, column.key) }} }
@if (loading) {
{{ loadingMessage }}
}
`, styleUrl: './table.component.scss' }) export class TableComponent { @Input() data: any[] = []; @Input() columns: TableColumn[] = []; @Input() variant: TableVariant = 'default'; @Input() size: TableSize = 'default'; @Input() showHeader: boolean = true; @Input() showEmptyState: boolean = true; @Input() emptyMessage: string = 'No data available'; @Input() loading: boolean = false; @Input() loadingMessage: string = 'Loading...'; @Input() hoverable: boolean = true; @Input() selectable: boolean = false; @Input() stickyHeader: boolean = false; @Input() maxHeight: string = ''; @Input() sortColumn: string = ''; @Input() sortDirection: 'asc' | 'desc' | null = null; @Input() trackByFn?: (index: number, item: any) => any; @Input() cellTemplates: Record> = {}; @Input() class: string = ''; @Output() rowClick = new EventEmitter<{row: any, index: number}>(); @Output() sort = new EventEmitter(); @Output() selectionChange = new EventEmitter(); @ContentChild('emptyTemplate') emptyTemplate?: TemplateRef; selectedRows: Set = new Set(); get tableContainerClasses(): string { return [ 'ui-table-container', this.stickyHeader ? 'ui-table-container--sticky-header' : '', this.maxHeight ? 'ui-table-container--max-height' : '', this.loading ? 'ui-table-container--loading' : '', this.class ].filter(Boolean).join(' '); } get tableClasses(): string { return [ 'ui-table', `ui-table--${this.variant}`, `ui-table--${this.size}`, this.hoverable ? 'ui-table--hoverable' : '', this.selectable ? 'ui-table--selectable' : '', ].filter(Boolean).join(' '); } getHeaderCellClasses(column: TableColumn): string { return [ 'ui-table__header-cell', `ui-table__header-cell--${column.align || 'left'}`, column.sortable ? 'ui-table__header-cell--sortable' : '', column.sticky ? 'ui-table__header-cell--sticky' : '', this.sortColumn === column.key ? 'ui-table__header-cell--sorted' : '' ].filter(Boolean).join(' '); } getBodyCellClasses(column: TableColumn, row: any): string { return [ 'ui-table__body-cell', `ui-table__body-cell--${column.align || 'left'}`, column.sticky ? 'ui-table__body-cell--sticky' : '' ].filter(Boolean).join(' '); } getRowClasses(row: any, index: number): string { return [ 'ui-table__body-row', this.selectedRows.has(row) ? 'ui-table__body-row--selected' : '', index % 2 === 1 && this.variant === 'striped' ? 'ui-table__body-row--striped' : '' ].filter(Boolean).join(' '); } handleSort(column: TableColumn): void { if (!column.sortable) return; let newDirection: 'asc' | 'desc' | null; if (this.sortColumn === column.key) { // Toggle through: asc -> desc -> null -> asc if (this.sortDirection === 'asc') { newDirection = 'desc'; } else if (this.sortDirection === 'desc') { newDirection = null; } else { newDirection = 'asc'; } } else { newDirection = 'asc'; } this.sort.emit({ column: column.key, direction: newDirection }); } handleRowClick(row: any, index: number): void { if (this.selectable) { this.toggleRowSelection(row); } this.rowClick.emit({ row, index }); } toggleRowSelection(row: any): void { if (this.selectedRows.has(row)) { this.selectedRows.delete(row); } else { this.selectedRows.add(row); } this.selectionChange.emit(Array.from(this.selectedRows)); } getCellValue(row: any, key: string): any { return key.split('.').reduce((obj, prop) => obj?.[prop], row) ?? ''; } getCellTemplate(columnKey: string): TemplateRef | null { return this.cellTemplates[columnKey] || null; } getSortAttribute(columnKey: string): string | null { if (this.sortColumn === columnKey) { return this.sortDirection === 'asc' ? 'ascending' : 'descending'; } return null; } }