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

atinc / ngx-tethys / 8d4ea048-b423-4541-9c63-9cb4f297bb0c

04 Mar 2024 02:09AM UTC coverage: 90.58% (-0.02%) from 90.604%
8d4ea048-b423-4541-9c63-9cb4f297bb0c

Pull #3022

circleci

why520crazy
build: update @tethys/cdk to 17.0.0-next.2 for peerDependencies
Pull Request #3022: feat: upgrade ng to 17 #INFR-11427 (#3021)

5422 of 6642 branches covered (81.63%)

Branch coverage included in aggregate %.

328 of 338 new or added lines in 193 files covered. (97.04%)

141 existing lines in 29 files now uncovered.

13502 of 14250 relevant lines covered (94.75%)

981.99 hits per line

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

88.48
/src/table/table.component.ts
1
import {
2
    Constructor,
3
    InputBoolean,
4
    InputCssPixel,
5
    InputNumber,
6
    MixinBase,
7
    mixinUnsubscribe,
8
    ThyUnsubscribe,
9
    UpdateHostClassService
10
} from 'ngx-tethys/core';
11
import { Dictionary, SafeAny } from 'ngx-tethys/types';
12
import { coerceBooleanProperty, get, helpers, isString, keyBy, set } from 'ngx-tethys/util';
13
import { EMPTY, fromEvent, merge, Observable, of } from 'rxjs';
14
import { delay, startWith, switchMap, takeUntil } from 'rxjs/operators';
15

16
import { CdkDrag, CdkDragDrop, CdkDragEnd, CdkDragStart, CdkDropList, moveItemInArray } from '@angular/cdk/drag-drop';
17
import { ViewportRuler } from '@angular/cdk/overlay';
18
import { normalizePassiveListenerOptions } from '@angular/cdk/platform';
19
import { DOCUMENT, isPlatformServer, NgClass, NgFor, NgIf, NgTemplateOutlet, NgStyle } from '@angular/common';
20
import {
21
    AfterViewInit,
22
    ChangeDetectorRef,
23
    Component,
24
    ContentChild,
25
    ContentChildren,
26
    ElementRef,
1✔
27
    EventEmitter,
1✔
28
    HostBinding,
1✔
29
    Inject,
2✔
30
    Input,
1✔
31
    IterableChangeRecord,
32
    IterableChanges,
33
    IterableDiffer,
34
    IterableDiffers,
35
    NgZone,
1✔
36
    OnChanges,
37
    OnDestroy,
38
    OnInit,
39
    Output,
40
    PLATFORM_ID,
41
    QueryList,
1✔
42
    Renderer2,
43
    SimpleChanges,
44
    TemplateRef,
45
    ViewChild,
46
    ViewChildren,
47
    ViewEncapsulation
1✔
48
} from '@angular/core';
1✔
49

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

67✔
77
export type ThyTableTheme = 'default' | 'bordered' | 'boxed';
67✔
78

67✔
79
export type ThyTableMode = 'list' | 'group' | 'tree';
67✔
80

1✔
81
export type ThyTableSize = 'md' | 'sm' | 'xs' | 'lg' | 'xlg' | 'default';
82

83
export enum ThyFixedDirection {
84
    left = 'left',
45!
85
    right = 'right'
45✔
86
}
87

88
interface ThyTableGroup<T = unknown> {
50✔
89
    id?: string;
50✔
90
    expand?: boolean;
91
    children?: object[];
92
    origin?: T;
48✔
93
}
45✔
94

45!
UNCOV
95
const tableThemeMap = {
×
96
    default: 'table-default',
97
    bordered: 'table-bordered',
98
    boxed: 'table-boxed'
45✔
99
};
100

45✔
101
const customType = {
45✔
102
    index: 'index',
103
    checkbox: 'checkbox',
104
    radio: 'radio',
45✔
105
    switch: 'switch'
106
};
107

