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

atinc / ngx-tethys / d4d3bf94-0b08-4dba-b53c-fd90c5e0b80a

27 Mar 2025 01:46AM UTC coverage: 90.236% (+0.06%) from 90.179%
d4d3bf94-0b08-4dba-b53c-fd90c5e0b80a

push

circleci

minlovehua
feat: save draft

5598 of 6865 branches covered (81.54%)

Branch coverage included in aggregate %.

8 of 8 new or added lines in 7 files covered. (100.0%)

157 existing lines in 46 files now uncovered.

13357 of 14141 relevant lines covered (94.46%)

992.52 hits per line

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

88.1
/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 { DOCUMENT, 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,
1✔
28
    NgZone,
1✔
29
    numberAttribute,
1✔
30
    OnChanges,
2✔
31
    OnDestroy,
1✔
32
    OnInit,
33
    Output,
34
    PLATFORM_ID,
35
    QueryList,
36
    Renderer2,
1✔
37
    SimpleChanges,
38
    TemplateRef,
39
    ViewChild,
40
    ViewChildren,
41
    ViewEncapsulation
42
} from '@angular/core';
1✔
43

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

71
export type ThyTableTheme = 'default' | 'bordered' | 'boxed';
56✔
72

3✔
73
export type ThyTableMode = 'list' | 'group' | 'tree';
74

75
export type ThyTableSize = 'md' | 'sm' | 'xs' | 'lg' | 'xlg' | 'default';
76

67✔
77
export enum ThyFixedDirection {
67✔
78
    left = 'left',
67✔
79
    right = 'right'
67✔
80
}
1✔
81

82
interface ThyTableGroup<T = unknown> {
83
    id?: string;
84
    expand?: boolean;
45!
85
    children?: object[];
45✔
86
    origin?: T;
87
}
88

50✔
89
const tableThemeMap = {
50✔
90
    default: 'table-default',
91
    bordered: 'table-bordered',
92
    boxed: 'table-boxed'
48✔
93
};
45✔
94

45!
95
const customType = {
×
96
    index: 'index',
97
    checkbox: 'checkbox',
98
    radio: 'radio',
45✔
99
    switch: 'switch'
100
};
45✔
101

45✔
102
const css = {
103
    tableBody: 'thy-table-body',
104
    tableScrollLeft: 'thy-table-scroll-left',
45✔
105
    tableScrollRight: 'thy-table-scroll-right',
106
    tableScrollMiddle: 'thy-table-scroll-middle'
107
};
45✔
108

109
const passiveEventListenerOptions = normalizePassiveListenerOptions({ passive: true });
110

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

70✔
163
    private readonly destroyRef = inject(DestroyRef);
70✔
164

70✔
165
    public customType = customType;
70✔
166

70✔
167
    public model: object[] = [];
70✔
168

70✔
169
    public groups: ThyTableGroup[] = [];
70✔
170

70✔
171
    public rowKey = '_id';
70✔
172

70✔
173
    public groupBy: string;
70✔
174

70✔
175
    public mode: ThyTableMode = 'list';
70✔
176

70✔
177
    public theme: ThyTableTheme = 'default';
70✔
178

70✔
179
    public className = '';
70✔
180

70✔
181
    public size: ThyTableSize = 'md';
70✔
182

70✔
183
    public rowClassName: string | Function;
70✔
184

70✔
185
    public loadingDone = true;
70✔
186

70✔
187
    public loadingText: string;
70✔
188

70✔
189
    public emptyOptions: ThyTableEmptyOptions = {};
384✔
190

70✔
191
    public draggable = false;
192

193
    public selectedRadioRow: SafeAny = null;
194

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

70✔
197
    public trackByFn: SafeAny;
70✔
198

70✔
199
    public wholeRowSelect = false;
70✔
200

70✔
201
    public fixedDirection = ThyFixedDirection;
70✔
202

70✔
203
    public hasFixed = false;
70✔
204

70✔
205
    public columns: ThyTableColumnComponent[] = [];
206

70✔
207
    private _diff: IterableDiffer<SafeAny>;
70✔
208

70✔
209
    private initialized = false;
