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

atinc / ngx-tethys / 13fcf11d-0958-4626-8dcf-f200b8133961

14 Jun 2024 10:13AM UTC coverage: 90.422%. Remained the same
13fcf11d-0958-4626-8dcf-f200b8133961

push

circleci

web-flow
feat: use the ngx-tethys/util's coerceBooleanProperty instead of booleanAttribute #INFR-12648 (#3106)

5467 of 6692 branches covered (81.69%)

Branch coverage included in aggregate %.

117 of 120 new or added lines in 66 files covered. (97.5%)

183 existing lines in 46 files now uncovered.

13216 of 13970 relevant lines covered (94.6%)

985.9 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, NgFor, NgIf, 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
    Inject,
24
    Input,
25
    IterableChangeRecord,
26
    IterableChanges,
27
    IterableDiffer,
1✔
28
    IterableDiffers,
1✔
29
    NgZone,
1✔
30
    numberAttribute,
2✔
31
    OnChanges,
1✔
32
    OnDestroy,
33
    OnInit,
34
    Output,
35
    PLATFORM_ID,
36
    QueryList,
1✔
37
    Renderer2,
38
    SimpleChanges,
39
    TemplateRef,
40
    ViewChild,
41
    ViewChildren,
42
    ViewEncapsulation
1✔
43
} from '@angular/core';
44

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

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

74
export type ThyTableMode = 'list' | 'group' | 'tree';
75

76
export type ThyTableSize = 'md' | 'sm' | 'xs' | 'lg' | 'xlg' | 'default';
67✔
77

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

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

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

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

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

110
const passiveEventListenerOptions = normalizePassiveListenerOptions({ passive: true });
45✔
111

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

70✔
159
    public customType = customType;
70✔
160

70✔
161
    public model: object[] = [];
70✔
162

70✔
163
    public groups: ThyTableGroup[] = [];
70✔
164

70✔
165
    public rowKey = '_id';
70✔
166

70✔
167
    public groupBy: string;
70✔
168

70✔
169
    public mode: ThyTableMode = 'list';
70✔
170

70✔
171
    public theme: ThyTableTheme = 'default';
70✔
172

70✔
173
    public className = '';
70✔
174

70✔
175
    public size: ThyTableSize = 'md';
70✔
176

70✔
177
    public rowClassName: string | Function;
70✔
178

70✔
179
    public loadingDone = true;
70✔
180

70✔
181
    public loadingText: string;
70✔
182

70✔
183
    public emptyOptions: ThyTableEmptyOptions = {};
70✔
184

70✔
185
    public draggable = false;
70✔
186

70✔
187
    public selectedRadioRow: SafeAny = null;
70✔
188

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

70✔
191
    public trackByFn: SafeAny;
192

193
    public wholeRowSelect = false;
194

195
    public fixedDirection = ThyFixedDirection;
70✔
196

70✔
197
    public hasFixed = false;
70✔
198

70✔
199
    public columns: ThyTableColumnComponent[] = [];
70✔
200

70✔
201
    private _diff: IterableDiffer<SafeAny>;
70✔
202

70✔
203
    private initialized = false;
70✔
204

70✔
205
    private _oldThyClassName = '';
206

70✔
207
    private scrollClassName = css.tableScrollLeft;
70✔
208

70✔
209
    private get tableScrollElement(): HTMLElement {
70✔
210
        return this.elementRef.nativeElement.getElementsByClassName(css.tableBody)[0] as HTMLElement;
70✔
211
    }
70✔
UNCOV
212

×
213
    private get scroll$() {
214
        return merge(this.tableScrollElement ? fromEvent<MouseEvent>(this.tableScrollElement, 'scroll') : EMPTY);
70✔
215
    }
216

217
    /**
374✔
218
     * 设置数据为空时展示的模板
65✔
219
     * @type TemplateRef
220
     */
66✔
221
    @ContentChild('empty') emptyTemplate: TemplateRef<SafeAny>;
222

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

66✔
225
    @ViewChildren('rows', { read: ElementRef }) rows: QueryList<ElementRef<HTMLElement>>;
4✔
226

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

731✔
237
    /**
2,097✔
238
     * thyMode的值为 `group` 时分组的 Key
2,097✔
239
     */
240
    @Input()