45✔
108
const css = {
109
    tableBody: 'thy-table-body',
110
    tableScrollLeft: 'thy-table-scroll-left',
45✔
111
    tableScrollRight: 'thy-table-scroll-right',
112
    tableScrollMiddle: 'thy-table-scroll-middle'
113
};
44✔
114

115
const passiveEventListenerOptions = normalizePassiveListenerOptions({ passive: true });
116

64✔
117
const _MixinBase: Constructor<ThyUnsubscribe> & typeof MixinBase = mixinUnsubscribe(MixinBase);
64✔
118

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

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

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

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

70✔
172
    public groupBy: string;
70✔
173

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

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

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

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

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

70✔
184
    public loadingDone = true;
70✔
185

70✔
186
    public loadingText: string;
70✔
187

70✔
188
    public emptyOptions: ThyTableEmptyOptions = {};
70✔
189

384✔
190
    public draggable = false;
70✔
191

192
    public selectedRadioRow: SafeAny = null;
193

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

70✔
196
    public trackByFn: SafeAny;
70✔
197

70✔
198
    public wholeRowSelect = false;
70✔
199

70✔
200
    public fixedDirection = ThyFixedDirection;
70✔
201

70✔
202
    public hasFixed = false;
70✔
203

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

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

70✔
208
    private initialized = false;
70✔
209

70✔
210
    private _oldThyClassName = '';
70✔
211

70✔
UNCOV
212
    private scrollClassName = css.tableScrollLeft;
×
213

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

367
    /**
UNCOV
368
     * 设置加载时显示的文本,已废弃
×
369
     * @deprecated
×
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()
180✔
389
    @InputBoolean()
390
    set thyDraggable(value: boolean) {
391
        this.draggable = coerceBooleanProperty(value);
392
        if ((typeof ngDevMode === 'undefined' || ngDevMode) && this.draggable && this.mode === 'tree') {
393
            throw new Error('Tree mode sorting is not supported');
394
        }
7✔
395
    }
3✔
396

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

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

417
    /**
2✔
418
     * 设置总页数
419
     */
1✔
420
    @Input()
421
    @InputNumber()
422
    set thyPageTotal(value: number) {
423
        this.pagination.total = value;
424
    }
425

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

439
    /**
440
     * 是否显示表格头
441
     * @default false
442
     */
1✔
443
    @Input() @InputBoolean() thyHeadless = false;
1✔
444

445
    /**
446
     * 是否显示表格头,已废弃,请使用 thyHeadless
447
     * @deprecated please use thyHeadless
3✔
448
     */
3✔
449
    @Input()
3✔
450
    @InputBoolean()
3!
451
    set thyShowHeader(value: boolean) {
3✔
452
        this.thyHeadless = !value;
12✔
453
    }
454

455
    /**
456
     * 是否显示左侧 Total
457
     */
458
    @Input('thyShowTotal') @InputBoolean() showTotal = false;
1✔
459

460
    /**
461
     * 是否显示调整每页显示条数下拉框
462
     */
463
    @Input('thyShowSizeChanger') @InputBoolean() showSizeChanger = false;
464

1✔
465
    /**
1✔
466
     * 每页显示条数下拉框可选项
467
     * @type number[]
468
     */
3✔
469
    @Input('thyPageSizeOptions')
2✔
470
    set pageSizeOptions(value: number[]) {
471
        this.pagination.sizeOptions = value;
1!
472
    }
1✔
473

474
    /**
475
     * thyMode 为 tree 时,设置 Tree 树状数据展示时的缩进
476
     */
3!
477
    @Input() @InputNumber() thyIndent = 20;
3✔
478

479
    /**
3✔
480
     * thyMode 为 tree 时,设置 Tree 树状数据对象中的子节点 Key
1✔
481
     * @type string
482
     */
2✔
483
    @Input() thyChildrenKey = 'children';
1✔
484

485
    /**
486
     * 开启 Hover 后显示操作,默认不显示操作区内容,鼠标 Hover 时展示
1✔
487
     * @default false
488
     */