70✔
210

70✔
211
    private _oldThyClassName = '';
70✔
UNCOV
212

×
213
    private scrollClassName = css.tableScrollLeft;
214

70✔
215
    private get tableScrollElement(): HTMLElement {
216
        return this.elementRef.nativeElement.getElementsByClassName(css.tableBody)[0] as HTMLElement;
217
    }
374✔
218

65✔
219
    private get scroll$() {
220
        return merge(this.tableScrollElement ? fromEvent<MouseEvent>(this.tableScrollElement, 'scroll') : EMPTY);
66✔
221
    }
222

223
    /**
377✔
224
     * 设置数据为空时展示的模板
66✔
225
     * @type TemplateRef
4✔
226
     */
4✔
227
    @ContentChild('empty') emptyTemplate: TemplateRef<SafeAny>;
228

377✔
229
    @ViewChild('table', { static: true }) tableElementRef: ElementRef<SafeAny>;
66✔
230

1✔
231
    @ViewChildren('rows', { read: ElementRef }) rows: QueryList<ElementRef<HTMLElement>>;
1!
232

233
    /**
234
     * 表格展示方式,列表/分组/树
235
     * @type list | group | tree
133✔
236
     * @default list
731✔
237
     */
2,097✔
238
    @Input()
2,097✔
239
    set thyMode(value: ThyTableMode) {
240
        this.mode = value || this.mode;
241
    }
242

243
    /**
2,097✔
244
     * thyMode的值为 `group` 时分组的 Key
504✔
245
     */
246✔
246
    @Input()
246✔
247
    set thyGroupBy(value: string) {
248
        this.groupBy = value;
504✔
249
    }
6!
UNCOV
250

×
251
    /**
252
     * 设置每行数据的唯一标识属性名
253
     * @default _id
254
     */
255
    @Input()
256
    set thyRowKey(value: SafeAny) {
2,132✔
257
        this.rowKey = value || this.rowKey;
356✔
258
    }
259

260
    /**
261
     * 分组数据源
369✔
262
     */
35✔
263
    @Input()
264
    set thyGroups(value: SafeAny) {
265
        if (this.mode === 'group') {
266
            this.buildGroups(value);
78✔
267
        }
62✔
268
    }
369✔
269

270
    /**
271
     * 数据源
272
     */
273
    @Input()
70✔
274
    set thyModel(value: SafeAny) {
180!
275
        this.model = value || [];
276
        this._diff = this._differs.find(this.model).create();
277
        this._initializeDataModel();
278

70✔
279
        if (this.mode === 'group') {
363✔
280
            this.buildModel();
2,911✔
281
        }
589✔
282
    }
283

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

71!
295
    /**
71✔
296
     * 表格的大小
297
     * @type xs | sm | md | lg | xlg | default
71✔
298
     * @default md
299
     */
300
    @Input()
×
301
    set thySize(value: ThyTableSize) {
×
UNCOV
302
        this.size = value || this.size;
×
303
        this._setClass();
304
    }
305

306
    /**
13,820✔
307
     * 设置表格最小宽度,一般是适用于设置列宽为百分之或auto时限制表格最小宽度'
308
     */
309
    @Input()
6,957✔
310
    @InputCssPixel()
311
    thyMinWidth: string | number;
312

1,220✔
313
    /**
410✔
314
     * 设置为 fixed 布局表格,设置 fixed 后,列宽将严格按照设置宽度展示,列宽将不会根据表格内容自动调整
315
     * @default false
810!
316
     */
810✔
317
    @Input({ transform: coerceBooleanProperty }) thyLayoutFixed: boolean;
318

UNCOV
319
    /**
×
320
     * 是否表头固定,若设置为 true, 需要同步设置 thyHeight
321
     * @default false
322
     */
323
    @Input({ transform: coerceBooleanProperty }) thyHeaderFixed: boolean;
249!
324

249✔
325
    /**
326
     * 表格的高度
327
     */
328
    @HostBinding('style.height')
1!
UNCOV
329
    @Input()
×
330
    @InputCssPixel()
331
    thyHeight: string;
332

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

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