241
    set thyGroupBy(value: string) {
242
        this.groupBy = value;
243
    }
2,097✔
244

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

254
    /**
255
     * 分组数据源
256
     */
2,132✔
257
    @Input()
356✔
258
    set thyGroups(value: SafeAny) {
259
        if (this.mode === 'group') {
260
            this.buildGroups(value);
261
        }
369✔
262
    }
35✔
263

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

273
        if (this.mode === 'group') {
70✔
274
            this.buildModel();
634!
275
        }
276
    }
277

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

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

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

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

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

UNCOV
319
    /**
×
320
     * 表格的高度
321
     */
322
    @HostBinding('style.height')
323
    @Input()
249!
324
    @InputCssPixel()
249✔
325
    thyHeight: string;
326

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

481
    @Input() thyDragDisabledPredicate: (item: SafeAny) => boolean = () => false;
3✔
482

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

3✔
493
    /**
3✔
494
     * 切换组件回调事件
495
     */
496
    @Output() thyOnSwitchChange: EventEmitter<ThySwitchEvent> = new EventEmitter<ThySwitchEvent>();
497

11✔
498
    /**
11✔
499
     * 表格分页回调事件
4✔
500
     */
2✔
501
    @Output() thyOnPageChange: EventEmitter<PageChangedEvent> = new EventEmitter<PageChangedEvent>();
2✔
502

503
    /**
2!
504
     * 表格分页当前页改变回调事件
2✔
505
     */
1✔
506
    @Output() thyOnPageIndexChange: EventEmitter<number> = new EventEmitter<number>();
1✔
507

1✔
508
    @Output() thyOnPageSizeChange: EventEmitter<number> = new EventEmitter<number>();
509

2✔
510
    /**
1✔
511
     * 多选回调事件
1✔
512
     */
513
    @Output() thyOnMultiSelectChange: EventEmitter<ThyMultiSelectEvent> = new EventEmitter<ThyMultiSelectEvent>();
514

515
    /**
4✔
516
     * 单选回调事件
517
     */
518
    @Output() thyOnRadioSelectChange: EventEmitter<ThyRadioSelectEvent> = new EventEmitter<ThyRadioSelectEvent>();
519

4✔
520
    /**
521
     * 拖动修改事件
522
     */
523
    @Output() thyOnDraggableChange: EventEmitter<ThyTableDraggableEvent> = new EventEmitter<ThyTableDraggableEvent>();
11✔
524

7✔
525
    /**
7✔
526
     * 表格行点击触发事件
527
     */
4✔
528
    @Output() thyOnRowClick: EventEmitter<ThyTableRowEvent> = new EventEmitter<ThyTableRowEvent>();
529

530
    /**
1✔
531
     * 列排序修改事件
532
     */
533
    @Output() thySortChange: EventEmitter<ThyTableSortEvent> = new EventEmitter<ThyTableSortEvent>();
534

1✔
535
    @Output() thyOnRowContextMenu: EventEmitter<ThyTableEvent> = new EventEmitter<ThyTableEvent>();
536

537
    @ContentChild('group', { static: true }) groupTemplate: TemplateRef<SafeAny>;
8!
538

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

14✔
552
    // 数据的折叠展开状态
28✔
553
    public expandStatusMap: Dictionary<boolean> = {};
28!
UNCOV
554

×
555
    public expandStatusMapOfGroup: Dictionary<boolean> = {};
556

557
    private expandStatusMapOfGroupBeforeDrag: Dictionary<boolean> = {};
28✔
558

559
    dragPreviewClass = 'thy-table-drag-preview';
28✔
560

561
    public skeletonColumns: ThyTableSkeletonColumn[] = [];
562

563
    constructor(
13✔
564
        public elementRef: ElementRef,
13✔
565
        private _differs: IterableDiffers,
79✔
566
        private viewportRuler: ViewportRuler,
79✔
567
        private updateHostClassService: UpdateHostClassService,
78✔
568
        @Inject(DOCUMENT) private document: SafeAny,
569
        @Inject(PLATFORM_ID) private platformId: string,
570
        private ngZone: NgZone,
571
        private renderer: Renderer2,
572
        private cdr: ChangeDetectorRef
4✔
573
    ) {
4✔
574
        this._bindTrackFn();
575
    }
576

