• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

atinc / ngx-tethys / #102

26 May 2026 08:11AM UTC coverage: 91.111% (+0.7%) from 90.407%
#102

push

web-flow
build: bump docgeni to 2.8.0-next.5 (#3809)

4571 of 5491 branches covered (83.25%)

Branch coverage included in aggregate %.

13141 of 13949 relevant lines covered (94.21%)

966.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.8
/src/table/table.component.ts
1
import { InputCssPixel, UpdateHostClassService } from 'ngx-tethys/core';
2
import { Dictionary, SafeAny } from 'ngx-tethys/types';
3
import { coerceBooleanProperty, get, helpers, isString, keyBy, set } from 'ngx-tethys/util';
4
import { EMPTY, fromEvent, merge, Observable, of } from 'rxjs';
5
import { delay, startWith, switchMap } from 'rxjs/operators';
6
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
7

8
import { CdkDrag, CdkDragDrop, CdkDragEnd, CdkDragStart, CdkDropList, moveItemInArray } from '@angular/cdk/drag-drop';
9
import { ViewportRuler } from '@angular/cdk/overlay';
10
import { normalizePassiveListenerOptions } from '@angular/cdk/platform';
11
import { isPlatformServer, NgClass, NgTemplateOutlet, NgStyle } from '@angular/common';
12
import {
13
    AfterViewInit,
14
    ChangeDetectorRef,
15
    Component,
16
    ContentChild,
17
    ContentChildren,
18
    DestroyRef,
19
    ElementRef,
20
    EventEmitter,
21
    HostBinding,
22
    inject,
23
    Input,
24
    IterableChangeRecord,
25
    IterableChanges,
26
    IterableDiffer,
27
    IterableDiffers,
28
    NgZone,
29
    numberAttribute,
30
    OnChanges,
31
    OnDestroy,
32
    OnInit,
33
    Output,
34
    PLATFORM_ID,
35
    QueryList,
36
    Renderer2,
37
    SimpleChanges,
38
    TemplateRef,
39
    ViewChild,
40
    ViewChildren,
41
    ViewEncapsulation,
42
    DOCUMENT
43
} from '@angular/core';
44

45
import { IThyTableColumnParentComponent, THY_TABLE_COLUMN_PARENT_COMPONENT, ThyTableColumnComponent } from './table-column.component';
46
import {
47
    PageChangedEvent,
48
    ThyMultiSelectEvent,
49
    ThyPage,
50
    ThyRadioSelectEvent,
51
    ThySwitchEvent,
52
    ThyTableSkeletonColumn,
53
    ThyTableDraggableEvent,
54
    ThyTableEmptyOptions,
55
    ThyTableEvent,
56
    ThyTableRowEvent,
57
    ThyTableSortDirection,
58
    ThyTableSortEvent
59
} from './table.interface';
60
import { TableRowDragDisabledPipe } from './pipes/drag.pipe';
61
import { TableIsValidModelValuePipe } from './pipes/table.pipe';
62
import { ThyPagination } from 'ngx-tethys/pagination';
63
import { ThyTableSkeleton } from './table-skeleton.component';
64
import { ThyEmpty } from 'ngx-tethys/empty';
65
import { ThySwitch } from 'ngx-tethys/switch';
66
import { FormsModule } from '@angular/forms';
67
import { ThyDragDropDirective, ThyContextMenuDirective } from 'ngx-tethys/shared';
68
import { ThyIcon } from 'ngx-tethys/icon';
69
import { CdkScrollable } from '@angular/cdk/scrolling';
70
import { ThyTableColumnSkeletonType } from './enums';
71
import { ThyTableTheme, ThyTableMode, ThyTableSize } from './table.type';
72

1✔
73
export enum ThyFixedDirection {
1✔
74
    left = 'left',
1✔
75
    right = 'right'
76
}
77

78
interface ThyTableGroup<T = unknown> {
79
    id?: string;
80
    expand?: boolean;
81
    children?: object[];
82
    origin?: T;
83
}
84

1✔
85
const tableThemeMap = {
86
    default: 'table-default',
87
    bordered: 'table-bordered',
88
    boxed: 'table-boxed'
89
};
90

1✔
91
const customType = {
92
    index: 'index',
93
    checkbox: 'checkbox',
94
    radio: 'radio',
95
    switch: 'switch'
96
};
97

1✔
98
const css = {
99
    tableBody: 'thy-table-body',
100
    tableScrollLeft: 'thy-table-scroll-left',
101
    tableScrollRight: 'thy-table-scroll-right',
102
    tableScrollMiddle: 'thy-table-scroll-middle'
103
};
104

1✔
105
const passiveEventListenerOptions = normalizePassiveListenerOptions({ passive: true });
106

107
/**
108
 * 表格组件
109
 * @name thy-table
110
 * @order 10
111
 */
112
@Component({
113
    selector: 'thy-table',
114
    templateUrl: './table.component.html',
115
    providers: [
116
        {
117
            provide: THY_TABLE_COLUMN_PARENT_COMPONENT,
118
            useExisting: ThyTable
119
        },
120
        UpdateHostClassService
121
    ],
122
    encapsulation: ViewEncapsulation.None,
123
    host: {
124
        class: 'thy-table',
125
        '[class.thy-table-bordered]': `theme === 'bordered'`,
126
        '[class.thy-table-boxed]': `theme === 'boxed'`,
127
        '[class.thy-table-fixed-header]': 'thyHeaderFixed'
128
    },
129
    imports: [
130
        CdkScrollable,
131
        NgClass,
132
        NgTemplateOutlet,
133
        ThyIcon,
134
        ThyDragDropDirective,
135
        CdkDropList,
136
        CdkDrag,
137
        ThyContextMenuDirective,
138
        NgStyle,
139
        FormsModule,
140
        ThySwitch,
141
        ThyEmpty,
142
        ThyTableSkeleton,
143
        ThyPagination,
144
        TableIsValidModelValuePipe,
145
        TableRowDragDisabledPipe
146
    ]
147
})
1✔
148
export class ThyTable implements OnInit, OnChanges, AfterViewInit, OnDestroy, IThyTableColumnParentComponent {
70✔
149
    elementRef = inject(ElementRef);
70✔
150
    private _differs = inject(IterableDiffers);
70✔
151
    private viewportRuler = inject(ViewportRuler);
70✔
152
    private updateHostClassService = inject(UpdateHostClassService);
70✔
153
    private document = inject(DOCUMENT);
70✔
154
    private platformId = inject(PLATFORM_ID);
70✔
155
    private ngZone = inject(NgZone);
70✔
156
    private renderer = inject(Renderer2);
70✔
157
    private cdr = inject(ChangeDetectorRef);
158

70✔
159
    private readonly destroyRef = inject(DestroyRef);
160

70✔
161
    public customType = customType;
162

70✔
163
    public model: object[] = [];
164

70✔
165
    public groups: ThyTableGroup[] = [];
166

70✔
167
    public rowKey = '_id';
168

169
    public groupBy!: string;
170

70✔
171
    public mode: ThyTableMode = 'list';
172

70✔
173
    public theme: ThyTableTheme = 'default';
174

70✔
175
    public className = '';
176

70✔
177
    public size: ThyTableSize = 'md';
178

179
    public rowClassName!: string | Function;
180

70✔
181
    public loadingDone = true;
182

183
    public loadingText!: string;
184

70✔
185
    public emptyOptions: ThyTableEmptyOptions = {};
186

70✔
187
    public draggable = false;
188

70✔
189
    public selectedRadioRow: SafeAny = null;
190

70✔
191
    public pagination: ThyPage = { index: 1, size: 20, total: 0, sizeOptions: [20, 50, 100] };
192

193
    public trackByFn: SafeAny;
194

70✔
195
    public wholeRowSelect = false;
196

70✔
197
    public fixedDirection = ThyFixedDirection;
198

70✔
199
    public hasFixed = false;
200

70✔
201
    public columns: ThyTableColumnComponent[] = [];
202

203
    private _diff!: IterableDiffer<SafeAny>;
204

70✔
205
    private initialized = false;
206

70✔
207
    private _oldThyClassName = '';
208

70✔
209
    private scrollClassName = css.tableScrollLeft;
210

211
    private get tableScrollElement(): HTMLElement {
148✔
212
        return this.elementRef.nativeElement.getElementsByClassName(css.tableBody)[0] as HTMLElement;
213
    }
214

215
    private get scroll$() {
66!
216
        return merge(this.tableScrollElement ? fromEvent<MouseEvent>(this.tableScrollElement, 'scroll') : EMPTY);
217
    }
218

219
    /**
220
     * 设置数据为空时展示的模板
221
     * @type TemplateRef
222
     */
223
    @ContentChild('empty') emptyTemplate!: TemplateRef<SafeAny>;
224

225
    @ViewChild('table', { static: true }) tableElementRef!: ElementRef<SafeAny>;
226

227
    @ViewChildren('rows', { read: ElementRef }) rows!: QueryList<ElementRef<HTMLElement>>;
228

229
    /**
230
     * 表格展示方式,列表/分组/树
231
     * @type list | group | tree
232
     * @default list
233
     */
234
    @Input()
235
    set thyMode(value: ThyTableMode) {
60!
236
        this.mode = value || this.mode;
237
    }
238

239
    /**
240
     * thyMode的值为 `group` 时分组的 Key
241
     */
242
    @Input()
243
    set thyGroupBy(value: string) {
57✔
244
        this.groupBy = value;
245
    }
246

247
    /**
248
     * 设置每行数据的唯一标识属性名
249
     * @default _id
250
     */
251
    @Input()
252
    set thyRowKey(value: SafeAny) {
70!
253
        this.rowKey = value || this.rowKey;
254
    }
255

256
    /**
257
     * 分组数据源
258
     */
259
    @Input()
260
    set thyGroups(value: SafeAny) {
56✔
261
        if (this.mode === 'group') {
3✔
262
            this.buildGroups(value);
263
        }
264
    }
265

266
    /**
267
     * 数据源
268
     */
269
    @Input()
270
    set thyModel(value: SafeAny) {
67✔
271
        this.model = value || [];
67✔
272
        this._diff = this._differs.find(this.model).create();
67✔
273
        this._initializeDataModel();
274

67✔
275
        if (this.mode === 'group') {
1✔
276
            this.buildModel();
277
        }
278
    }
279

280
    /**
281
     * 表格的显示风格,`bordered` 时头部有背景色且分割线区别明显
282
     * @type default | bordered | boxed
283
     * @default default
284
     */
285
    @Input()
286
    set thyTheme(value: ThyTableTheme) {
45!
287
        this.theme = value || this.theme;
45✔
288
        this._setClass();
289
    }
290

291
    /**
292
     * 表格的大小
293
     * @type xs | sm | md | lg | xlg | default
294
     * @default md
295
     */
296
    @Input()
297
    set thySize(value: ThyTableSize) {
50✔
298
        this.size = value || this.size;
50✔
299
        this._setClass();
300
    }
301

302
    /**
303
     * 设置表格最小宽度,一般是适用于设置列宽为百分之或auto时限制表格最小宽度'
304
     */
305
    @Input()
306
    @InputCssPixel()
307
    thyMinWidth!: string | number;
308

309
    /**
310
     * 设置为 fixed 布局表格,设置 fixed 后,列宽将严格按照设置宽度展示,列宽将不会根据表格内容自动调整
311
     * @default false
312
     */
313
    @Input({ transform: coerceBooleanProperty }) thyLayoutFixed!: boolean;
314

315
    /**
316
     * 是否表头固定,若设置为 true, 需要同步设置 thyHeight
317
     * @default false
318
     */
319
    @Input({ transform: coerceBooleanProperty }) thyHeaderFixed!: boolean;
320

321
    /**
322
     * 表格的高度
323
     */
324
    @HostBinding('style.height')
325
    @Input()
326
    @InputCssPixel()
327
    thyHeight!: string;
328

329
    /**
330
     * 设置表格的样式
331
     */
332
    @Input()
333
    set thyClassName(value: string) {
48✔
334
        const list = this.className.split(' ').filter(a => a.trim());
45✔
335
        const index: number = list.findIndex(item => item === this._oldThyClassName);
45!
336
        if (index !== -1) {
×
337
            list.splice(index, 1, value);
338
        } else {
45✔
339
            list.push(value);
340
        }
45✔
341
        this._oldThyClassName = value;
45✔
342
        this.className = list.join(' ');
343
    }
344

345
    /**
346
     * 设置表格行的样式,传入函数,支持 row、index
347
     * @type string | (row, index) => string
348
     */
349
    @Input()
350
    set thyRowClassName(value: string | Function) {
45✔
351
        this.rowClassName = value;
352
    }
353

354
    /**
355
     * 设置加载状态
356
     * @default true
357
     */
358
    @Input({ transform: coerceBooleanProperty })
359
    set thyLoadingDone(value: boolean) {
45✔
360
        this.loadingDone = value;
361
    }
362

363
    /**
364
     * 设置加载时显示的文本,已废弃
365
     * @deprecated
366
     */
367
    @Input()
368
    set thyLoadingText(value: string) {
45✔
369
        this.loadingText = value;
370
    }
371

372
    /**
373
     * 配置空状态组件
374
     */
375
    @Input()
376
    set thyEmptyOptions(value: ThyTableEmptyOptions) {
44✔
377
        this.emptyOptions = value;
378
    }
379

380
    /**
381
     * 是否开启行拖拽
382
     * @default false
383
     */
384
    @Input({ transform: coerceBooleanProperty })
385
    set thyDraggable(value: boolean) {
64✔
386
        this.draggable = value;
64✔
387
        if ((typeof ngDevMode === 'undefined' || ngDevMode) && this.draggable && this.mode === 'tree') {
1✔
388
            throw new Error('Tree mode sorting is not supported');
389
        }
390
    }
391

392
    /**
393
     * 设置当前页码
394
     * @default 1
395
     */
396
    @Input({ transform: numberAttribute })
397
    set thyPageIndex(value: number) {
55✔
398
        this.pagination.index = value;
399
    }
400

401
    /**
402
     * 设置每页显示数量
403
     * @default 20
404
     */
405
    @Input({ transform: numberAttribute })
406
    set thyPageSize(value: number) {
55✔
407
        this.pagination.size = value;
408
    }
409

410
    /**
411
     * 设置总页数
412
     */
413
    @Input({ transform: numberAttribute })
414
    set thyPageTotal(value: number) {
55✔
415
        this.pagination.total = value;
416
    }
417

418
    /**
419
     * 选中当前行是否自动选中 Checkbox,不开启时只有点击 Checkbox 列时才会触发选中
420
     * @default false
421
     */
422
    @Input({ transform: coerceBooleanProperty })
423
    set thyWholeRowSelect(value: boolean) {
45✔
424
        if (value) {
3✔
425
            this.className += ' table-hover';
426
        }
45✔
427
        this.wholeRowSelect = value;
428
    }
429

430
    /**
431
     * 是否显示表格头
432
     * @default false
433
     */
70✔
434
    @Input({ transform: coerceBooleanProperty }) thyHeadless = false;
435

436
    /**
437
     * 是否显示表格头,已废弃,请使用 thyHeadless
438
     * @deprecated please use thyHeadless
439
     */
440
    @Input({ transform: coerceBooleanProperty })
441
    set thyShowHeader(value: boolean) {
1✔
442
        this.thyHeadless = !value;
443
    }
444

445
    /**
446
     * 是否显示左侧 Total
447
     */
70✔
448
    @Input({ alias: 'thyShowTotal', transform: coerceBooleanProperty }) showTotal = false;
449

450
    /**
451
     * 是否显示调整每页显示条数下拉框
452
     */
70✔
453
    @Input({ alias: 'thyShowSizeChanger', transform: coerceBooleanProperty }) showSizeChanger = false;
454

455
    /**
456
     * 每页显示条数下拉框可选项
457
     * @type number[]
458
     */
459
    @Input('thyPageSizeOptions')
460
    set pageSizeOptions(value: number[]) {
44✔
461
        this.pagination.sizeOptions = value;
462
    }
463

464
    /**
465
     * thyMode 为 tree 时,设置 Tree 树状数据展示时的缩进
466
     */
70✔
467
    @Input({ transform: numberAttribute }) thyIndent = 20;
468

469
    /**
470
     * thyMode 为 tree 时,设置 Tree 树状数据对象中的子节点 Key
471
     * @type string
472
     */
70✔
473
    @Input() thyChildrenKey = 'children';
474

475
    /**
476
     * 开启 Hover 后显示操作,默认不显示操作区内容,鼠标 Hover 时展示
477
     * @default false
478
     */
479
    @HostBinding('class.thy-table-hover-display-operation')
480
    @Input({ transform: coerceBooleanProperty })
481
    thyHoverDisplayOperation!: boolean;
482

384✔
483
    @Input() thyDragDisabledPredicate: (item: SafeAny) => boolean = () => false;
484

485
    /**
486
     * 表格列的骨架类型
487
     * @type ThyTableColumnSkeletonType[]
488
     */
70✔
489
    @Input() thyColumnSkeletonTypes: ThyTableColumnSkeletonType[] = [
490
        ThyTableColumnSkeletonType.title,
491
        ThyTableColumnSkeletonType.member,
492
        ThyTableColumnSkeletonType.default
493
    ];
494

495
    /**
496
     * 切换组件回调事件
497
     */
70✔
498
    @Output() readonly thyOnSwitchChange: EventEmitter<ThySwitchEvent> = new EventEmitter<ThySwitchEvent>();
499

500
    /**
501
     * 表格分页回调事件
502
     */
70✔
503
    @Output() readonly thyOnPageChange: EventEmitter<PageChangedEvent> = new EventEmitter<PageChangedEvent>();
504

505
    /**
506
     * 表格分页当前页改变回调事件
507
     */
70✔
508
    @Output() readonly thyOnPageIndexChange: EventEmitter<number> = new EventEmitter<number>();
509

70✔
510
    @Output() readonly thyOnPageSizeChange: EventEmitter<number> = new EventEmitter<number>();
511

512
    /**
513
     * 多选回调事件
514
     */
70✔
515
    @Output() readonly thyOnMultiSelectChange: EventEmitter<ThyMultiSelectEvent> = new EventEmitter<ThyMultiSelectEvent>();
516

517
    /**
518
     * 单选回调事件
519
     */
70✔
520
    @Output() readonly thyOnRadioSelectChange: EventEmitter<ThyRadioSelectEvent> = new EventEmitter<ThyRadioSelectEvent>();
521

522
    /**
523
     * 拖动修改事件
524
     */
70✔
525
    @Output() readonly thyOnDraggableChange: EventEmitter<ThyTableDraggableEvent> = new EventEmitter<ThyTableDraggableEvent>();
526

527
    /**
528
     * 表格行点击触发事件
529
     */
70✔
530
    @Output() readonly thyOnRowClick: EventEmitter<ThyTableRowEvent> = new EventEmitter<ThyTableRowEvent>();
531

532
    /**
533
     * 列排序修改事件
534
     */
70✔
535
    @Output() readonly thySortChange: EventEmitter<ThyTableSortEvent> = new EventEmitter<ThyTableSortEvent>();
536

70✔
537
    @Output() readonly thyOnRowContextMenu: EventEmitter<ThyTableEvent> = new EventEmitter<ThyTableEvent>();
538

539
    @ContentChild('group', { static: true }) groupTemplate!: TemplateRef<SafeAny>;
540

541
    @ContentChildren(ThyTableColumnComponent)
542
    set listOfColumnComponents(components: QueryList<ThyTableColumnComponent>) {
66✔
543
        if (components) {
66✔
544
            this.columns = components.toArray();
66✔
545
            this.hasFixed = !!this.columns.find(item => {
367✔
546
                return item.fixed === this.fixedDirection.left || item.fixed === this.fixedDirection.right;
547
            });
66✔
548
            this.buildSkeletonColumns();
66✔
549
            this._initializeColumns();
66✔
550
            this._initializeDataModel();
551
        }
552
    }
553

554
    // 数据的折叠展开状态
70✔
555
    public expandStatusMap: Dictionary<boolean> = {};
556

70✔
557
    public expandStatusMapOfGroup: Dictionary<boolean> = {};
558

70✔
559
    private expandStatusMapOfGroupBeforeDrag: Dictionary<boolean> = {};
560

70✔
561
    dragPreviewClass = 'thy-table-drag-preview';
562

70✔
563
    public skeletonColumns: ThyTableSkeletonColumn[] = [];
564

565
    constructor() {
70✔
566
        this._bindTrackFn();
567
    }
568

569
    private _initializeColumns() {
374✔
570
        if (!this.columns.some(item => item.expand === true) && this.columns.length > 0) {
65✔
571
            this.columns[0].expand = true;
572
        }
66✔
573
        this._initializeColumnFixedPositions();
574
    }
575

576
    private _initializeColumnFixedPositions() {
377✔
577
        const leftFixedColumns = this.columns.filter(item => item.fixed === ThyFixedDirection.left);
66✔
578
        leftFixedColumns.forEach((item, index) => {
4✔
579
            const previous = leftFixedColumns[index - 1];
4✔
580
            item.left = previous ? previous.left! + parseInt(previous.width.toString(), 10) : 0;
581
        });
377✔
582
        const rightFixedColumns = this.columns.filter(item => item.fixed === ThyFixedDirection.right).reverse();
66✔
583
        rightFixedColumns.forEach((item, index) => {
1✔
584
            const previous = rightFixedColumns[index - 1];
1!
585
            item.right = previous ? previous.right! + parseInt(previous.width.toString(), 10) : 0;
586
        });
587
    }
588

589
    private _initializeDataModel() {
133✔
590
        this.model.forEach(row => {
731✔
591
            this.columns.forEach(column => {
2,097✔
592
                this._initialSelections(row, column);
2,097✔
593
                this._initialCustomModelValue(row, column);
594
            });
595
        });
596
    }
597

598
    private _initialSelections(row: object, column: ThyTableColumnComponent) {
2,097✔
599
        if (column.selections) {
504✔
600
            if (column.type === 'checkbox') {
246✔
601
                helpers.set(row, column.key, column.selections.includes(helpers.get(row, this.rowKey)));
246✔
602
                this.onModelChange(row, column);
603
            }
504✔
604
            if (column.type === 'radio') {
6!
605
                if (column.selections.includes(helpers.get(row, this.rowKey))) {
×
606
                    this.selectedRadioRow = row;
607
                }
608
            }
609
        }
610
    }
611

612
    private _initialCustomModelValue(row: object, column: ThyTableColumnComponent) {
2,132✔
613
        if (column.type === customType.switch) {
356✔
614
            helpers.set(row, column.key, get(row, column.model));
615
        }
616
    }
617

618
    private _refreshCustomModelValue(row: SafeAny) {
369✔
619
        this.columns.forEach(column => {
35✔
620
            this._initialCustomModelValue(row, column);
621
        });
622
    }
623

624
    private _applyDiffChanges(changes: IterableChanges<SafeAny> | undefined | null) {
78✔
625
        if (changes) {
62✔
626
            changes.forEachAddedItem((record: IterableChangeRecord<SafeAny>) => {
369✔
627
                this._refreshCustomModelValue(record.item);
628
            });
629
        }
630
    }
631

632
    private _bindTrackFn() {
70✔
633
        this.trackByFn = function (this: SafeAny, index: number, row: SafeAny): SafeAny {
180!
634
            return row && this.rowKey ? row[this.rowKey] : index;
635
        }.bind(this);
636
    }
637

638
    private _destroyInvalidAttribute() {
70✔
639
        this.model.forEach(row => {
363✔
640
            for (const key in row) {
2,911✔
641
                if (key.includes('[$$column]')) {
589✔
642
                    delete (row as SafeAny)[key];
643
                }
644
            }
645
        });
646
    }
647

95✔
648
    private _setClass(first = false) {
161✔
649
        if (!first && !this.initialized) {
90✔
650
            return;
651
        }
71✔
652
        const classNames: string[] = [];
71✔
653
        if (this.size) {
71✔
654
            classNames.push(`table-${this.size}`);
655
        }
71✔
656
        if (tableThemeMap[this.theme]) {
71✔
657
            classNames.push(tableThemeMap[this.theme]);
658
        }
659

71✔
660
        this.updateHostClassService.updateClass(classNames);
661
    }
662

663
    public updateColumnSelections(key: string, selections: SafeAny): void {
×
664
        const column = this.columns.find(item => item.key === key);
×
665
        this.model.forEach(row => {
×
666
            this._initialSelections(row, column!);
667
        });
668
    }
669

670
    public isTemplateRef(ref: SafeAny) {
13,820✔
671
        return ref instanceof TemplateRef;
672
    }
673

674
    public getModelValue(row: SafeAny, path: string) {
6,957✔
675
        return get(row, path);
676
    }
677

678
    public renderRowClassName(row: SafeAny, index: number) {
1,220✔
679
        if (!this.rowClassName) {
410✔
680
            return null;
681
        }
810!
682
        if (isString(this.rowClassName)) {
810✔
683
            return this.rowClassName;
684
        } else {
×
685
            return (this.rowClassName as Function)(row, index);
686
        }
687
    }
688

689
    public onModelChange(row: SafeAny, column: ThyTableColumnComponent) {
249✔
690
        if (column.model) {
249✔
691
            set(row, column.model, row[column.key]);
692
        }
693
    }
694

695
    public onStopPropagation(event: Event) {
1!
696
        if (this.wholeRowSelect) {
×
697
            event.stopPropagation();
698
        }
699
    }
700

701
    public onPageChange(event: PageChangedEvent) {
1✔
702
        this.thyOnPageChange.emit(event);
703
    }
704

705
    public onPageIndexChange(event: number) {
1✔
706
        this.thyOnPageIndexChange.emit(event);
707
    }
708

709
    public onPageSizeChange(event: number) {
1✔
710
        this.thyOnPageSizeChange.emit(event);
711
    }
712

713
    public onCheckboxChange(row: SafeAny, column: ThyTableColumnComponent) {
1✔
714
        this.onModelChange(row, column);
1✔
715
        this.onMultiSelectChange(null as SafeAny, row, column);
716
    }
717

718
    public onMultiSelectChange(event: Event, row: SafeAny, column: ThyTableColumnComponent) {
2✔
719
        const rows = this.model.filter(item => {
12✔
720
            return helpers.get(item, column.key);
721
        });
2✔
722
        const multiSelectEvent: ThyMultiSelectEvent = {
723
            event: event,
724
            row: row,
725
            rows: rows
726
        };
2✔
727
        this.thyOnMultiSelectChange.emit(multiSelectEvent);
728
    }
729

730
    public onRadioSelectChange(event: Event, row: SafeAny) {
1✔
731
        const radioSelectEvent: ThyRadioSelectEvent = {
732
            event: event,
733
            row: row
734
        };
1✔
735
        this.thyOnRadioSelectChange.emit(radioSelectEvent);
736
    }
737

738
    public onSwitchChange(event: Event, row: SafeAny, column: SafeAny) {
1✔
739
        const switchEvent: ThySwitchEvent = {
740
            event: event,
741
            row: row,
742
            refresh: (value: SafeAny) => {
×
743
                value = value || row;
×
744
                setTimeout(() => {
×
745
                    value[column.key] = get(value, column.model);
746
                });
747
            }
748
        };
1✔
749
        this.thyOnSwitchChange.emit(switchEvent);
750
    }
751

752
    showExpand(row: SafeAny) {
180✔
753
        return row[this.thyChildrenKey] && row[this.thyChildrenKey].length > 0;
754
    }
755

756
    isExpanded(row: SafeAny) {
367✔
757
        return this.expandStatusMap[row[this.rowKey]];
758
    }
759

760
    iconIndentComputed(level: number) {
180✔
761
        if (this.mode === 'tree') {
180✔
762
            return level * this.thyIndent - 5;
763
        }
764
    }
765

766
    tdIndentComputed(level: number, column: SafeAny) {
180✔
767
        return {
768
            left: `${column.left}px`,
769
            right: `${column.right}px`,
770
            position: 'relative',
771
            paddingLeft: `${(level + 1) * this.thyIndent - 5}px`
772
        };
773
    }
774

775
    expandChildren(row: SafeAny) {
7✔
776
        if (this.isExpanded(row)) {
3✔
777
            this.expandStatusMap[row[this.rowKey]] = false;
778
        } else {
4✔
779
            this.expandStatusMap[row[this.rowKey]] = true;
780
        }
781
    }
782

783
    onDragGroupStarted(event: CdkDragStart<unknown>) {
1✔
784
        this.expandStatusMapOfGroupBeforeDrag = { ...this.expandStatusMapOfGroup };
2✔
785
        const groups = this.groups.filter(group => group.expand);
1✔
786
        this.foldGroups(groups);
1✔
787
        this.onDragStarted(event);
1✔
788
        this.cdr.detectChanges();
789
    }
790

791
    onDragGroupEnd(event: CdkDragEnd<unknown>) {
2✔
792
        const groups = this.groups.filter(group => this.expandStatusMapOfGroupBeforeDrag[group!.id!]);
1✔
793
        this.expandGroups(groups);
1✔
794
        this.cdr.detectChanges();
795
    }
796

797
    private onDragGroupDropped(event: CdkDragDrop<unknown>) {
2✔
798
        const group = this.groups.find(group => {
3✔
799
            return event.item.data.id === group.id;
800
        });
2✔
801
        if (group) {
802
            // drag group
1✔
803
            const dragEvent: ThyTableDraggableEvent = {
804
                model: event.item,
805
                models: this.groups,
806
                oldIndex: event.previousIndex,
807
                newIndex: event.currentIndex
808
            };
1✔
809
            moveItemInArray(this.groups, event.previousIndex, event.currentIndex);
1✔
810
            this.thyOnDraggableChange.emit(dragEvent);
811
        } else {
812
            // drag group children
1✔
813
            const group = this.groups.find(group => {
2✔
814
                return event.item.data[this.groupBy] === group.id;
815
            });
816
            const groupIndex =
1✔
817
                event.container.getSortedItems().findIndex(item => {
2✔
818
                    return item.data.id === event.item.data[this.groupBy];
819
                }) + 1;
1✔
820
            const dragEvent: ThyTableDraggableEvent = {
821
                model: event.item,
822
                models: group!.children,
823
                oldIndex: event.previousIndex - groupIndex,
824
                newIndex: event.currentIndex - groupIndex
825
            };
1✔
826
            moveItemInArray(group!.children!, dragEvent.oldIndex!, dragEvent.newIndex!);
1✔
827
            this.thyOnDraggableChange.emit(dragEvent);
828
        }
829
    }
830

831
    onDragStarted(event: CdkDragStart<unknown>) {
3✔
832
        this.ngZone.runOutsideAngular(() =>
3✔
833
            setTimeout(() => {
3✔
834
                const preview = this.document.getElementsByClassName(this.dragPreviewClass)[0];
3✔
835
                const originalTds: HTMLCollection = event.source._dragRef.getPlaceholderElement()?.children;
3✔
836
                if (preview) {
3✔
837
                    Array.from(preview?.children).forEach((element, index: number) => {
12✔
838
                        (element as HTMLElement).style.width = `${originalTds[index]?.clientWidth}px`;
839
                    });
840
                }
841
            })
842
        );
843
    }
844

70✔
845
    dropListEnterPredicate = (index: number, drag: CdkDrag, drop: CdkDropList) => {
×
846
        return drop.getSortedItems()[index].data.group_id === drag.data.group_id;
847
    };
848

849
    private onDragModelDropped(event: CdkDragDrop<unknown>) {
1✔
850
        const dragEvent: ThyTableDraggableEvent = {
851
            model: event.item,
852
            models: this.model,
853
            oldIndex: event.previousIndex,
854
            newIndex: event.currentIndex
855
        };
1✔
856
        moveItemInArray(this.model, event.previousIndex, event.currentIndex);
1✔
857
        this.thyOnDraggableChange.emit(dragEvent);
858
    }
859

860
    onDragDropped(event: CdkDragDrop<unknown>) {
3✔
861
        if (this.mode === 'group') {
2✔
862
            this.onDragGroupDropped(event);
1✔
863
        } else if (this.mode === 'list') {
1✔
864
            this.onDragModelDropped(event);
865
        }
866
    }
867

868
    onColumnHeaderClick(event: Event, column: ThyTableColumnComponent) {
3✔
869
        if (column.sortable) {
3✔
870
            const { sortDirection, model, sortChange } = column;
871
            let direction;
3✔
872
            if (sortDirection === ThyTableSortDirection.default) {
1✔
873
                direction = ThyTableSortDirection.desc;
2✔
874
            } else if (sortDirection === ThyTableSortDirection.desc) {
1✔
875
                direction = ThyTableSortDirection.asc;
876
            } else {
1✔
877
                direction = ThyTableSortDirection.default;
878
            }
3✔
879
            column.sortDirection = direction;
3✔
880
            const sortEvent = { event, key: model, direction };
3✔
881
            sortChange.emit(sortEvent);
3✔
882
            this.thySortChange.emit(sortEvent);
883
        }
884
    }
885

886
    public onRowClick(event: Event, row: SafeAny) {
11✔
887
        const next = this.onRowClickPropagationEventHandler(event, row);
11✔
888
        if (next) {
4✔
889
            if (this.wholeRowSelect) {
2✔
890
                const column = this.columns.find(item => {
2✔
891
                    return item.type === customType.checkbox || item.type === customType.radio;
892
                });
2✔
893
                if (column && !column.disabled) {
2✔
894
                    if (column.type === customType.checkbox) {
1✔
895
                        row[column.key] = !row[column.key];
1✔
896
                        this.onModelChange(row, column);
1✔
897
                        this.onMultiSelectChange(event, row, column);
898
                    }
2✔
899
                    if (column.type === customType.radio) {
1✔
900
                        this.selectedRadioRow = row;
1✔
901
                        this.onRadioSelectChange(event, row);
902
                    }
903
                }
904
            }
4✔
905
            const rowEvent = {
906
                event: event,
907
                row: row
908
            };
4✔
909
            this.thyOnRowClick.emit(rowEvent);
910
        }
911
    }
912

913
    private onRowClickPropagationEventHandler(event: Event, row: SafeAny): boolean {
11✔
914
        if ((event.target as Element).closest('.tree-expand-icon')) {
7✔
915
            this.expandChildren(row);
7✔
916
            return false;
917
        }
4✔
918
        return true;
919
    }
920

921
    public onRowContextMenu(event: Event, row: SafeAny) {
1✔
922
        const contextMenuEvent: ThyTableEvent = {
923
            event: event,
924
            row: row
925
        };
1✔
926
        this.thyOnRowContextMenu.emit(contextMenuEvent);
927
    }
928

929
    private _refreshColumns() {
8!
930
        const components = this.columns || [];
8✔
931
        const _columns = components.map(component => {
47✔
932
            return {
933
                width: component.width,
934
                className: component.className
935
            };
936
        });
937

8✔
938
        this.columns.forEach((n, i) => {
47✔
939
            Object.assign(n, _columns[i]);
940
        });
941
    }
942

943
    private buildGroups(originGroups: SafeAny) {
14✔
944
        const originGroupsMap = helpers.keyBy(originGroups, 'id');
14✔
945
        this.groups = [];
14✔
946
        originGroups.forEach((origin: SafeAny) => {
28✔
947
            const group: ThyTableGroup = { id: origin[this.rowKey], children: [], origin };
948

28!
949
            if (this.expandStatusMapOfGroup.hasOwnProperty(group!.id!)) {
×
950
                group.expand = this.expandStatusMapOfGroup[group!.id!];
951
            } else {
28✔
952
                group.expand = !!(originGroupsMap[group.id!] as SafeAny).expand;
953
            }
954

28✔
955
            this.groups.push(group);
956
        });
957
    }
958

959
    private buildModel() {
13✔
960
        const groupsMap = keyBy(this.groups, 'id');
13✔
961
        this.model.forEach(row => {
79✔
962
            const group = groupsMap[helpers.get(row, this.groupBy)];
79✔
963
            if (group) {
78✔
964
                group.children!.push(row);
965
            }
966
        });
967
    }
968

969
    public expandGroup(group: ThyTableGroup) {
4✔
970
        group.expand = !group.expand;
4✔
971
        this.expandStatusMapOfGroup[group.id!] = group.expand;
972
    }
973

974
    private expandGroups(groups: ThyTableGroup[]) {
1✔
975
        groups.forEach(group => {
×
976
            this.expandGroup(group);
977
        });
978
    }
979

980
    private foldGroups(groups: ThyTableGroup[]) {
1✔
981
        groups.forEach(group => {
1✔
982
            this.expandGroup(group);
983
        });
984
    }
985

986
    private updateScrollClass() {
8✔
987
        const scrollElement = this.tableScrollElement;
8✔
988
        const maxScrollLeft = scrollElement.scrollWidth - scrollElement.offsetWidth;
8✔
989
        const scrollX = scrollElement.scrollLeft;
8✔
990
        const lastScrollClassName = this.scrollClassName;
8✔
991
        this.scrollClassName = '';
8!
992
        if (scrollElement.scrollWidth > scrollElement.clientWidth) {
×
993
            if (scrollX >= maxScrollLeft) {
×
994
                this.scrollClassName = css.tableScrollRight;
×
995
            } else if (scrollX === 0) {
×
996
                this.scrollClassName = css.tableScrollLeft;
997
            } else {
×
998
                this.scrollClassName = css.tableScrollMiddle;
999
            }
1000
        }
8✔
1001
        if (lastScrollClassName) {
8✔
1002
            this.renderer.removeClass(this.tableScrollElement, lastScrollClassName);
1003
        }
8!
1004
        if (this.scrollClassName) {
×
1005
            this.renderer.addClass(this.tableScrollElement, this.scrollClassName);
1006
        }
1007
    }
1008

1009
    ngOnInit() {
66✔
1010
        this.updateHostClassService.initializeElement(this.tableElementRef.nativeElement);
66✔
1011
        this._setClass(true);
66✔
1012
        this.initialized = true;
1013

66✔
1014
        merge(this.viewportRuler.change(200), of(null).pipe(delay(200)))
1015
            .pipe(takeUntilDestroyed(this.destroyRef))
1016
            .subscribe(() => {
8✔
1017
                this._refreshColumns();
8✔
1018
                this.updateScrollClass();
8✔
1019
                this.cdr.detectChanges();
1020
            });
1021

66✔
1022
        this.ngZone.runOutsideAngular(() => {
66✔
1023
            this.scroll$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
×
1024
                this.updateScrollClass();
1025
            });
1026
        });
1027
    }