1✔
358
    /**
359
     * 设置加载状态
360
     * @default true
361
     */
1✔
362
    @Input({ transform: coerceBooleanProperty })
363
    set thyLoadingDone(value: boolean) {
364
        this.loadingDone = value;
1✔
365
    }
366

367
    /**
368
     * 设置加载时显示的文本,已废弃
×
369
     * @deprecated
×
UNCOV
370
     */
×
371
    @Input()
372
    set thyLoadingText(value: string) {
373
        this.loadingText = value;
374
    }
1✔
375

376
    /**
377
     * 配置空状态组件
180✔
378
     */
379
    @Input()
380
    set thyEmptyOptions(value: ThyTableEmptyOptions) {
367✔
381
        this.emptyOptions = value;
382
    }
383

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

396
    /**
7✔
397
     * 设置当前页码
3✔
398
     * @default 1
399
     */
400
    @Input({ transform: numberAttribute })
4✔
401
    set thyPageIndex(value: number) {
402
        this.pagination.index = value;
403
    }
404

1✔
405
    /**
2✔
406
     * 设置每页显示数量
1✔
407
     * @default 20
1✔
408
     */
1✔
409
    @Input({ transform: numberAttribute })
410
    set thyPageSize(value: number) {
411
        this.pagination.size = value;
2✔
412
    }
1✔
413

1✔
414
    /**
415
     * 设置总页数
416
     */
2✔
417
    @Input({ transform: numberAttribute })
3✔
418
    set thyPageTotal(value: number) {
419
        this.pagination.total = value;
2✔
420
    }
421

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

2✔
434
    /**
435
     * 是否显示表格头
1✔
436
     * @default false
2✔
437
     */
438
    @Input({ transform: coerceBooleanProperty }) thyHeadless = false;
1✔
439

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

449
    /**
3✔
450
     * 是否显示左侧 Total
3✔
451
     */
3✔
452
    @Input({ alias: 'thyShowTotal', transform: coerceBooleanProperty }) showTotal = false;
3!
453

3✔
454
    /**
12✔
455
     * 是否显示调整每页显示条数下拉框
456
     */
457
    @Input({ alias: 'thyShowSizeChanger', transform: coerceBooleanProperty }) showSizeChanger = false;
458

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

1✔
468
    /**
469
     * thyMode 为 tree 时,设置 Tree 树状数据展示时的缩进
470
     */
3✔
471
    @Input({ transform: numberAttribute }) thyIndent = 20;
2✔
472

473
    /**
1!
474
     * thyMode 为 tree 时,设置 Tree 树状数据对象中的子节点 Key
1✔
475
     * @type string
476
     */
477
    @Input() thyChildrenKey = 'children';
478

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

487
    @Input() thyDragDisabledPredicate: (item: SafeAny) => boolean = () => false;
488

1✔
489
    /**
490
     * 表格列的骨架类型
3✔
491
     * @type ThyTableColumnSkeletonType[]
3✔
492
     */
3✔
493
    @Input() thyColumnSkeletonTypes: ThyTableColumnSkeletonType[] = [
3✔
494
        ThyTableColumnSkeletonType.title,
495
        ThyTableColumnSkeletonType.member,
496
        ThyTableColumnSkeletonType.default
497
    ];
11✔
498

11✔
499
    /**
4✔
500
     * 切换组件回调事件
2✔
501
     */
2✔
502
    @Output() thyOnSwitchChange: EventEmitter<ThySwitchEvent> = new EventEmitter<ThySwitchEvent>();
503

2!
504
    /**
2✔
505
     * 表格分页回调事件
1✔
506
     */
1✔
507
    @Output() thyOnPageChange: EventEmitter<PageChangedEvent> = new EventEmitter<PageChangedEvent>();
1✔
508

509
    /**
2✔
510
     * 表格分页当前页改变回调事件
1✔
511
     */
1✔
512
    @Output() thyOnPageIndexChange: EventEmitter<number> = new EventEmitter<number>();
513

514
    @Output() thyOnPageSizeChange: EventEmitter<number> = new EventEmitter<number>();
515

4✔
516
    /**
517
     * 多选回调事件
518
     */