3✔
489
    @HostBinding('class.thy-table-hover-display-operation')
3✔
490
    @Input()
3✔
491
    @InputBoolean()
3✔
492
    thyHoverDisplayOperation: boolean;
493

494
    @Input() thyDragDisabledPredicate: (item: SafeAny) => boolean = () => false;
495

11✔
496
    /**
11✔
497
     * 表格列的骨架类型
4✔
498
     * @type ThyTableColumnSkeletonType[]
2✔
499
     */
2✔
500
    @Input() thyColumnSkeletonTypes: ThyTableColumnSkeletonType[] = [
501
        ThyTableColumnSkeletonType.title,
2!
502
        ThyTableColumnSkeletonType.member,
2✔
503
        ThyTableColumnSkeletonType.default
1✔
504
    ];
1✔
505

1✔
506
    /**
507
     * 切换组件回调事件
2✔
508
     */
1✔
509
    @Output() thyOnSwitchChange: EventEmitter<ThySwitchEvent> = new EventEmitter<ThySwitchEvent>();
1✔
510

511
    /**
512
     * 表格分页回调事件
513
     */
4✔
514
    @Output() thyOnPageChange: EventEmitter<PageChangedEvent> = new EventEmitter<PageChangedEvent>();
515

516
    /**
517
     * 表格分页当前页改变回调事件
4✔
518
     */
519
    @Output() thyOnPageIndexChange: EventEmitter<number> = new EventEmitter<number>();
520

521
    @Output() thyOnPageSizeChange: EventEmitter<number> = new EventEmitter<number>();
11✔
522

7✔
523
    /**
7✔
524
     * 多选回调事件
525
     */
4✔
526
    @Output() thyOnMultiSelectChange: EventEmitter<ThyMultiSelectEvent> = new EventEmitter<ThyMultiSelectEvent>();
527

528
    /**
1✔
529
     * 单选回调事件
530
     */
531
    @Output() thyOnRadioSelectChange: EventEmitter<ThyRadioSelectEvent> = new EventEmitter<ThyRadioSelectEvent>();
532

1✔
533
    /**
534
     * 拖动修改事件
535
     */
8!
536
    @Output() thyOnDraggableChange: EventEmitter<ThyTableDraggableEvent> = new EventEmitter<ThyTableDraggableEvent>();
8✔
537

47✔
538
    /**
539
     * 表格行点击触发事件
540
     */
541
    @Output() thyOnRowClick: EventEmitter<ThyTableRowEvent> = new EventEmitter<ThyTableRowEvent>();
542

8✔
543
    /**
47✔
544
     * 列排序修改事件
545
     */
546
    @Output() thySortChange: EventEmitter<ThyTableSortEvent> = new EventEmitter<ThyTableSortEvent>();
547

14✔
548
    @Output() thyOnRowContextMenu: EventEmitter<ThyTableEvent> = new EventEmitter<ThyTableEvent>();
14✔
549

14✔
550
    @ContentChild('group', { static: true }) groupTemplate: TemplateRef<SafeAny>;
28✔
551

28!
UNCOV
552
    @ContentChildren(ThyTableColumnComponent)
×
553
    set listOfColumnComponents(components: QueryList<ThyTableColumnComponent>) {
554
        if (components) {
555
            this.columns = components.toArray();
28✔
556
            this.hasFixed = !!this.columns.find(item => {
557
                return item.fixed === this.fixedDirection.left || item.fixed === this.fixedDirection.right;
28✔
558
            });
559
            this.buildSkeletonColumns();
560
            this._initializeColumns();
561
            this._initializeDataModel();
13✔
562
        }
13✔
563
    }
79✔
564

79✔
565
    // 数据的折叠展开状态
78✔
566
    public expandStatusMap: Dictionary<boolean> = {};
567

568
    public expandStatusMapOfGroup: Dictionary<boolean> = {};
569

570
    private expandStatusMapOfGroupBeforeDrag: Dictionary<boolean> = {};
