@avoraui/av-data-table 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,666 @@
1
+ import { Component, EventEmitter, forwardRef, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
2
+ import { MatCard, MatCardContent } from "@angular/material/card";
3
+ import { MatIcon } from "@angular/material/icon";
4
+ import { MatIconButton } from "@angular/material/button";
5
+ import { CommonModule } from "@angular/common";
6
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
7
+ import { MatPaginator, PageEvent } from "@angular/material/paginator";
8
+ import { MatMenuModule } from "@angular/material/menu";
9
+ import { MatFormFieldModule } from "@angular/material/form-field";
10
+ import { MatInputModule } from "@angular/material/input";
11
+ import { MatSelectModule } from "@angular/material/select";
12
+ import { MatButtonModule } from "@angular/material/button";
13
+ import { FormsModule } from "@angular/forms";
14
+ import { Headers, Columns } from './table-prop';
15
+
16
+ /**
17
+ * AvDataTable is a standalone Angular component that provides a feature-rich, customizable
18
+ * data table interface with advanced pagination, dynamic column rendering, and interactive row actions.
19
+ * It offers a flexible and powerful solution for displaying and managing tabular data with support for
20
+ * nested object properties, custom styling, and comprehensive CRUD operations.
21
+ *
22
+ * The component implements ControlValueAccessor to integrate seamlessly with Angular reactive forms,
23
+ * supporting bidirectional data binding and form validation. It features server-side and client-side
24
+ * pagination, dynamic grid layout calculation, configurable action columns, and event-driven architecture
25
+ * for parent component interaction. The table supports nested property access with dot notation, custom
26
+ * column alignment, color customization, and intelligent data synchronization.
27
+ *
28
+ * @template T - The type of items displayed in the table rows.
29
+ *
30
+ * <p><strong>Features</strong></p>
31
+ * <ul>
32
+ * <li>Advanced pagination with configurable page sizes and options</li>
33
+ * <li>Dynamic grid template generation based on column count</li>
34
+ * <li>Nested object property access using dot notation (e.g., 'user.address.city')</li>
35
+ * <li>Array value handling with automatic formatting and joining</li>
36
+ * <li>Configurable action column with modify and delete operations</li>
37
+ * <li>Individual control over action button visibility and disabled states</li>
38
+ * <li>Custom column alignment (left, center, right) for headers and data</li>
39
+ * <li>Dynamic color customization for table cells</li>
40
+ * <li>Event emitters for modify, add, and remove operations</li>
41
+ * <li>Automatic page adjustment when data changes or items are removed</li>
42
+ * <li>Two-way data synchronization between internal and external data sources</li>
43
+ * <li>Angular reactive forms integration via ControlValueAccessor</li>
44
+ * <li>Material Design UI components integration</li>
45
+ * <li>Change detection optimization with explicit data reference updates</li>
46
+ * <li>Graceful handling of null/undefined values with fallback display</li>
47
+ * </ul>
48
+ *
49
+ * <p><strong>Technical Implementation</strong></p>
50
+ * <ul>
51
+ * <li>Dynamic CSS Grid layout calculation for responsive column sizing</li>
52
+ * <li>Paginated data slicing with index transformation for accurate positioning</li>
53
+ * <li>Nested property resolution with array mapping and filtering</li>
54
+ * <li>Data synchronization using efficient object reference and length comparison</li>
55
+ * <li>Event-driven communication pattern with parent components</li>
56
+ * <li>Lifecycle management with OnInit and OnChanges implementations</li>
57
+ * <li>Conditional action execution based on disabled state flags</li>
58
+ * <li>Automatic pagination state management and validation</li>
59
+ * </ul>
60
+ *
61
+ * <p><strong>Event Handling</strong></p>
62
+ * <ul>
63
+ * <li><b>onModify:</b> Emits when a row modification is requested, includes index, item data, and disabled state</li>
64
+ * <li><b>onNewItemAdded:</b> Emits when a new item is added to the table</li>
65
+ * <li><b>onItemRemoved:</b> Emits when an item is removed, includes index, data size, removed item, and disabled state</li>
66
+ * </ul>
67
+ *
68
+ * <p><strong>Configuration Options</strong></p>
69
+ * <ul>
70
+ * <li>PageSize: Number of rows per page (default: 5)</li>
71
+ * <li>PageSizeOptions: Array of available page size options</li>
72
+ * <li>TableHeaders: Column header definitions with labels and alignment</li>
73
+ * <li>TableColumns: Column data definitions with field names, alignment, and colors</li>
74
+ * <li>EnableActionColumn: Toggle visibility of action column</li>
75
+ * <li>EnableButtonDelete: Control delete button visibility</li>
76
+ * <li>EnableButtonModify: Control modify button visibility</li>
77
+ * <li>DisableRemove: Prevent item removal while maintaining event emission</li>
78
+ * <li>DisableModify: Prevent item modification while maintaining event emission</li>
79
+ * </ul>
80
+ *
81
+ * <p><strong>Authorship</strong></p>
82
+ * <ul>
83
+ * <li><b>Author:</b> Dileesha Ekanayake</li>
84
+ * <li><b>Email:</b> dileesha.r.ekanayake@gmail.com</li>
85
+ * <li><b>Created:</b> 2024</li>
86
+ * <li><b>Version:</b> 1.0.0</li>
87
+ * <li><b>Responsibility:</b> Design, implementation, and documentation of the data table component
88
+ * with Angular Material integration. Provides comprehensive table functionality including
89
+ * pagination, nested property access, dynamic styling, CRUD operations, and seamless form
90
+ * control integration with reactive forms support. Ensures robust data synchronization,
91
+ * event-driven architecture, and flexible configuration options for diverse use cases.</li>
92
+ * </ul>
93
+ */
94
+ @Component({
95
+ selector: 'av-data-table',
96
+ providers: [
97
+ {
98
+ provide: NG_VALUE_ACCESSOR,
99
+ useExisting: forwardRef(() => AvDataTable),
100
+ multi: true
101
+ }
102
+ ],
103
+ imports: [
104
+ MatCard,
105
+ MatCardContent,
106
+ MatIcon,
107
+ MatIconButton,
108
+ CommonModule,
109
+ MatPaginator,
110
+ MatMenuModule,
111
+ MatFormFieldModule,
112
+ MatInputModule,
113
+ MatSelectModule,
114
+ MatButtonModule,
115
+ FormsModule
116
+ ],
117
+ templateUrl: './av-data-table.html',
118
+ styleUrl: './av-data-table.css',
119
+ standalone: true
120
+ })
121
+ export class AvDataTable implements OnInit, OnChanges, ControlValueAccessor {
122
+
123
+ @Input() PageSize: number = 5;
124
+ @Input() PageSizeOptions: Array<number> = [];
125
+ @Input() currentPage: number = 0;
126
+ @Input() TableHeaders: Headers[] = [];
127
+ @Input() TableColumns: Columns[] = [];
128
+ protected TableData: any[] = [];
129
+ @Input() Data: any[] = [];
130
+ @Input() EnableActionColumn: boolean = false;
131
+ @Input() EnableButtonDelete: boolean = false;
132
+ @Input() EnableButtonModify: boolean = false;
133
+ @Output() onModify = new EventEmitter<{ index: number; modifiedItem: any, disabled: boolean }>();
134
+ @Output() onNewItemAdded = new EventEmitter<{ index: number, dataSize: number, removedItem: any }>();
135
+ @Output() onItemRemoved = new EventEmitter<{ index: number, dataSize: number, removedItem: any, disabled: boolean }>();
136
+ @Input() DisableRemove: boolean = false;
137
+ @Input() DisableModify: boolean = false;
138
+ gridTemplateColumns: string = '';
139
+
140
+ // New features state
141
+ sortField: string = '';
142
+ sortDirection: 'asc' | 'desc' | '' = '';
143
+ globalSearchTerm: string = '';
144
+ columnFilters: Map<string, { matchMode: 'all' | 'any', rules: { operator: string, value: string }[] }> = new Map();
145
+ processedData: any[] = [];
146
+
147
+ private onChange: (value: any) => void = () => { };
148
+ private onTouched: () => void = () => { };
149
+
150
+ constructor(
151
+ ) {
152
+ }
153
+
154
+ /**
155
+ * Lifecycle hook that is called after Angular has initialized all data-bound properties of a directive.
156
+ * This method is used to perform initialization logic for the component.
157
+ *
158
+ * It executes the following:
159
+ * - Initializes the paginator for managing paginated data display.
160
+ * - Calculates the grid template for layout adjustments.
161
+ * - Synchronizes data sources to ensure the component has up-to-date data.
162
+ *
163
+ * @return {void} This method does not return any value.
164
+ */
165
+ ngOnInit(): void {
166
+ this.initializePaginator();
167
+ this.calculateGridTemplate();
168
+ this.syncDataSources();
169
+ this.applyAllFilters();
170
+ }
171
+
172
+ /**
173
+ * Initializes the paginator configuration. Sets default values for PageSize, PageSizeOptions,
174
+ * and currentPage if they are undefined, null, or invalid. Ensures the PageSize is included
175
+ * in the PageSizeOptions array and sorts the options in ascending order.
176
+ *
177
+ * @return {void} Does not return a value.
178
+ */
179
+ initializePaginator(): void {
180
+ if (this.PageSize === undefined || this.PageSize === null || this.PageSize <= 0) {
181
+ this.PageSize = 5;
182
+ }
183
+ if (this.PageSizeOptions === undefined || this.PageSizeOptions === null || this.PageSizeOptions.length === 0) {
184
+ this.PageSizeOptions = [5, 10, 20, 50];
185
+ }
186
+ if (this.currentPage === undefined || this.currentPage === null || this.currentPage < 0) {
187
+ this.currentPage = 0;
188
+ }
189
+
190
+ // Ensure PageSize is included in PageSizeOptions if not already present
191
+ if (!this.PageSizeOptions.includes(this.PageSize)) {
192
+ this.PageSizeOptions = [...this.PageSizeOptions, this.PageSize].sort((a, b) => a - b);
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Handles changes to the component's input properties during the lifecycle of the component.
198
+ *
199
+ * This method is triggered whenever an input property bound to the component changes. It processes
200
+ * updates to properties such as `Data`, `PageSize`, `PageSizeOptions`, and `currentPage`, and
201
+ * ensures the internal state is kept in sync with the new inputs. Additionally, it updates the paginator
202
+ * and adjusts the current page as necessary.
203
+ *
204
+ * @param {SimpleChanges} changes - A collection of SimpleChange objects representing the changed properties.
205
+ * Each `SimpleChange` object provides information like the current and previous values as well as a flag
206
+ * indicating if it is the first change to this input.
207
+ * @return {void} - This method does not return any value.
208
+ */
209
+ ngOnChanges(changes: SimpleChanges): void {
210
+ // Listen for changes in the external Data input
211
+ if (changes['Data'] && !changes['Data'].firstChange) {
212
+ this.syncDataSources();
213
+ this.applyAllFilters();
214
+ // Reset to first page when data changes
215
+ this.currentPage = 0;
216
+ }
217
+
218
+ // Handle changes in pagination settings
219
+ if (changes['PageSize'] || changes['PageSizeOptions'] || changes['currentPage']) {
220
+ this.initializePaginator();
221
+
222
+ // Validate current page doesn't exceed available pages
223
+ const totalPages = Math.ceil(this.processedData.length / this.PageSize);
224
+ if (this.currentPage >= totalPages && totalPages > 0) {
225
+ this.currentPage = totalPages - 1;
226
+ }
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Retrieves a subset of data from the full dataset based on the current page and page size.
232
+ *
233
+ * @return {Array} A portion of the TableData array corresponding to the current page.
234
+ */
235
+ getPaginatedData(): Array<any> {
236
+ const startIndex = this.currentPage * this.PageSize;
237
+ const endIndex = startIndex + this.PageSize;
238
+ return this.processedData.slice(startIndex, endIndex);
239
+ }
240
+
241
+ /**
242
+ * Handles changes in the pagination state, such as changing the current page or the page size.
243
+ *
244
+ * @param {PageEvent} event - The event triggered by a pagination action that contains the updated page index and page size.
245
+ * @return {void} This method does not return anything.
246
+ */
247
+ pageChange(event: PageEvent): void {
248
+ this.currentPage = event.pageIndex;
249
+ this.PageSize = event.pageSize;
250
+ }
251
+
252
+ /**
253
+ * Calculates the actual index in the dataset based on the paginated index.
254
+ *
255
+ * @param {number} paginatedIndex - The index within the current page of paginated data.
256
+ * @return {number} The actual index in the entire dataset.
257
+ */
258
+ getActualIndex(paginatedIndex: number): number {
259
+ const item = this.getPaginatedData()[paginatedIndex];
260
+ return item ? this.TableData.indexOf(item) : -1;
261
+ }
262
+
263
+ /**
264
+ * Synchronizes external data source with the internal TableData.
265
+ * If the external Data differs from the current TableData, the TableData
266
+ * is updated with the values from Data and a change notification is triggered.
267
+ *
268
+ * @return {void} This method does not return a value.
269
+ */
270
+ syncDataSources(): void {
271
+ if (this.Data && (this.TableData.length !== this.Data.length || this.TableData !== this.Data)) {
272
+ this.TableData = [...this.Data];
273
+ this.applyAllFilters();
274
+ this.onChange(this.TableData);
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Updates the form value by notifying changes and updating external data if available.
280
+ * This method triggers the onChange callback with the current table data
281
+ * and notifies that the form field has been touched. Additionally, it updates
282
+ * the external data reference to ensure proper change detection in the parent components.
283
+ *
284
+ * @return {void} No return value.
285
+ */
286
+ updateFormValue(): void {
287
+ // Notify the form about the change
288
+ if (this.onChange) {
289
+ this.onChange(this.TableData);
290
+ this.onTouched();
291
+ }
292
+
293
+ // Also update the external Data if it exists
294
+ if (this.Data) {
295
+ // Create a new reference to trigger OnChanges in parent components
296
+ this.Data = [...this.TableData];
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Calculates and sets the grid template for a layout based on the number of displayed headers
302
+ * and the presence of an action column. The grid template determines the column structure with
303
+ * equal fractions for data columns and a fixed width for the action column if enabled.
304
+ *
305
+ * @return {void} Does not return a value. Sets the `gridTemplateColumns` property to the calculated template.
306
+ */
307
+ calculateGridTemplate(): void {
308
+ // Calculate grid template based on columns count
309
+ const displayedHeaders = this.getDisplayedHeaders();
310
+ const columnsCount = displayedHeaders.length;
311
+
312
+ if (columnsCount === 0) return;
313
+
314
+ // Create grid template with equal fractions for data columns
315
+ // and fixed width for action column if enabled
316
+ if (this.EnableActionColumn) {
317
+ const dataColumns = columnsCount - 1; // Subtract action column
318
+ this.gridTemplateColumns = dataColumns > 0
319
+ ? `repeat(${dataColumns}, 1fr) 140px`
320
+ : '140px';
321
+ } else {
322
+ this.gridTemplateColumns = `repeat(${columnsCount}, 1fr)`;
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Retrieves the headers that should be displayed in the table.
328
+ * The method includes or excludes the "action" column based on the value of `EnableActionColumn`.
329
+ *
330
+ * @return {TableProp[]} An array of headers to be displayed in the table. If `EnableActionColumn` is false, the "action" column is excluded.
331
+ */
332
+ getDisplayedHeaders(): Headers[] {
333
+ return this.EnableActionColumn
334
+ ? this.TableHeaders
335
+ : this.TableHeaders.filter(h => h.label.toLowerCase() !== 'action');
336
+ }
337
+
338
+ /**
339
+ * Generates and returns an object representing CSS classes for a header based on its alignment property.
340
+ *
341
+ * @param {TableProp} header - An object containing header properties, including an alignment property ('center', 'right', or 'left').
342
+ * @return {Object} An object mapping class names to a boolean indicating their applicability based on the alignment of the header.
343
+ */
344
+ getHeaderFieldClasses(header: Headers): object {
345
+ return {
346
+ 'header-text-center': header.align === 'center',
347
+ 'header-text-right': header.align === 'right',
348
+ 'header-text-left': header.align === 'left',
349
+ };
350
+ }
351
+
352
+ /**
353
+ * Generates a mapping of CSS classes for a data field based on the alignment property of the given column.
354
+ *
355
+ * @param {Columns} column - The column object containing alignment information.
356
+ * @return {Object} An object where the keys are class names, and the values are booleans indicating whether each class applies.
357
+ */
358
+ getDataFieldClasses(column: Columns): object {
359
+ return {
360
+ 'text-center': column.align === 'center',
361
+ 'text-right': column.align === 'right',
362
+ 'text-left': column.align === 'left',
363
+ };
364
+ }
365
+
366
+ /**
367
+ * Retrieves the color property from the given column object.
368
+ *
369
+ * @param {Columns} column - The column object containing the color property.
370
+ * @return {string} The color associated with the column.
371
+ */
372
+ getDataFiledColor(column: Columns): any {
373
+ return column.color;
374
+ }
375
+
376
+ /**
377
+ * Retrieves the value of a specified column from the given item.
378
+ *
379
+ * @param {any} item - The object containing the data to extract the value from.
380
+ * @param {Columns} column - The column definition, including the field name or path.
381
+ * @return {string} The value of the specified column for the given item. If the field is not found, an empty string is returned.
382
+ */
383
+ getValueForColumn(item: any, column: Columns): string {
384
+ if (!item || !column || !column.field) return '';
385
+
386
+ // Check if the field has dot notation (e.g., 'gender.name')
387
+ if (column.field.includes('.')) {
388
+ return this.getNestedValue(item, column.field);
389
+ } else {
390
+ // Handle standard fields
391
+ return column.field in item ? item[column.field] : '';
392
+ }
393
+ }
394
+
395
+ /**
396
+ * Retrieves a nested value from a given object based on a dot-separated path string.
397
+ * If the path does not exist or the value is undefined/null, it returns a placeholder '-'.
398
+ * If accessing an array, it maps the values at the given key and processes them.
399
+ *
400
+ * @param {any} object - The object from which the nested value is extracted.
401
+ * @param {string} path - The dot-separated string representing the path to the desired value.
402
+ * @return {any} The value found at the given path, an array joined into a string, or '-' if not found.
403
+ */
404
+ getNestedValue(object: any, path: string): any {
405
+ // Return early if object is null or undefined
406
+ if (!object) return '-';
407
+
408
+ // Split the path into individual keys (e.g., "gender.name" → ["gender", "name"])
409
+ const keys = path.split('.');
410
+ let value = object;
411
+
412
+ // Navigate through the object hierarchy
413
+ for (const key of keys) {
414
+ if (value === null || value === undefined) {
415
+ return '-';
416
+ }
417
+
418
+ if (Array.isArray(value)) {
419
+ // If we encounter an array, map over its items to extract the property
420
+ value = value.map(item => item[key]).filter(Boolean);
421
+ } else {
422
+ // Access the next property level
423
+ value = value[key];
424
+ }
425
+ }
426
+
427
+ // Format the final result
428
+ if (Array.isArray(value)) {
429
+ return value.length > 0 ? value.join(', ') : '-';
430
+ }
431
+
432
+ return value !== null && value !== undefined ? value : '-';
433
+ }
434
+
435
+ /**
436
+ * Removes an item from the data table at the specified index.
437
+ *
438
+ * @param {number} actualIndex - The index of the item to remove from the data table.
439
+ * @return {void} This method does not return a value.
440
+ */
441
+ removeItem(actualIndex: number): void {
442
+ // Check disable condition FIRST - before touching any data
443
+ if (this.DisableRemove) {
444
+ // Don't remove from TableData, just emit event for parent to handle
445
+ this.onItemRemoved.emit({
446
+ index: actualIndex,
447
+ dataSize: this.TableData.length,
448
+ removedItem: this.TableData[actualIndex], // Get the item that would be removed
449
+ disabled: true
450
+ });
451
+ return; // Exit early - don't modify TableData
452
+ } else {
453
+ // Only proceed with actual removal if not disabled
454
+ const newData = [...this.TableData];
455
+ const removedData = newData[actualIndex]; // Get the item being removed
456
+ newData.splice(actualIndex, 1);
457
+ this.TableData = newData;
458
+
459
+ // Rest of your existing logic...
460
+ const totalPages = Math.ceil(this.TableData.length / this.PageSize);
461
+ if (this.currentPage >= totalPages && this.currentPage > 0) {
462
+ this.currentPage = totalPages - 1;
463
+ }
464
+
465
+ if (this.TableData.length === 0) {
466
+ this.onChange([removedData]);
467
+ } else {
468
+ this.updateFormValue();
469
+ }
470
+
471
+ this.applyAllFilters();
472
+
473
+ this.onItemRemoved.emit({
474
+ index: actualIndex,
475
+ dataSize: this.TableData.length,
476
+ removedItem: removedData,
477
+ disabled: false
478
+ });
479
+ }
480
+ }
481
+
482
+ /**
483
+ * Modifies an item at the specified index in the dataset. If the modification is disabled, emits an event without making any changes.
484
+ *
485
+ * @param {number} actualIndex - The index of the item to be modified in the dataset.
486
+ * @return {void} This method does not return a value.
487
+ */
488
+ modifyItem(actualIndex: number): void {
489
+ // Check disable condition FIRST - before any modification logic
490
+ if (this.DisableModify) {
491
+ // Emit event for parent to handle the disabled state
492
+ this.onModify.emit({
493
+ index: actualIndex,
494
+ modifiedItem: this.TableData[actualIndex],
495
+ disabled: true
496
+ });
497
+ return; // Exit early - don't proceed with modification
498
+ }
499
+
500
+ // Only proceed with modification if not disabled
501
+ const modifiedItem = this.TableData[actualIndex];
502
+ this.onModify.emit({
503
+ index: actualIndex,
504
+ modifiedItem,
505
+ disabled: false
506
+ });
507
+ }
508
+
509
+ // ControlValueAccessor interface implementation
510
+ /**
511
+ * Registers a callback function to be called whenever the value changes.
512
+ *
513
+ * @param {any} fn - The function to be executed on a value change.
514
+ * @return {void} This method does not return a value.
515
+ */
516
+ registerOnChange(fn: any): void {
517
+ this.onChange = fn;
518
+ }
519
+
520
+ /**
521
+ * Registers a callback function that should be called when the control is touched.
522
+ *
523
+ * @param {any} fn - The callback function to execute when the control is touched.
524
+ * @return {void} This method does not return a value.
525
+ */
526
+ registerOnTouched(fn: any): void {
527
+ this.onTouched = fn;
528
+ }
529
+
530
+ /**
531
+ * Writes a new value to the table data and resets to the first page.
532
+ *
533
+ * @param {any[]} value - The array of data to be written to the table.
534
+ * @return {void} Does not return a value.
535
+ */
536
+ writeValue(value: any[]): void {
537
+ if (value) {
538
+ this.TableData = [...value];
539
+ this.applyAllFilters();
540
+ // Reset to first page when new data is written
541
+ this.currentPage = 0;
542
+ } else {
543
+ this.TableData = [];
544
+ this.processedData = [];
545
+ this.currentPage = 0;
546
+ }
547
+ }
548
+
549
+ // Sorting Logic
550
+ toggleSort(field: string): void {
551
+ if (this.sortField === field) {
552
+ if (this.sortDirection === 'asc') {
553
+ this.sortDirection = 'desc';
554
+ } else if (this.sortDirection === 'desc') {
555
+ this.sortDirection = '';
556
+ this.sortField = '';
557
+ } else {
558
+ this.sortDirection = 'asc';
559
+ }
560
+ } else {
561
+ this.sortField = field;
562
+ this.sortDirection = 'asc';
563
+ }
564
+ this.applyAllFilters();
565
+ }
566
+
567
+ // Filtering Logic
568
+ applyAllFilters(): void {
569
+ let data = [...this.TableData];
570
+
571
+ // 1. Global Search
572
+ if (this.globalSearchTerm) {
573
+ const term = this.globalSearchTerm.toLowerCase();
574
+ data = data.filter(item => {
575
+ return this.TableColumns.some(col => {
576
+ const val = this.getValueForColumn(item, col);
577
+ return val?.toString().toLowerCase().includes(term);
578
+ });
579
+ });
580
+ }
581
+
582
+ // 2. Column-wise Filters
583
+ this.columnFilters.forEach((filter, field) => {
584
+ if (filter.rules.length > 0) {
585
+ data = data.filter(item => {
586
+ const val = (this.getValueForColumn(item, { field, align: 'left' } as any) || '').toString().toLowerCase();
587
+ const ruleResults = filter.rules.map(rule => {
588
+ const ruleVal = rule.value.toLowerCase();
589
+ switch (rule.operator) {
590
+ case 'starts': return val.startsWith(ruleVal);
591
+ case 'contains': return val.includes(ruleVal);
592
+ case 'not_contains': return !val.includes(ruleVal);
593
+ case 'ends': return val.endsWith(ruleVal);
594
+ case 'equals': return val === ruleVal;
595
+ case 'not_equals': return val !== ruleVal;
596
+ default: return true;
597
+ }
598
+ });
599
+
600
+ return filter.matchMode === 'all'
601
+ ? ruleResults.every(r => r)
602
+ : ruleResults.some(r => r);
603
+ });
604
+ }
605
+ });
606
+
607
+ // 3. Sorting
608
+ if (this.sortField && this.sortDirection) {
609
+ data.sort((a, b) => {
610
+ const valA = this.getValueForColumn(a, { field: this.sortField, align: 'left' } as any);
611
+ const valB = this.getValueForColumn(b, { field: this.sortField, align: 'left' } as any);
612
+
613
+ if (valA < valB) return this.sortDirection === 'asc' ? -1 : 1;
614
+ if (valA > valB) return this.sortDirection === 'asc' ? 1 : -1;
615
+ return 0;
616
+ });
617
+ }
618
+
619
+ this.processedData = data;
620
+
621
+ // Adjust pagination if current page is out of bounds
622
+ const totalPages = Math.ceil(this.processedData.length / this.PageSize);
623
+ if (this.currentPage >= totalPages && totalPages > 0) {
624
+ this.currentPage = totalPages - 1;
625
+ }
626
+ }
627
+
628
+ clearAllFilters(): void {
629
+ this.globalSearchTerm = '';
630
+ this.columnFilters.clear();
631
+ this.sortField = '';
632
+ this.sortDirection = '';
633
+ this.applyAllFilters();
634
+ }
635
+
636
+ updateGlobalSearch(event: any): void {
637
+ this.globalSearchTerm = event.target.value;
638
+ this.applyAllFilters();
639
+ }
640
+
641
+ // Column filter management
642
+ setColumnFilter(field: string, filter: any): void {
643
+ if (!filter || filter.rules.length === 0) {
644
+ this.columnFilters.delete(field);
645
+ } else {
646
+ this.columnFilters.set(field, filter);
647
+ }
648
+ this.applyAllFilters();
649
+ }
650
+
651
+ getColumnFilter(field: string): { matchMode: 'all' | 'any', rules: { operator: string, value: string }[] } {
652
+ let filter = this.columnFilters.get(field);
653
+ if (!filter) {
654
+ filter = { matchMode: 'all', rules: [{ operator: 'starts', value: '' }] };
655
+ this.columnFilters.set(field, filter);
656
+ }
657
+ return filter;
658
+ }
659
+
660
+ // Check if a column has an active filter with actual values (stable during change detection)
661
+ hasActiveFilter(field: string): boolean {
662
+ const filter = this.columnFilters.get(field);
663
+ if (!filter) return false;
664
+ return filter.rules.some(rule => rule.value !== '');
665
+ }
666
+ }
@@ -0,0 +1,10 @@
1
+ export interface Headers {
2
+ label: string;
3
+ align: 'left' | 'center' | 'right';
4
+ }
5
+
6
+ export interface Columns {
7
+ field: string;
8
+ align: 'left' | 'center' | 'right';
9
+ color?: string;
10
+ }
@@ -0,0 +1,6 @@
1
+ /*
2
+ * Public API Surface of data-table
3
+ */
4
+
5
+ export * from './lib/av-data-table';
6
+ export * from './lib/table-prop';