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>
This commit is contained in:
246
src/lib/components/data-display/table/table.component.ts
Normal file
246
src/lib/components/data-display/table/table.component.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user