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

atinc / ngx-tethys / 2c11b3d7-fd65-411c-a4e3-dc774f8b7c23

02 Apr 2024 08:08AM UTC coverage: 90.55% (-0.04%) from 90.585%
2c11b3d7-fd65-411c-a4e3-dc774f8b7c23

Pull #3061

circleci

minlovehua
refactor(all): use takeUntilDestroyed instead of mixinUnsubscribe INFR-9529
Pull Request #3061: refactor(all): use takeUntilDestroyed instead of mixinUnsubscribe INFR-9529

5417 of 6635 branches covered (81.64%)

Branch coverage included in aggregate %.

52 of 55 new or added lines in 16 files covered. (94.55%)

54 existing lines in 9 files now uncovered.

13460 of 14212 relevant lines covered (94.71%)

979.65 hits per line

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

88.44
/src/table/table.component.ts
1
import { InputBoolean, InputCssPixel, InputNumber, 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
    OnChanges,
2✔
31
    OnDestroy,
1✔
32
    OnInit,
33
    Output,
34
    PLATFORM_ID,
35
    QueryList,
36
    Renderer2,
1✔
37
    SimpleChanges,
38
    TemplateRef,
39
    ViewChild,
40
    ViewChildren,
41
    ViewEncapsulation
42
} from '@angular/core';
1✔
43

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

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

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

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

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

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

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

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

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

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

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

70✔
158
    public customType = customType;
70✔
159

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

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

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

70✔
166
    public groupBy: string;
70✔
167

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

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

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

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

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

70✔
178
    public loadingDone = true;
70✔
179

70✔
180
    public loadingText: string;
70✔
181

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

70✔
184
    public draggable = false;
70✔
185

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

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

384✔
190
    public trackByFn: SafeAny;
70✔
191

192
    public wholeRowSelect = false;
193

194
    public fixedDirection = ThyFixedDirection;
195

70✔
196
    public hasFixed = false;
70✔
197

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

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

70✔
202
    private initialized = false;
70✔
203

70✔
204
    private _oldThyClassName = '';
70✔
205

206
    private scrollClassName = css.tableScrollLeft;
70✔
207

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

351
    /**
352
     * 设置加载状态
353
     * @default true
354
     */
2✔
355
    @Input()
356
    @InputBoolean()
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;
UNCOV
368
    }
×
UNCOV
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()
383
    @InputBoolean()