4✔
571

4✔
572
    dragPreviewClass = 'thy-table-drag-preview';
573

574
    public skeletonColumns: ThyTableSkeletonColumn[] = [];
1✔
UNCOV
575

×
576
    constructor(
577
        public elementRef: ElementRef,
578
        private _differs: IterableDiffers,
579
        private viewportRuler: ViewportRuler,
1✔
580
        private updateHostClassService: UpdateHostClassService,
1✔
581
        @Inject(DOCUMENT) private document: SafeAny,
582
        @Inject(PLATFORM_ID) private platformId: string,
583
        private ngZone: NgZone,
584
        private renderer: Renderer2,
8✔
585
        private cdr: ChangeDetectorRef
8✔
586
    ) {
8✔
587
        super();
8✔
588
        this._bindTrackFn();
8✔
589
    }
8!
UNCOV
590

×
591
    private _initializeColumns() {
×
592
        if (!this.columns.some(item => item.expand === true) && this.columns.length > 0) {
UNCOV
593
            this.columns[0].expand = true;
×
594
        }
×
595
        this._initializeColumnFixedPositions();
596
    }
UNCOV
597

×
598
    private _initializeColumnFixedPositions() {
599
        const leftFixedColumns = this.columns.filter(item => item.fixed === ThyFixedDirection.left);
600
        leftFixedColumns.forEach((item, index) => {
8!
601
            const previous = leftFixedColumns[index - 1];
8✔
602
            item.left = previous ? previous.left + parseInt(previous.width.toString(), 10) : 0;
603
        });
8!
UNCOV
604
        const rightFixedColumns = this.columns.filter(item => item.fixed === ThyFixedDirection.right).reverse();
×
605
        rightFixedColumns.forEach((item, index) => {
606
            const previous = rightFixedColumns[index - 1];
607
            item.right = previous ? previous.right + parseInt(previous.width.toString(), 10) : 0;
608
        });
66✔
609
    }
66✔
610

66✔
611
    private _initializeDataModel() {
66✔
612
        this.model.forEach(row => {
613
            this.columns.forEach(column => {
614
                this._initialSelections(row, column);
8✔
615
                this._initialCustomModelValue(row, column);
8✔
616
            });
8✔
617
        });
618
    }
66✔
619

66✔
UNCOV
620
    private _initialSelections(row: object, column: ThyTableColumnComponent) {
×
621
        if (column.selections) {
622
            if (column.type === 'checkbox') {
623
                row[column.key] = column.selections.includes(row[this.rowKey]);
624
                this.onModelChange(row, column);
625
            }
66✔
626
            if (column.type === 'radio') {
66✔
627
                if (column.selections.includes(row[this.rowKey])) {
377✔
628
                    this.selectedRadioRow = row;
556✔
629
                }
682✔
630
            }
631
        }
377✔
632
    }
633

634
    private _initialCustomModelValue(row: object, column: ThyTableColumnComponent) {
635
        if (column.type === customType.switch) {
66!
UNCOV
636
            row[column.key] = get(row, column.model);
×
637
        }
638
    }
66✔
639

392✔
640
    private _refreshCustomModelValue(row: SafeAny) {
641
        this.columns.forEach(column => {
642
            this._initialCustomModelValue(row, column);
643
        });
644
    }
645

646
    private _applyDiffChanges(changes: IterableChanges<SafeAny>) {
647
        if (changes) {
392!
648
            changes.forEachAddedItem((record: IterableChangeRecord<SafeAny>) => {
649
                this._refreshCustomModelValue(record.item);
×
650
            });
651
        }
652
    }
653

654
    private _bindTrackFn() {
655
        this.trackByFn = function (this: SafeAny, index: number, row: SafeAny): SafeAny {
656
            return row && this.rowKey ? row[this.rowKey] : index;
657
        }.bind(this);
1!
658
    }
1✔
659