519
    @Output() thyOnMultiSelectChange: EventEmitter<ThyMultiSelectEvent> = new EventEmitter<ThyMultiSelectEvent>();
4✔
520

521
    /**
522
     * 单选回调事件
523
     */
11✔
524
    @Output() thyOnRadioSelectChange: EventEmitter<ThyRadioSelectEvent> = new EventEmitter<ThyRadioSelectEvent>();
7✔
525

7✔
526
    /**
527
     * 拖动修改事件
4✔
528
     */
529
    @Output() thyOnDraggableChange: EventEmitter<ThyTableDraggableEvent> = new EventEmitter<ThyTableDraggableEvent>();
530

1✔
531
    /**
532
     * 表格行点击触发事件
533
     */
534
    @Output() thyOnRowClick: EventEmitter<ThyTableRowEvent> = new EventEmitter<ThyTableRowEvent>();
1✔
535

536
    /**
537
     * 列排序修改事件
8!
538
     */
8✔
539
    @Output() thySortChange: EventEmitter<ThyTableSortEvent> = new EventEmitter<ThyTableSortEvent>();
47✔
540

541
    @Output() thyOnRowContextMenu: EventEmitter<ThyTableEvent> = new EventEmitter<ThyTableEvent>();
542

543
    @ContentChild('group', { static: true }) groupTemplate: TemplateRef<SafeAny>;
544

8✔
545
    @ContentChildren(ThyTableColumnComponent)
47✔
546
    set listOfColumnComponents(components: QueryList<ThyTableColumnComponent>) {
547
        if (components) {
548
            this.columns = components.toArray();
549
            this.hasFixed = !!this.columns.find(item => {
14✔
550
                return item.fixed === this.fixedDirection.left || item.fixed === this.fixedDirection.right;
14✔
551
            });
14✔
552
            this.buildSkeletonColumns();
28✔
553
            this._initializeColumns();
28!
UNCOV
554
            this._initializeDataModel();
×
555
        }
556
    }
557

28✔
558
    // 数据的折叠展开状态
559
    public expandStatusMap: Dictionary<boolean> = {};
28✔
560

561
    public expandStatusMapOfGroup: Dictionary<boolean> = {};
562

563
    private expandStatusMapOfGroupBeforeDrag: Dictionary<boolean> = {};
13✔
564

13✔
565
    dragPreviewClass = 'thy-table-drag-preview';
79✔
566

79✔
567
    public skeletonColumns: ThyTableSkeletonColumn[] = [];
78✔
568

569
    constructor() {
570
        this._bindTrackFn();
571
    }
572

4✔
573
    private _initializeColumns() {
4✔
574
        if (!this.columns.some(item => item.expand === true) && this.columns.length > 0) {
575
            this.columns[0].expand = true;
576
        }
1✔
UNCOV
577
        this._initializeColumnFixedPositions();
×
578
    }
579

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

×
UNCOV
593
    private _initializeDataModel() {
×
594
        this.model.forEach(row => {
595
            this.columns.forEach(column => {
×
UNCOV
596
                this._initialSelections(row, column);
×
597
                this._initialCustomModelValue(row, column);
598
            });
UNCOV
599
        });
×
600
    }
601

602
    private _initialSelections(row: object, column: ThyTableColumnComponent) {
8!
603
        if (column.selections) {
8✔
604
            if (column.type === 'checkbox') {
605
                row[column.key] = column.selections.includes(row[this.rowKey]);
8!
UNCOV
606
                this.onModelChange(row, column);
×
607
            }
608
            if (column.type === 'radio') {
609
                if (column.selections.includes(row[this.rowKey])) {
610
                    this.selectedRadioRow = row;
66✔
611
                }
66✔
612
            }
66✔
613
        }
66✔
614
    }
615

616
    private _initialCustomModelValue(row: object, column: ThyTableColumnComponent) {
8✔
617
        if (column.type === customType.switch) {
8✔
618
            row[column.key] = get(row, column.model);
8✔
619
        }
620
    }
66✔
621

66✔
UNCOV
622
    private _refreshCustomModelValue(row: SafeAny) {
×
623
        this.columns.forEach(column => {
624
            this._initialCustomModelValue(row, column);
625
        });
626
    }