1✔
UNCOV
577
    private _initializeColumns() {
×
578
        if (!this.columns.some(item => item.expand === true) && this.columns.length > 0) {
579
            this.columns[0].expand = true;
580
        }
581
        this._initializeColumnFixedPositions();
1✔
582
    }
1✔
583

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

×
597
    private _initializeDataModel() {
598
        this.model.forEach(row => {
UNCOV
599
            this.columns.forEach(column => {
×
600
                this._initialSelections(row, column);
601
                this._initialCustomModelValue(row, column);
602
            });
8!
603
        });
8✔
604
    }
605

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

620
    private _initialCustomModelValue(row: object, column: ThyTableColumnComponent) {
66✔
621
        if (column.type === customType.switch) {
66✔
UNCOV
622
            row[column.key] = get(row, column.model);
×
623
        }
624
    }
625

626
    private _refreshCustomModelValue(row: SafeAny) {
627
        this.columns.forEach(column => {
66✔
628
            this._initialCustomModelValue(row, column);
66✔
629
        });
377✔
630
    }
556✔
631

682✔
632
    private _applyDiffChanges(changes: IterableChanges<SafeAny>) {
633
        if (changes) {
377✔
634
            changes.forEachAddedItem((record: IterableChangeRecord<SafeAny>) => {
635
                this._refreshCustomModelValue(record.item);
636
            });
637
        }
66!
UNCOV
638
    }
×
639

640
    private _bindTrackFn() {
66✔
641
        this.trackByFn = function (this: SafeAny, index: number, row: SafeAny): SafeAny {
392✔
642
            return row && this.rowKey ? row[this.rowKey] : index;
643
        }.bind(this);
644
    }
645

646
    private _destroyInvalidAttribute() {
647
        this.model.forEach(row => {
648
            for (const key in row) {
649
                if (key.includes('[$$column]')) {
392!
650
                    delete row[key];
651
                }
×
652
            }
653
        });
654
    }
655

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

78✔
668
        this.updateHostClassService.updateClass(classNames);
78✔
669
    }
12✔
670

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

678
    public isTemplateRef(ref: SafeAny) {
70✔
679
        return ref instanceof TemplateRef;
680
    }
1✔
681

682
    public getModelValue(row: SafeAny, path: string) {
683
        return get(row, path);
684
    }
685

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

697
    public onModelChange(row: SafeAny, column: ThyTableColumnComponent) {
698
        if (column.model) {
699
            set(row, column.model, row[column.key]);
700
        }
701
    }
702

703
    public onStopPropagation(event: Event) {
704
        if (this.wholeRowSelect) {
705
            event.stopPropagation();
706
        }
707
    }
708

709
    public onPageChange(event: PageChangedEvent) {
710
        this.thyOnPageChange.emit(event);
711
    }
712

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

717
    public onPageSizeChange(event: number) {
718
        this.thyOnPageSizeChange.emit(event);
719
    }
720

721
    public onCheckboxChange(row: SafeAny, column: ThyTableColumnComponent) {
722
        this.onModelChange(row, column);
723
        this.onMultiSelectChange(null, row, column);
724
    }
725

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

738
    public onRadioSelectChange(event: Event, row: SafeAny) {
739
        const radioSelectEvent: ThyRadioSelectEvent = {
740
            event: event,
1✔
741
            row: row
742
        };
743
        this.thyOnRadioSelectChange.emit(radioSelectEvent);
744
    }
1✔
745

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

760
    showExpand(row: SafeAny) {
761
        return row[this.thyChildrenKey] && row[this.thyChildrenKey].length > 0;
762
    }
763

764
    isExpanded(row: SafeAny) {
765
        return this.expandStatusMap[row[this.rowKey]];
766
    }
767

768
    iconIndentComputed(level: number) {
769
        if (this.mode === 'tree') {
770
            return level * this.thyIndent - 5;
771
        }
772
    }
773

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

783
    expandChildren(row: SafeAny) {
784
        if (this.isExpanded(row)) {
785
            this.expandStatusMap[row[this.rowKey]] = false;
786
        } else {
787
            this.expandStatusMap[row[this.rowKey]] = true;
788
        }
789
    }
790