660
    private _destroyInvalidAttribute() {
661
        this.model.forEach(row => {
662
            for (const key in row) {
663
                if (key.includes('[$$column]')) {
78✔
664
                    delete row[key];
78✔
665
                }
78✔
666
            }
78✔
667
        });
12✔
668
    }
12✔
669

670
    private _setClass(first = false) {
78!
671
        if (!first && !this.initialized) {
78✔
672
            return;
78✔
673
        }
674
        const classNames: string[] = [];
675
        if (this.size) {
676
            classNames.push(`table-${this.size}`);
70✔
677
        }
70✔
678
        if (tableThemeMap[this.theme]) {
679
            classNames.push(tableThemeMap[this.theme]);
1✔
680
        }
681

682
        this.updateHostClassService.updateClass(classNames);
683
    }
684

685
    public updateColumnSelections(key: string, selections: SafeAny): void {
686
        const column = this.columns.find(item => item.key === key);
687
        this.model.forEach(row => {
688
            this._initialSelections(row, column);
689
        });
690
    }
1✔
691

692
    public isTemplateRef(ref: SafeAny) {
693
        return ref instanceof TemplateRef;
694
    }
695

696
    public getModelValue(row: SafeAny, path: string) {
697
        return get(row, path);
698
    }
699

700
    public renderRowClassName(row: SafeAny, index: number) {
701
        if (!this.rowClassName) {
702
            return null;
703
        }
704
        if (isString(this.rowClassName)) {
705
            return this.rowClassName;
706
        } else {
707
            return (this.rowClassName as Function)(row, index);
708
        }
709
    }
710

711
    public onModelChange(row: SafeAny, column: ThyTableColumnComponent) {
712
        if (column.model) {
713
            set(row, column.model, row[column.key]);
714
        }
715
    }
716

717
    public onStopPropagation(event: Event) {
718
        if (this.wholeRowSelect) {
719
            event.stopPropagation();
720
        }
721
    }
722

723
    public onPageChange(event: PageChangedEvent) {
724
        this.thyOnPageChange.emit(event);
725
    }
726

727
    public onPageIndexChange(event: number) {
728
        this.thyOnPageIndexChange.emit(event);
729
    }
730

731
    public onPageSizeChange(event: number) {
732
        this.thyOnPageSizeChange.emit(event);
733
    }
734

735
    public onCheckboxChange(row: SafeAny, column: ThyTableColumnComponent) {
736
        this.onModelChange(row, column);
737
        this.onMultiSelectChange(null, row, column);
738
    }
739

1✔
740
    public onMultiSelectChange(event: Event, row: SafeAny, column: ThyTableColumnComponent) {
741
        const rows = this.model.filter(item => {
742
            return item[column.key];
743
        });
1✔
744
        const multiSelectEvent: ThyMultiSelectEvent = {
745
            event: event,
746
            row: row,
747
            rows: rows
1✔
748
        };
749
        this.thyOnMultiSelectChange.emit(multiSelectEvent);
750
    }
751

1✔
752
    public onRadioSelectChange(event: Event, row: SafeAny) {
753
        const radioSelectEvent: ThyRadioSelectEvent = {
754
            event: event,
755
            row: row
1✔
756
        };
757
        this.thyOnRadioSelectChange.emit(radioSelectEvent);
758
    }
759

760
    public onSwitchChange(event: Event, row: SafeAny, column: SafeAny) {
1✔
761
        const switchEvent: ThySwitchEvent = {
762
            event: event,
763
            row: row,
764
            refresh: (value: SafeAny) => {
765
                value = value || row;
1✔
766
                setTimeout(() => {
767
                    value[column.key] = get(value, column.model);
768
                });
769
            }
770
        };
1✔
771
        this.thyOnSwitchChange.emit(switchEvent);
772
    }
773

774
    showExpand(row: SafeAny) {
775
        return row[this.thyChildrenKey] && row[this.thyChildrenKey].length > 0;
1✔
776
    }
777