627

66✔
628
    private _applyDiffChanges(changes: IterableChanges<SafeAny>) {
66✔
629
        if (changes) {
377✔
630
            changes.forEachAddedItem((record: IterableChangeRecord<SafeAny>) => {
556✔
631
                this._refreshCustomModelValue(record.item);
682✔
632
            });
633
        }
377✔
634
    }
635

636
    private _bindTrackFn() {
637
        this.trackByFn = function (this: SafeAny, index: number, row: SafeAny): SafeAny {
66!
UNCOV
638
            return row && this.rowKey ? row[this.rowKey] : index;
×
639
        }.bind(this);
640
    }
66✔
641

392✔
642
    private _destroyInvalidAttribute() {
643
        this.model.forEach(row => {
644
            for (const key in row) {
645
                if (key.includes('[$$column]')) {
646
                    delete row[key];
647
                }
648
            }
649
        });
392!
650
    }
651

×
652
    private _setClass(first = false) {
653
        if (!first && !this.initialized) {
654
            return;
655
        }
656
        const classNames: string[] = [];
657
        if (this.size) {
658
            classNames.push(`table-${this.size}`);
659
        }
1!
660
        if (tableThemeMap[this.theme]) {
1✔
661
            classNames.push(tableThemeMap[this.theme]);
662
        }
663

664
        this.updateHostClassService.updateClass(classNames);
665
    }
78✔
666

78✔
667
    public updateColumnSelections(key: string, selections: SafeAny): void {
78✔
668
        const column = this.columns.find(item => item.key === key);
78✔
669
        this.model.forEach(row => {
12✔
670
            this._initialSelections(row, column);
12✔
671
        });
672
    }
78!
673

78✔
674
    public isTemplateRef(ref: SafeAny) {
78✔
675
        return ref instanceof TemplateRef;
676
    }
677

678
    public getModelValue(row: SafeAny, path: string) {
70✔
679
        return get(row, path);
680
    }
1✔
681

1✔
682
    public renderRowClassName(row: SafeAny, index: number) {
683
        if (!this.rowClassName) {
684
            return null;
685
        }
686
        if (isString(this.rowClassName)) {
687
            return this.rowClassName;
688
        } else {
689
            return (this.rowClassName as Function)(row, index);
690
        }
691
    }
692

693
    public onModelChange(row: SafeAny, column: ThyTableColumnComponent) {
694
        if (column.model) {
695
            set(row, column.model, row[column.key]);
696
        }
697
    }
698

699
    public onStopPropagation(event: Event) {
700
        if (this.wholeRowSelect) {
701
            event.stopPropagation();
702
        }
703
    }
704

705
    public onPageChange(event: PageChangedEvent) {
706
        this.thyOnPageChange.emit(event);
707
    }
708

709
    public onPageIndexChange(event: number) {
710
        this.thyOnPageIndexChange.emit(event);
711
    }
712

713
    public onPageSizeChange(event: number) {
714
        this.thyOnPageSizeChange.emit(event);
715
    }
716

717
    public onCheckboxChange(row: SafeAny, column: ThyTableColumnComponent) {
718
        this.onModelChange(row, column);
719
        this.onMultiSelectChange(null, row, column);
720
    }
721

722
    public onMultiSelectChange(event: Event, row: SafeAny, column: ThyTableColumnComponent) {
723
        const rows = this.model.filter(item => {
724
            return item[column.key];
725
        });
726
        const multiSelectEvent: ThyMultiSelectEvent = {
727
            event: event,
728
            row: row,
729
            rows: rows
730
        };
1✔
731
        this.thyOnMultiSelectChange.emit(multiSelectEvent);
732
    }
733

734
    public onRadioSelectChange(event: Event, row: SafeAny) {
1✔
735
        const radioSelectEvent: ThyRadioSelectEvent = {
736
            event: event,
737
            row: row
738
        };
1✔
739
        this.thyOnRadioSelectChange.emit(radioSelectEvent);
740
    }
741