791
    onDragGroupStarted(event: CdkDragStart<unknown>) {
792
        this.expandStatusMapOfGroupBeforeDrag = { ...this.expandStatusMapOfGroup };
793
        const groups = this.groups.filter(group => group.expand);
794
        this.foldGroups(groups);
795
        this.onDragStarted(event);
796
        this.cdr.detectChanges();
797
    }
798

799
    onDragGroupEnd(event: CdkDragEnd<unknown>) {
800
        const groups = this.groups.filter(group => this.expandStatusMapOfGroupBeforeDrag[group.id]);
801
        this.expandGroups(groups);
802
        this.cdr.detectChanges();
803
    }
804

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

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

853
    dropListEnterPredicate = (index: number, drag: CdkDrag, drop: CdkDropList) => {
854
        return drop.getSortedItems()[index].data.group_id === drag.data.group_id;
855
    };
856

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

868
    onDragDropped(event: CdkDragDrop<unknown>) {
869
        if (this.mode === 'group') {
870
            this.onDragGroupDropped(event);
871
        } else if (this.mode === 'list') {
872
            this.onDragModelDropped(event);
873
        }
874
    }
875

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

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

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

929
    public onRowContextMenu(event: Event, row: SafeAny) {
930
        const contextMenuEvent: ThyTableEvent = {
931
            event: event,
932
            row: row
933
        };
934
        this.thyOnRowContextMenu.emit(contextMenuEvent);
935
    }
936

937
    private _refreshColumns() {
938
        const components = this.columns || [];
939
        const _columns = components.map(component => {
940
            return {
941
                width: component.width,
942
                className: component.className
943
            };
944
        });
945

946
        this.columns.forEach((n, i) => {
947
            Object.assign(n, _columns[i]);
948
        });
949
    }
950

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

957
            if (this.expandStatusMapOfGroup.hasOwnProperty(group.id)) {
958
                group.expand = this.expandStatusMapOfGroup[group.id];
959
            } else {
960
                group.expand = !!(originGroupsMap[group.id] as SafeAny).expand;
961
            }
962

963
            this.groups.push(group);
964
        });
965
    }
966

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

977
    public expandGroup(group: ThyTableGroup) {
978
        group.expand = !group.expand;
979
        this.expandStatusMapOfGroup[group.id] = group.expand;
980
    }
981

982
    private expandGroups(groups: ThyTableGroup[]) {
983
        groups.forEach(group => {
984
            this.expandGroup(group);
985
        });
986
    }
987

988
    private foldGroups(groups: ThyTableGroup[]) {
989
        groups.forEach(group => {
990
            this.expandGroup(group);
991
        });
992
    }
993

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

1017
    ngOnInit() {
1018
        this.updateHostClassService.initializeElement(this.tableElementRef.nativeElement);
1019
        this._setClass(true);
1020
        this.initialized = true;
1021

1022
        merge(this.viewportRuler.change(200), of(null).pipe(delay(200)))
1023
            .pipe(takeUntilDestroyed(this.destroyRef))
1024
            .subscribe(() => {
1025
                this._refreshColumns();
1026
                this.updateScrollClass();
1027
                this.cdr.detectChanges();
1028
            });
1029

1030
        this.ngZone.runOutsideAngular(() => {
1031
            this.scroll$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
1032
                this.updateScrollClass();
1033
            });
1034
        });
1035
    }
1036

1037
    private buildSkeletonColumns() {
1038
        this.skeletonColumns = [];
1039

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

1049
    ngAfterViewInit(): void {
1050
        if (isPlatformServer(this.platformId)) {
1051
            return;
1052
        }
1053

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

1096
    ngOnChanges(simpleChanges: SimpleChanges) {
1097
        const modeChange = simpleChanges.thyMode;
1098
        const thyGroupsChange = simpleChanges.thyGroups;
1099
        const isGroupMode = modeChange && modeChange.currentValue === 'group';
1100
        if (isGroupMode && thyGroupsChange && thyGroupsChange.firstChange) {
1101
            this.buildGroups(thyGroupsChange.currentValue);
1102
            this.buildModel();
1103
        }
1104

1105
        if (this._diff) {
1106
            const changes = this._diff.diff(this.model);
1107
            this._applyDiffChanges(changes);
1108
        }
1109
    }
1110

1111
    ngOnDestroy() {
1112
        this._destroyInvalidAttribute();
1113
    }
1114
}
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