778
    isExpanded(row: SafeAny) {
779
        return this.expandStatusMap[row[this.rowKey]];
780
    }
1✔
781

782
    iconIndentComputed(level: number) {
783
        if (this.mode === 'tree') {
784
            return level * this.thyIndent - 5;
785
        }
1✔
786
    }
787

788
    tdIndentComputed(level: number) {
789
        return {
1✔
790
            position: 'relative',
791
            paddingLeft: `${(level + 1) * this.thyIndent - 5}px`
792
        };
793
    }
794

1✔
795
    expandChildren(row: SafeAny) {
796
        if (this.isExpanded(row)) {
797
            this.expandStatusMap[row[this.rowKey]] = false;
798
        } else {
1✔
799
            this.expandStatusMap[row[this.rowKey]] = true;
800
        }
801
    }
802

1✔
803
    onDragGroupStarted(event: CdkDragStart<unknown>) {
804
        this.expandStatusMapOfGroupBeforeDrag = { ...this.expandStatusMapOfGroup };
805
        const groups = this.groups.filter(group => group.expand);
806
        this.foldGroups(groups);
1✔
807
        this.onDragStarted(event);
808
        this.cdr.detectChanges();
809
    }
810

1✔
811
    onDragGroupEnd(event: CdkDragEnd<unknown>) {
812
        const groups = this.groups.filter(group => this.expandStatusMapOfGroupBeforeDrag[group.id]);
813
        this.expandGroups(groups);
814
        this.cdr.detectChanges();
815
    }
816

817
    private onDragGroupDropped(event: CdkDragDrop<unknown>) {
818
        const group = this.groups.find(group => {
819
            return event.item.data.id === group.id;
820
        });
821
        if (group) {
822
            // drag group
823
            const dragEvent: ThyTableDraggableEvent = {
824
                model: event.item,
825
                models: this.groups,
826
                oldIndex: event.previousIndex,
827
                newIndex: event.currentIndex
828
            };
829
            moveItemInArray(this.groups, event.previousIndex, event.currentIndex);
830
            this.thyOnDraggableChange.emit(dragEvent);
831
        } else {
832
            // drag group children
833
            const group = this.groups.find(group => {
834
                return event.item.data[this.groupBy] === group.id;
835
            });
836
            const groupIndex =
837
                event.container.getSortedItems().findIndex(item => {
838
                    return item.data.id === event.item.data[this.groupBy];
839
                }) + 1;
840
            const dragEvent: ThyTableDraggableEvent = {
841
                model: event.item,
842
                models: group.children,
843
                oldIndex: event.previousIndex - groupIndex,
844
                newIndex: event.currentIndex - groupIndex
845
            };
846
            moveItemInArray(group.children, dragEvent.oldIndex, dragEvent.newIndex);
847
            this.thyOnDraggableChange.emit(dragEvent);
848
        }
849
    }
850

851
    onDragStarted(event: CdkDragStart<unknown>) {
852
        this.ngZone.runOutsideAngular(() =>
853
            setTimeout(() => {
854
                const preview = this.document.getElementsByClassName(this.dragPreviewClass)[0];
855
                const originalTds: HTMLCollection = event.source._dragRef.getPlaceholderElement()?.children;
856
                if (preview) {
857
                    Array.from(preview?.children).forEach((element: HTMLElement, index: number) => {
858
                        element.style.width = `${originalTds[index]?.clientWidth}px`;
859
                    });
860
                }
861
            })
862
        );
863
    }
864

865
    dropListEnterPredicate = (index: number, drag: CdkDrag, drop: CdkDropList) => {
866
        return drop.getSortedItems()[index].data.group_id === drag.data.group_id;
867
    };
868

869
    private onDragModelDropped(event: CdkDragDrop<unknown>) {
870
        const dragEvent: ThyTableDraggableEvent = {
871
            model: event.item,
872
            models: this.model,
873
            oldIndex: event.previousIndex,
874
            newIndex: event.currentIndex
875
        };
876
        moveItemInArray(this.model, event.previousIndex, event.currentIndex);
877
        this.thyOnDraggableChange.emit(dragEvent);
878
    }