1028

1029
    private buildSkeletonColumns() {
66✔
1030
        this.skeletonColumns = [];
1031

66✔
1032
        this.columns.forEach((column: ThyTableColumnComponent, index: number) => {
377✔
1033
            const item = {
556✔
1034
                type: this.thyColumnSkeletonTypes[index] || ThyTableColumnSkeletonType.default,
682✔
1035
                width: column.width || 'auto'
1036
            };
377✔
1037
            this.skeletonColumns = [...this.skeletonColumns, item];
1038
        });
1039
    }
1040

1041
    ngAfterViewInit(): void {
66!
1042
        if (isPlatformServer(this.platformId)) {
×
1043
            return;
1044
        }
1045

66✔
1046
        this.rows.changes
1047
            .pipe(
1048
                startWith(this.rows),
1049
                switchMap(
1050
                    () =>
78✔
1051
                        new Observable<Event>(subscriber =>
78✔
1052
                            this.ngZone.runOutsideAngular(() =>
78✔
1053
                                merge(
1054
                                    ...this.rows.map(row =>
392✔
1055
                                        fromEvent(
1056
                                            row.nativeElement,
1057
                                            // Note: there's no need to add touch, pointer and mouse event listeners together.
1058
                                            // There can be any number of rows, which will lead to adding N * 3 event listeners.
1059
                                            // According to the spec (https://www.w3.org/TR/pointerevents/#examples), we can use feature detection
1060
                                            // to determine if pointer events are available. If pointer events are available, we have to listen only
1061
                                            // to the `pointerdown` event. Otherwise, we have to determine if we're on a touch device or not.
1062
                                            // Touch events are handled earlier than mouse events, tho not all user agents dispatch mouse events
1063
                                            // after touch events. See the spec: https://www.w3.org/TR/touch-events/#mouse-events.
392!
1064
                                            window.PointerEvent
1065
                                                ? 'pointerdown'
×
1066
                                                : 'ontouchstart' in row.nativeElement
1067
                                                  ? 'touchstart'
1068
                                                  : 'mousedown',
1069
                                            // Note: since Chrome 56 defaults document level `touchstart` listener to passive.
1070
                                            // The element `touchstart` listener is not passive by default
1071
                                            // We never call `preventDefault()` on it, so we're safe making it passive too.
1072
                                            passiveEventListenerOptions as AddEventListenerOptions
1073
                                        )
1074
                                    )
1075
                                ).subscribe(subscriber)
1076
                            )
1077
                        )
1078
                ),
1079
                takeUntilDestroyed(this.destroyRef)
1080
            )
1081
            .subscribe(event => {
1✔
1082
                if (!this.draggable) {
1✔
1083
                    event.stopPropagation();
1084
                }
1085
            });
1086
    }
1087

1088
    ngOnChanges(simpleChanges: SimpleChanges) {
78✔
1089
        const modeChange = simpleChanges.thyMode;
78✔
1090
        const thyGroupsChange = simpleChanges.thyGroups;
78✔
1091
        const isGroupMode = modeChange && modeChange.currentValue === 'group';
78✔
1092
        if (isGroupMode && thyGroupsChange && thyGroupsChange.firstChange) {
12✔
1093
            this.buildGroups(thyGroupsChange.currentValue);
12✔
1094
            this.buildModel();
1095
        }
1096

78✔
1097
        if (this._diff) {
78✔
1098
            const changes = this._diff.diff(this.model);
78✔
1099
            this._applyDiffChanges(changes);
1100
        }
1101
    }
1102

1103
    ngOnDestroy() {
70✔
1104
        this._destroyInvalidAttribute();
1105
    }
1106
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc