This repository has been archived on 2026-06-18. You can view files and clone it, but cannot push or open issues or pull requests.
Files
ui-essentials/src/lib/components/data-display/table/table.component.ts
Jules 0a0cade343 Initial commit - Angular library: ui-essentials
🎯 Implementation Complete!

This library has been extracted from the monorepo and is ready for Git submodule distribution.

Features:
- Standardized SCSS imports (no relative paths)
- Optimized public-api.ts exports
- Independent Angular library structure
- Ready for consumer integration as submodule

🚀 Generated with Claude Code (https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 21:12:46 +10:00

246 lines
8.0 KiB
TypeScript

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: `
<div [class]="tableContainerClasses">
<table [class]="tableClasses">
<!-- Table Header -->
@if (showHeader && columns.length > 0) {
<thead class="ui-table__header">
<tr class="ui-table__header-row">
@for (column of columns; track column.key) {
<th
[class]="getHeaderCellClasses(column)"
[style.width]="column.width"
[attr.aria-sort]="getSortAttribute(column.key)"
(click)="handleSort(column)"
>
<div class="ui-table__header-content">
<span class="ui-table__header-text">{{ column.label }}</span>
@if (column.sortable && sortColumn === column.key) {
<span class="ui-table__sort-indicator" [attr.data-direction]="sortDirection">
@if (sortDirection === 'asc') {
} @else if (sortDirection === 'desc') {
}
</span>
}
</div>
</th>
}
</tr>
</thead>
}
<!-- Table Body -->
<tbody class="ui-table__body">
@if (data.length === 0 && showEmptyState) {
<tr class="ui-table__empty-row">
<td [attr.colspan]="columns.length" class="ui-table__empty-cell">
<div class="ui-table__empty-content">
@if (emptyTemplate) {
<ng-container [ngTemplateOutlet]="emptyTemplate"></ng-container>
} @else {
<div class="ui-table__empty-text">{{ emptyMessage }}</div>
}
</div>
</td>
</tr>
} @else {
@for (row of data; track trackByFn ? trackByFn($index, row) : $index; let rowIndex = $index) {
<tr
[class]="getRowClasses(row, rowIndex)"
(click)="handleRowClick(row, rowIndex)"
[attr.aria-rowindex]="rowIndex + 1"
>
@for (column of columns; track column.key) {
<td [class]="getBodyCellClasses(column, row)">
@if (getCellTemplate(column.key)) {
<ng-container
[ngTemplateOutlet]="getCellTemplate(column.key)!"
[ngTemplateOutletContext]="{
$implicit: getCellValue(row, column.key),
row: row,
column: column,
index: rowIndex
}"
></ng-container>
} @else {
<span class="ui-table__cell-content">
{{ getCellValue(row, column.key) }}
</span>
}
</td>
}
</tr>
}
}
</tbody>
</table>
<!-- Loading Overlay -->
@if (loading) {
<div class="ui-table__loading-overlay">
<div class="ui-table__loading-spinner"></div>
<div class="ui-table__loading-text">{{ loadingMessage }}</div>
</div>
}
</div>
`,
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<string, TemplateRef<any>> = {};
@Input() class: string = '';
@Output() rowClick = new EventEmitter<{row: any, index: number}>();
@Output() sort = new EventEmitter<TableSortEvent>();
@Output() selectionChange = new EventEmitter<any[]>();
@ContentChild('emptyTemplate') emptyTemplate?: TemplateRef<any>;
selectedRows: Set<any> = 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<any> | null {
return this.cellTemplates[columnKey] || null;
}
getSortAttribute(columnKey: string): string | null {
if (this.sortColumn === columnKey) {
return this.sortDirection === 'asc' ? 'ascending' : 'descending';
}
return null;
}
}