879

880
    onDragDropped(event: CdkDragDrop<unknown>) {
881
        if (this.mode === 'group') {
882
            this.onDragGroupDropped(event);
883
        } else if (this.mode === 'list') {
884
            this.onDragModelDropped(event);
885
        }
886
    }
887

888
    onColumnHeaderClick(event: Event, column: ThyTableColumnComponent) {
889
        if (column.sortable) {
890
            const { sortDirection, model, sortChange } = column;
891
            let direction;
892
            if (sortDirection === ThyTableSortDirection.default) {
893
                direction = ThyTableSortDirection.desc;
894
            } else if (sortDirection === ThyTableSortDirection.desc) {
895
                direction = ThyTableSortDirection.asc;
896
            } else {
897
                direction = ThyTableSortDirection.default;
898
            }
899
            column.sortDirection = direction;
900
            const sortEvent = { event, key: model, direction };
901
            sortChange.emit(sortEvent);
902
            this.thySortChange.emit(sortEvent);
903
        }
904
    }
905

906
    public onRowClick(event: Event, row: SafeAny) {
907
        const next = this.onRowClickPropagationEventHandler(event, row);
908
        if (next) {
909
            if (this.wholeRowSelect) {
910
                const column = this.columns.find(item => {
911
                    return item.type === customType.checkbox || item.type === customType.radio;
912
                });
913
                if (column && !column.disabled) {
914
                    if (column.type === customType.checkbox) {
915
                        row[column.key] = !row[column.key];
916
                        this.onModelChange(row, column);
917
                        this.onMultiSelectChange(event, row, column);
918
                    }
919
                    if (column.type === customType.radio) {
920
                        this.selectedRadioRow = row;
921
                        this.onRadioSelectChange(event, row);
922
                    }
923
                }
924
            }
925
            const rowEvent = {
926
                event: event,
927
                row: row
928
            };
929
            this.thyOnRowClick.emit(rowEvent);
930
        }
931
    }
932

933
    private onRowClickPropagationEventHandler(event: Event, row: SafeAny): boolean {
934
        if ((event.target as Element).closest('.tree-expand-icon')) {
935
            this.expandChildren(row);
936
            return false;
937
        }
938
        return true;
939
    }
940

941
    public onRowContextMenu(event: Event, row: SafeAny) {
942
        const contextMenuEvent: ThyTableEvent = {
943
            event: event,
944
            row: row
945
        };
946
        this.thyOnRowContextMenu.emit(contextMenuEvent);
947
    }
948

949
    private _refreshColumns() {
950
        const components = this.columns || [];
951
        const _columns = components.map(component => {
952
            return {
953
                width: component.width,
954
                className: component.className
955
            };
956
        });
957

958
        this.columns.forEach((n, i) => {
959
            Object.assign(n, _columns[i]);
960
        });
961
    }
962

963
    private buildGroups(originGroups: SafeAny) {
964
        const originGroupsMap = helpers.keyBy(originGroups, 'id');
965
        this.groups = [];
966
        originGroups.forEach((origin: SafeAny) => {
967
            const group: ThyTableGroup = { id: origin[this.rowKey], children: [], origin };
968

969
            if (this.expandStatusMapOfGroup.hasOwnProperty(group.id)) {
970
                group.expand = this.expandStatusMapOfGroup[group.id];
971
            } else {
972
                group.expand = !!(originGroupsMap[group.id] as SafeAny).expand;
973
            }
974

975
            this.groups.push(group);
976
        });
977
    }
978

979
    private buildModel() {
980
        const groupsMap = keyBy(this.groups, 'id');
981
        this.model.forEach(row => {
982
            const group = groupsMap[row[this.groupBy]];
983
            if (group) {
984
                group.children.push(row);
985
            }
986
        });
987
    }