180!
384
    set thyDraggable(value: boolean) {
180✔
385
        this.draggable = coerceBooleanProperty(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()
3✔
396
    @InputNumber()
397
    set thyPageIndex(value: number) {
398
        this.pagination.index = value;
4✔
399
    }
400

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

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

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

433
    /**
1✔
434
     * 是否显示表格头
2✔
435
     * @default false
436
     */
1✔
437
    @Input() @InputBoolean() thyHeadless = false;
438

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

3✔
449
    /**
3✔
450
     * 是否显示左侧 Total
3!
451
     */
3✔
452
    @Input('thyShowTotal') @InputBoolean() showTotal = false;
12✔
453

454
    /**
455
     * 是否显示调整每页显示条数下拉框
456
     */
457
    @Input('thyShowSizeChanger') @InputBoolean() showSizeChanger = false;
458

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

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

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

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

488
    @Input() thyDragDisabledPredicate: (item: SafeAny) => boolean = () => false;
3✔
489

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

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

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

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

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

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

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

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

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

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

542
    @Output() thyOnRowContextMenu: EventEmitter<ThyTableEvent> = new EventEmitter<ThyTableEvent>();
8✔
543

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

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

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

13✔
562
    public expandStatusMapOfGroup: Dictionary<boolean> = {};
13✔
563

79✔
564
    private expandStatusMapOfGroupBeforeDrag: Dictionary<boolean> = {};
79✔
565

78✔
566
    dragPreviewClass = 'thy-table-drag-preview';
567

568
    public skeletonColumns: ThyTableSkeletonColumn[] = [];
569

570
    constructor(
4✔
571
        public elementRef: ElementRef,
4✔
572
        private _differs: IterableDiffers,
573
        private viewportRuler: ViewportRuler,
574
        private updateHostClassService: UpdateHostClassService,
1✔
UNCOV
575
        @Inject(DOCUMENT) private document: SafeAny,
×
576
        @Inject(PLATFORM_ID) private platformId: string,
577
        private ngZone: NgZone,
578
        private renderer: Renderer2,
579
        private cdr: ChangeDetectorRef
1✔
580
    ) {
1✔
581
        this._bindTrackFn();
582
    }
583

584
    private _initializeColumns() {
8✔
585
        if (!this.columns.some(item => item.expand === true) && this.columns.length > 0) {
8✔
586
            this.columns[0].expand = true;
8✔
587
        }
8✔
588
        this._initializeColumnFixedPositions();
8✔
589
    }
8!
590

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

8!
UNCOV
604
    private _initializeDataModel() {
×
605
        this.model.forEach(row => {
606
            this.columns.forEach(column => {
607
                this._initialSelections(row, column);
608
                this._initialCustomModelValue(row, column);
66✔
609
            });
66✔
610
        });
66✔
611
    }
66✔
612

613
    private _initialSelections(row: object, column: ThyTableColumnComponent) {
614
        if (column.selections) {
8✔
615
            if (column.type === 'checkbox') {
8✔
616
                row[column.key] = column.selections.includes(row[this.rowKey]);
8✔
617
                this.onModelChange(row, column);
618
            }
66✔
619
            if (column.type === 'radio') {
66✔
UNCOV
620
                if (column.selections.includes(row[this.rowKey])) {
×
621
                    this.selectedRadioRow = row;
622
                }
623
            }
624
        }
625
    }
66✔
626

66✔
627
    private _initialCustomModelValue(row: object, column: ThyTableColumnComponent) {
377✔
628
        if (column.type === customType.switch) {
556✔
629
            row[column.key] = get(row, column.model);
682✔
630
        }
631
    }
377✔
632

633
    private _refreshCustomModelValue(row: SafeAny) {
634
        this.columns.forEach(column => {
635
            this._initialCustomModelValue(row, column);
66!
UNCOV
636
        });
×
637
    }
638

66✔
639
    private _applyDiffChanges(changes: IterableChanges<SafeAny>) {
392✔
640
        if (changes) {
641
            changes.forEachAddedItem((record: IterableChangeRecord<SafeAny>) => {
642
                this._refreshCustomModelValue(record.item);
643
            });
644
        }
645
    }
646

647
    private _bindTrackFn() {
392!
648
        this.trackByFn = function (this: SafeAny, index: number, row: SafeAny): SafeAny {
649
            return row && this.rowKey ? row[this.rowKey] : index;
×
650
        }.bind(this);
651
    }
652

653
    private _destroyInvalidAttribute() {
654
        this.model.forEach(row => {
655
            for (const key in row) {
656
                if (key.includes('[$$column]')) {
657
                    delete row[key];
1!
658
                }
1✔
659
            }
660
        });
661
    }
662

663
    private _setClass(first = false) {
78✔
664
        if (!first && !this.initialized) {
78✔
665
            return;
78✔
666
        }
78✔
667
        const classNames: string[] = [];
12✔
668
        if (this.size) {
12✔
669
            classNames.push(`table-${this.size}`);
670
        }
78!
671
        if (tableThemeMap[this.theme]) {
78✔
672
            classNames.push(tableThemeMap[this.theme]);
78✔
673
        }
674

675
        this.updateHostClassService.updateClass(classNames);
676
    }
70✔
677

678
    public updateColumnSelections(key: string, selections: SafeAny): void {
1✔
679
        const column = this.columns.find(item => item.key === key);
680
        this.model.forEach(row => {
681
            this._initialSelections(row, column);
682
        });
683
    }
684

685
    public isTemplateRef(ref: SafeAny) {
686
        return ref instanceof TemplateRef;
687
    }
688

689
    public getModelValue(row: SafeAny, path: string) {
1✔
690
        return get(row, path);
691
    }
692

693
    public renderRowClassName(row: SafeAny, index: number) {
694
        if (!this.rowClassName) {
695
            return null;
696
        }
697
        if (isString(this.rowClassName)) {
698
            return this.rowClassName;
699
        } else {
700
            return (this.rowClassName as Function)(row, index);
701
        }
702
    }
703

704
    public onModelChange(row: SafeAny, column: ThyTableColumnComponent) {
705
        if (column.model) {
706
            set(row, column.model, row[column.key]);
707
        }
708
    }
709

710
    public onStopPropagation(event: Event) {
711
        if (this.wholeRowSelect) {
712
            event.stopPropagation();
713
        }
714
    }
715

716
    public onPageChange(event: PageChangedEvent) {
717
        this.thyOnPageChange.emit(event);
718
    }
719

720
    public onPageIndexChange(event: number) {
721
        this.thyOnPageIndexChange.emit(event);
722
    }
723

724
    public onPageSizeChange(event: number) {
725
        this.thyOnPageSizeChange.emit(event);
726
    }
727

728
    public onCheckboxChange(row: SafeAny, column: ThyTableColumnComponent) {
729
        this.onModelChange(row, column);
730
        this.onMultiSelectChange(null, row, column);
731
    }
732

733
    public onMultiSelectChange(event: Event, row: SafeAny, column: ThyTableColumnComponent) {
734
        const rows = this.model.filter(item => {
735
            return item[column.key];
736
        });
737
        const multiSelectEvent: ThyMultiSelectEvent = {
738
            event: event,
1✔
739
            row: row,
740
            rows: rows
741
        };
742
        this.thyOnMultiSelectChange.emit(multiSelectEvent);
1✔
743
    }
744

745
    public onRadioSelectChange(event: Event, row: SafeAny) {
746
        const radioSelectEvent: ThyRadioSelectEvent = {
1✔
747
            event: event,
748
            row: row
749
        };
750
        this.thyOnRadioSelectChange.emit(radioSelectEvent);
1✔
751
    }
752

753
    public onSwitchChange(event: Event, row: SafeAny, column: SafeAny) {
754
        const switchEvent: ThySwitchEvent = {
1✔
755
            event: event,
756
            row: row,
757
            refresh: (value: SafeAny) => {
758
                value = value || row;
759
                setTimeout(() => {
1✔
760
                    value[column.key] = get(value, column.model);
761
                });
762
            }
763
        };
764
        this.thyOnSwitchChange.emit(switchEvent);
1✔
765
    }
766

767
    showExpand(row: SafeAny) {
768
        return row[this.thyChildrenKey] && row[this.thyChildrenKey].length > 0;
769
    }
1✔
770

771
    isExpanded(row: SafeAny) {
772
        return this.expandStatusMap[row[this.rowKey]];
773
    }
774

1✔
775
    iconIndentComputed(level: number) {
776
        if (this.mode === 'tree') {
777
            return level * this.thyIndent - 5;
778
        }
779
    }
1✔
780

781
    tdIndentComputed(level: number) {
782
        return {
783
            position: 'relative',
784
            paddingLeft: `${(level + 1) * this.thyIndent - 5}px`
1✔
785
        };
786
    }
787

788
    expandChildren(row: SafeAny) {
1✔
789
        if (this.isExpanded(row)) {
790
            this.expandStatusMap[row[this.rowKey]] = false;
791
        } else {
792
            this.expandStatusMap[row[this.rowKey]] = true;
793
        }
1✔
794
    }
795

796
    onDragGroupStarted(event: CdkDragStart<unknown>) {
797
        this.expandStatusMapOfGroupBeforeDrag = { ...this.expandStatusMapOfGroup };
1✔
798
        const groups = this.groups.filter(group => group.expand);
799
        this.foldGroups(groups);
800
        this.onDragStarted(event);
801
        this.cdr.detectChanges();
1✔
802
    }
803

804
    onDragGroupEnd(event: CdkDragEnd<unknown>) {
805
        const groups = this.groups.filter(group => this.expandStatusMapOfGroupBeforeDrag[group.id]);
1✔
806
        this.expandGroups(groups);
807
        this.cdr.detectChanges();
808
    }
809

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

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

858
    dropListEnterPredicate = (index: number, drag: CdkDrag, drop: CdkDropList) => {
859
        return drop.getSortedItems()[index].data.group_id === drag.data.group_id;
860
    };
861

862
    private onDragModelDropped(event: CdkDragDrop<unknown>) {
863
        const dragEvent: ThyTableDraggableEvent = {
864
            model: event.item,
865
            models: this.model,
866
            oldIndex: event.previousIndex,
867
            newIndex: event.currentIndex
868
        };
869
        moveItemInArray(this.model, event.previousIndex, event.currentIndex);
870
        this.thyOnDraggableChange.emit(dragEvent);
871
    }
872

873
    onDragDropped(event: CdkDragDrop<unknown>) {
874
        if (this.mode === 'group') {
875
            this.onDragGroupDropped(event);
876
        } else if (this.mode === 'list') {
877
            this.onDragModelDropped(event);
878
        }
879
    }
880

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

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

926
    private onRowClickPropagationEventHandler(event: Event, row: SafeAny): boolean {
927
        if ((event.target as Element).closest('.tree-expand-icon')) {
928
            this.expandChildren(row);
929
            return false;
930
        }
931
        return true;
932
    }
933

934
    public onRowContextMenu(event: Event, row: SafeAny) {
935
        const contextMenuEvent: ThyTableEvent = {
936
            event: event,
937
            row: row
938
        };
939
        this.thyOnRowContextMenu.emit(contextMenuEvent);
940
    }
941

942
    private _refreshColumns() {
943
        const components = this.columns || [];
944
        const _columns = components.map(component => {
945
            return {
946
                width: component.width,
947
                className: component.className
948
            };
949
        });
950

951
        this.columns.forEach((n, i) => {
952
            Object.assign(n, _columns[i]);
953
        });
954
    }
955

956
    private buildGroups(originGroups: SafeAny) {
957
        const originGroupsMap = helpers.keyBy(originGroups, 'id');
958
        this.groups = [];
959
        originGroups.forEach((origin: SafeAny) => {
960
            const group: ThyTableGroup = { id: origin[this.rowKey], children: [], origin };
961

962
            if (this.expandStatusMapOfGroup.hasOwnProperty(group.id)) {
963
                group.expand = this.expandStatusMapOfGroup[group.id];
964
            } else {
965
                group.expand = !!(originGroupsMap[group.id] as SafeAny).expand;
966
            }
967

968
            this.groups.push(group);
969
        });
970
    }
971

972
    private buildModel() {
973
        const groupsMap = keyBy(this.groups, 'id');
974
        this.model.forEach(row => {
975
            const group = groupsMap[row[this.groupBy]];
976
            if (group) {
977
                group.children.push(row);
978
            }
979
        });
980
    }
981

982
    public expandGroup(group: ThyTableGroup) {
983
        group.expand = !group.expand;
984
        this.expandStatusMapOfGroup[group.id] = group.expand;
985
    }
986

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

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

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

1022
    ngOnInit() {
1023
        this.updateHostClassService.initializeElement(this.tableElementRef.nativeElement);
1024
        this._setClass(true);
1025
        this.initialized = true;
1026

1027
        merge(this.viewportRuler.change(200), of(null).pipe(delay(200)))
1028
            .pipe(takeUntilDestroyed(this.destroyRef))
1029
            .subscribe(() => {
1030
                this._refreshColumns();
1031
                this.updateScrollClass();
1032
                this.cdr.detectChanges();
1033
            });
1034

1035
        this.ngZone.runOutsideAngular(() => {
1036
            this.scroll$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
1037
                this.updateScrollClass();
1038
            });
1039
        });
1040
    }
