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

atinc / ngx-tethys / ea558a85-3cd6-4571-bcfc-1e0cc10c7d45

10 Apr 2024 04:59AM UTC coverage: 90.369% (-0.04%) from 90.404%
ea558a85-3cd6-4571-bcfc-1e0cc10c7d45

push

circleci

web-flow
refactor(all): use takeUntilDestroyed instead of mixinUnsubscribe INFR-9529 (#3061)

5411 of 6635 branches covered (81.55%)

Branch coverage included in aggregate %.

45 of 48 new or added lines in 16 files covered. (93.75%)

53 existing lines in 8 files now uncovered.

13149 of 13903 relevant lines covered (94.58%)

981.76 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 { 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
    booleanAttribute,
15
    ChangeDetectorRef,
16
    Component,
17
    ContentChild,
18
    ContentChildren,
19
    DestroyRef,
20
    ElementRef,
21
    EventEmitter,
22
    HostBinding,
23
    inject,
24
    Inject,
25
    Input,
26
    IterableChangeRecord,
27
    IterableChanges,
1✔
28
    IterableDiffer,
1✔
29
    IterableDiffers,
1✔
30
    NgZone,
2✔
31
    numberAttribute,
1✔
32
    OnChanges,
33
    OnDestroy,
34
    OnInit,
35
    Output,
36
    PLATFORM_ID,
1✔
37
    QueryList,
38
    Renderer2,
39
    SimpleChanges,
40
    TemplateRef,
41
    ViewChild,
42
    ViewChildren,
1✔
43
    ViewEncapsulation
44
} from '@angular/core';
45

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

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

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

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

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

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

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

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

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

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

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

70✔
160
    public customType = customType;
70✔
161

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

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

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

70✔
168
    public groupBy: string;
70✔
169

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

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

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

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

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

70✔
180
    public loadingDone = true;
70✔
181

70✔
182
    public loadingText: string;
70✔
183

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

70✔
186
    public draggable = false;
70✔
187

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

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

192
    public trackByFn: SafeAny;
193

194
    public wholeRowSelect = false;
195

70✔
196
    public fixedDirection = ThyFixedDirection;
70✔
197

70✔
198
    public hasFixed = false;
70✔
199

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

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

70✔
204
    private initialized = false;
70✔
205

206
    private _oldThyClassName = '';
70✔
207

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

482
    @Input() thyDragDisabledPredicate: (item: SafeAny) => boolean = () => false;
2✔
483

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

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

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

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

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

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

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

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

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

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

8!
536
    @Output() thyOnRowContextMenu: EventEmitter<ThyTableEvent> = new EventEmitter<ThyTableEvent>();
8✔
537

47✔
538
    @ContentChild('group', { static: true }) groupTemplate: TemplateRef<SafeAny>;
539

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

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

28✔
556
    public expandStatusMapOfGroup: Dictionary<boolean> = {};
557

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

560
    dragPreviewClass = 'thy-table-drag-preview';
561

13✔
562
    public skeletonColumns: ThyTableSkeletonColumn[] = [];
13✔
563

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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