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

atinc / ngx-tethys / c0ef8457-a839-451f-8b72-80fd73106231

02 Apr 2024 02:27PM UTC coverage: 90.524% (-0.06%) from 90.585%
c0ef8457-a839-451f-8b72-80fd73106231

Pull #3062

circleci

minlovehua
refactor(all): use the transform attribute of @Input() instead of @InputBoolean() and @InputNumber()
Pull Request #3062: refactor(all): use the transform attribute of @input() instead of @InputBoolean() and @InputNumber()

4987 of 6108 branches covered (81.65%)

Branch coverage included in aggregate %.

217 of 223 new or added lines in 82 files covered. (97.31%)

202 existing lines in 53 files now uncovered.

12246 of 12929 relevant lines covered (94.72%)

1055.59 hits per line

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

88.15
/src/table/table.component.ts
1
import { Constructor, InputCssPixel, MixinBase, mixinUnsubscribe, ThyUnsubscribe, 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, takeUntil } from 'rxjs/operators';
6

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

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

70
export type ThyTableTheme = 'default' | 'bordered' | 'boxed';
71

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

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

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

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

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

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

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

45✔
108
const passiveEventListenerOptions = normalizePassiveListenerOptions({ passive: true });
109

110
const _MixinBase: Constructor<ThyUnsubscribe> & typeof MixinBase = mixinUnsubscribe(MixinBase);
45✔
111

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

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

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

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

70✔
165
    public groupBy: string;
70✔
166

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

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

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

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

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

70✔
177
    public loadingDone = true;
70✔
178

70✔
179
    public loadingText: string;
70✔
180

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

70✔
183
    public draggable = false;
70✔
184

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

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

70✔
189
    public trackByFn: SafeAny;
384✔
190

70✔
191
    public wholeRowSelect = false;
192

193
    public fixedDirection = ThyFixedDirection;
194

195
    public hasFixed = false;
70✔
196

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

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

70✔
201
    private initialized = false;
70✔
202

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

70✔
205
    private scrollClassName = css.tableScrollLeft;
206

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

479
    @Input() thyDragDisabledPredicate: (item: SafeAny) => boolean = () => false;
3✔
480

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

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

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

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

1✔
506
    @Output() thyOnPageSizeChange: EventEmitter<number> = new EventEmitter<number>();
507

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

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

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

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

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

1✔
533
    @Output() thyOnRowContextMenu: EventEmitter<ThyTableEvent> = new EventEmitter<ThyTableEvent>();
534

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

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

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

×
553
    public expandStatusMapOfGroup: Dictionary<boolean> = {};
554

555
    private expandStatusMapOfGroupBeforeDrag: Dictionary<boolean> = {};
28✔
556

557
    dragPreviewClass = 'thy-table-drag-preview';
28✔
558

559
    public skeletonColumns: ThyTableSkeletonColumn[] = [];
560

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1027
        this.ngZone.runOutsideAngular(() => {
1028
            this.scroll$.pipe(takeUntil(this.ngUnsubscribe$)).subscribe(() => {
1029
                this.updateScrollClass();
1030
            });
1031
        });
1032
    }
1033

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

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

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

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

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

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

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