1041

1042
    private buildSkeletonColumns() {
1043
        this.skeletonColumns = [];
1044

1045
        this.columns.forEach((column: ThyTableColumnComponent, index: number) => {
1046
            const item = {
1047
                type: this.thyColumnSkeletonTypes[index] || ThyTableColumnSkeletonType.default,
1048
                width: column.width || 'auto'
1049
            };
1050
            this.skeletonColumns = [...this.skeletonColumns, item];
1051
        });
1052
    }
1053

1054
    ngAfterViewInit(): void {
1055
        if (isPlatformServer(this.platformId)) {
1056
            return;
1057
        }
1058

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

1101
    ngOnChanges(simpleChanges: SimpleChanges) {
1102
        const modeChange = simpleChanges.thyMode;
1103
        const thyGroupsChange = simpleChanges.thyGroups;
1104
        const isGroupMode = modeChange && modeChange.currentValue === 'group';
1105
        if (isGroupMode && thyGroupsChange && thyGroupsChange.firstChange) {
1106
            this.buildGroups(thyGroupsChange.currentValue);
1107
            this.buildModel();
1108
        }
1109

1110
        if (this._diff) {
1111
            const changes = this._diff.diff(this.model);
1112
            this._applyDiffChanges(changes);
1113
        }
1114
    }
1115

1116
    ngOnDestroy() {
1117
        this._destroyInvalidAttribute();
1118
    }
1119
}
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