742
    public onSwitchChange(event: Event, row: SafeAny, column: SafeAny) {
743
        const switchEvent: ThySwitchEvent = {
744
            event: event,
745
            row: row,
746
            refresh: (value: SafeAny) => {
747
                value = value || row;
748
                setTimeout(() => {
749
                    value[column.key] = get(value, column.model);
750
                });
751
            }
752
        };
753
        this.thyOnSwitchChange.emit(switchEvent);
754
    }
755

756
    showExpand(row: SafeAny) {
757
        return row[this.thyChildrenKey] && row[this.thyChildrenKey].length > 0;
758
    }
759

760
    isExpanded(row: SafeAny) {
761
        return this.expandStatusMap[row[this.rowKey]];
762
    }
763

764
    iconIndentComputed(level: number) {
765
        if (this.mode === 'tree') {
766
            return level * this.thyIndent - 5;
767
        }
768
    }
769

770
    tdIndentComputed(level: number, column: SafeAny) {
771
        return {
772
            left: `${column.left}px`,
773
            right: `${column.right}px`,
774
            position: 'relative',
775
            paddingLeft: `${(level + 1) * this.thyIndent - 5}px`
776
        };
777
    }
778

779
    expandChildren(row: SafeAny) {
780
        if (this.isExpanded(row)) {
781
            this.expandStatusMap[row[this.rowKey]] = false;
782
        } else {
783
            this.expandStatusMap[row[this.rowKey]] = true;
784
        }
785
    }
786

787
    onDragGroupStarted(event: CdkDragStart<unknown>) {
788
        this.expandStatusMapOfGroupBeforeDrag = { ...this.expandStatusMapOfGroup };
789
        const groups = this.groups.filter(group => group.expand);
790
        this.foldGroups(groups);
791
        this.onDragStarted(event);
792
        this.cdr.detectChanges();
793
    }
794

795
    onDragGroupEnd(event: CdkDragEnd<unknown>) {
796
        const groups = this.groups.filter(group => this.expandStatusMapOfGroupBeforeDrag[group.id]);
797
        this.expandGroups(groups);
798
        this.cdr.detectChanges();
799
    }
800

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

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

849
    dropListEnterPredicate = (index: number, drag: CdkDrag, drop: CdkDropList) => {
850
        return drop.getSortedItems()[index].data.group_id === drag.data.group_id;
851
    };
852

853
    private onDragModelDropped(event: CdkDragDrop<unknown>) {
854
        const dragEvent: ThyTableDraggableEvent = {
855
            model: event.item,
856
            models: this.model,
857
            oldIndex: event.previousIndex,
858
            newIndex: event.currentIndex
859
        };
860
        moveItemInArray(this.model, event.previousIndex, event.currentIndex);
861
        this.thyOnDraggableChange.emit(dragEvent);
862
    }
863

864
    onDragDropped(event: CdkDragDrop<unknown>) {
865
        if (this.mode === 'group') {
866
            this.onDragGroupDropped(event);
867
        } else if (this.mode === 'list') {
868
            this.onDragModelDropped(event);
869
        }
870
    }
871

872
    onColumnHeaderClick(event: Event, column: ThyTableColumnComponent) {
873
        if (column.sortable) {
874
            const { sortDirection, model, sortChange } = column;
875
            let direction;
876
            if (sortDirection === ThyTableSortDirection.default) {
877
                direction = ThyTableSortDirection.desc;
878
            } else if (sortDirection === ThyTableSortDirection.desc) {
879
                direction = ThyTableSortDirection.asc;
880
            } else {
881
                direction = ThyTableSortDirection.default;
882
            }
883
            column.sortDirection = direction;
884
            const sortEvent = { event, key: model, direction };
885
            sortChange.emit(sortEvent);
886
            this.thySortChange.emit(sortEvent);
887
        }
888
    }
889

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

917
    private onRowClickPropagationEventHandler(event: Event, row: SafeAny): boolean {
918
        if ((event.target as Element).closest('.tree-expand-icon')) {
919
            this.expandChildren(row);
920
            return false;
921
        }
922
        return true;
923
    }
924