988

989
    public expandGroup(group: ThyTableGroup) {
990
        group.expand = !group.expand;
991
        this.expandStatusMapOfGroup[group.id] = group.expand;
992
    }
993

994
    private expandGroups(groups: ThyTableGroup[]) {
995
        groups.forEach(group => {
996
            this.expandGroup(group);
997
        });
998
    }
999

1000
    private foldGroups(groups: ThyTableGroup[]) {
1001
        groups.forEach(group => {
1002
            this.expandGroup(group);
1003
        });
1004
    }
1005

1006
    private updateScrollClass() {
1007
        const scrollElement = this.tableScrollElement;
1008
        const maxScrollLeft = scrollElement.scrollWidth - scrollElement.offsetWidth;
1009
        const scrollX = scrollElement.scrollLeft;
1010
        const lastScrollClassName = this.scrollClassName;
1011
        this.scrollClassName = '';
1012
        if (scrollElement.scrollWidth > scrollElement.clientWidth) {
1013
            if (scrollX >= maxScrollLeft) {
1014
                this.scrollClassName = css.tableScrollRight;
1015
            } else if (scrollX === 0) {
1016
                this.scrollClassName = css.tableScrollLeft;
1017
            } else {
1018
                this.scrollClassName = css.tableScrollMiddle;
1019
            }
1020
        }
1021
        if (lastScrollClassName) {
1022
            this.renderer.removeClass(this.tableScrollElement, lastScrollClassName);
1023
        }
1024
        if (this.scrollClassName) {
1025
            this.renderer.addClass(this.tableScrollElement, this.scrollClassName);
1026
        }
1027
    }
1028

1029
    ngOnInit() {
1030
        this.updateHostClassService.initializeElement(this.tableElementRef.nativeElement);
1031
        this._setClass(true);
1032
        this.initialized = true;
1033

1034
        merge(this.viewportRuler.change(200), of(null).pipe(delay(200)))
1035
            .pipe(takeUntil(this.ngUnsubscribe$))
1036
            .subscribe(() => {
1037
                this._refreshColumns();
1038
                this.updateScrollClass();
1039
                this.cdr.detectChanges();
1040
            });
1041

1042
        this.ngZone.runOutsideAngular(() => {
1043
            this.scroll$.pipe(takeUntil(this.ngUnsubscribe$)).subscribe(() => {
1044
                this.updateScrollClass();
1045
            });
1046
        });
1047
    }
1048

1049
    private buildSkeletonColumns() {
1050
        this.skeletonColumns = [];
1051

1052
        this.columns.forEach((column: ThyTableColumnComponent, index: number) => {
1053
            const item = {
1054
                type: this.thyColumnSkeletonTypes[index] || ThyTableColumnSkeletonType.default,
1055
                width: column.width || 'auto'
1056
            };
1057
            this.skeletonColumns = [...this.skeletonColumns, item];
1058
        });
1059
    }
1060

1061
    ngAfterViewInit(): void {
1062
        if (isPlatformServer(this.platformId)) {
1063
            return;
1064
        }
1065

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

1108
    ngOnChanges(simpleChanges: SimpleChanges) {
1109
        const modeChange = simpleChanges.thyMode;
1110
        const thyGroupsChange = simpleChanges.thyGroups;
1111
        const isGroupMode = modeChange && modeChange.currentValue === 'group';
1112
        if (isGroupMode && thyGroupsChange && thyGroupsChange.firstChange) {
1113
            this.buildGroups(thyGroupsChange.currentValue);
1114
            this.buildModel();
1115
        }
1116

1117
        if (this._diff) {
1118
            const changes = this._diff.diff(this.model);
1119
            this._applyDiffChanges(changes);
1120
        }
1121
    }
1122

1123
    ngOnDestroy() {
1124
        super.ngOnDestroy();
1125
        this._destroyInvalidAttribute();
1126
    }
1127
}
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

© 2025 Coveralls, Inc