925
    public onRowContextMenu(event: Event, row: SafeAny) {
926
        const contextMenuEvent: ThyTableEvent = {
927
            event: event,
928
            row: row
929
        };
930
        this.thyOnRowContextMenu.emit(contextMenuEvent);
931
    }
932

933
    private _refreshColumns() {
934
        const components = this.columns || [];
935
        const _columns = components.map(component => {
936
            return {
937
                width: component.width,
938
                className: component.className
939
            };
940
        });
941

942
        this.columns.forEach((n, i) => {
943
            Object.assign(n, _columns[i]);
944
        });
945
    }
946

947
    private buildGroups(originGroups: SafeAny) {
948
        const originGroupsMap = helpers.keyBy(originGroups, 'id');
949
        this.groups = [];
950
        originGroups.forEach((origin: SafeAny) => {
951
            const group: ThyTableGroup = { id: origin[this.rowKey], children: [], origin };
952

953
            if (this.expandStatusMapOfGroup.hasOwnProperty(group.id)) {
954
                group.expand = this.expandStatusMapOfGroup[group.id];
955
            } else {
956
                group.expand = !!(originGroupsMap[group.id] as SafeAny).expand;
957
            }
958

959
            this.groups.push(group);
960
        });
961
    }
962

963
    private buildModel() {
964
        const groupsMap = keyBy(this.groups, 'id');
965
        this.model.forEach(row => {
966
            const group = groupsMap[row[this.groupBy]];
967
            if (group) {
968
                group.children.push(row);
969
            }
970
        });
971
    }
972

973
    public expandGroup(group: ThyTableGroup) {
974
        group.expand = !group.expand;
975
        this.expandStatusMapOfGroup[group.id] = group.expand;
976
    }
977

978
    private expandGroups(groups: ThyTableGroup[]) {
979
        groups.forEach(group => {
980
            this.expandGroup(group);
981
        });
982
    }
983

984
    private foldGroups(groups: ThyTableGroup[]) {
985
        groups.forEach(group => {
986
            this.expandGroup(group);
987
        });
988
    }
989

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

1013
    ngOnInit() {
1014
        this.updateHostClassService.initializeElement(this.tableElementRef.nativeElement);
1015
        this._setClass(true);
1016
        this.initialized = true;
1017

1018
        merge(this.viewportRuler.change(200), of(null).pipe(delay(200)))
1019
            .pipe(takeUntilDestroyed(this.destroyRef))
1020
            .subscribe(() => {
1021
                this._refreshColumns();
1022
                this.updateScrollClass();
1023
                this.cdr.detectChanges();
1024
            });
1025

1026
        this.ngZone.runOutsideAngular(() => {
1027
            this.scroll$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
1028
                this.updateScrollClass();
1029
            });
1030
        });
1031
    }
1032

1033
    private buildSkeletonColumns() {
1034
        this.skeletonColumns = [];
1035

1036
        this.columns.forEach((column: ThyTableColumnComponent, index: number) => {
1037
            const item = {
1038
                type: this.thyColumnSkeletonTypes[index] || ThyTableColumnSkeletonType.default,
1039
                width: column.width || 'auto'
1040
            };
1041
            this.skeletonColumns = [...this.skeletonColumns, item];
1042
        });
1043
    }
1044

1045
    ngAfterViewInit(): void {
1046
        if (isPlatformServer(this.platformId)) {
1047
            return;
1048
        }
1049

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

1092
    ngOnChanges(simpleChanges: SimpleChanges) {
1093
        const modeChange = simpleChanges.thyMode;
1094
        const thyGroupsChange = simpleChanges.thyGroups;
1095
        const isGroupMode = modeChange && modeChange.currentValue === 'group';
1096
        if (isGroupMode && thyGroupsChange && thyGroupsChange.firstChange) {
1097
            this.buildGroups(thyGroupsChange.currentValue);
1098
            this.buildModel();
1099
        }
1100

1101
        if (this._diff) {
1102
            const changes = this._diff.diff(this.model);
1103
            this._applyDiffChanges(changes);
1104
        }
1105
    }
1106

1107
    ngOnDestroy() {
1108
        this._destroyInvalidAttribute();
1109
    }
1110
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc