Skip to content

Commit 9a2dd3f

Browse files
refactor(header-cell): use signals
1 parent ba9bca8 commit 9a2dd3f

File tree

1 file changed

+107
-154
lines changed

1 file changed

+107
-154
lines changed

projects/ngx-datatable/src/lib/components/header/header-cell.component.ts

Lines changed: 107 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
import { NgTemplateOutlet } from '@angular/common';
22
import {
33
ChangeDetectionStrategy,
4-
ChangeDetectorRef,
54
Component,
65
ElementRef,
7-
EventEmitter,
8-
HostBinding,
9-
HostListener,
106
inject,
11-
Input,
127
OnDestroy,
138
OnInit,
14-
Output,
15-
TemplateRef
9+
TemplateRef,
10+
input,
11+
output,
12+
numberAttribute,
13+
computed,
14+
booleanAttribute
1615
} from '@angular/core';
1716
import { fromEvent, Subscription, takeUntil } from 'rxjs';
1817

19-
import { InnerSortEvent, TableColumnInternal } from '../../types/internal.types';
18+
import {
19+
InnerSortEvent,
20+
SortableTableColumnInternal,
21+
TableColumnInternal
22+
} from '../../types/internal.types';
2023
import {
2124
HeaderCellContext,
2225
SelectionType,
@@ -32,37 +35,38 @@ import { nextSortDir } from '../../utils/sort';
3235
imports: [NgTemplateOutlet],
3336
template: `
3437
<div class="datatable-header-cell-template-wrap">
35-
@if (isTarget) {
38+
@if (isTarget()) {
3639
<ng-template
37-
[ngTemplateOutlet]="targetMarkerTemplate!"
38-
[ngTemplateOutletContext]="targetMarkerContext"
40+
[ngTemplateOutlet]="targetMarkerTemplate()!"
41+
[ngTemplateOutletContext]="targetMarkerContext()"
3942
/>
4043
}
41-
@if (isCheckboxable) {
44+
@if (isCheckboxable()) {
4245
<label class="datatable-checkbox">
4346
<input
4447
type="checkbox"
45-
[attr.aria-label]="ariaHeaderCheckboxMessage"
46-
[checked]="allRowsSelected"
48+
[attr.aria-label]="ariaHeaderCheckboxMessage()"
49+
[checked]="allRowsSelected()"
4750
(change)="select.emit()"
4851
/>
4952
</label>
5053
}
54+
@let column = this.column();
5155
@if (column.headerTemplate) {
5256
<ng-template
5357
[ngTemplateOutlet]="column.headerTemplate"
54-
[ngTemplateOutletContext]="cellContext"
58+
[ngTemplateOutletContext]="cellContext()"
5559
/>
5660
} @else {
5761
<span class="datatable-header-cell-wrapper">
5862
<span class="datatable-header-cell-label draggable" (click)="onSort()">
59-
{{ name }}
63+
{{ name() }}
6064
</span>
6165
</span>
6266
}
63-
<span [class]="sortClass" (click)="onSort()"> </span>
67+
<span [class]="sortClass()" (click)="onSort()"> </span>
6468
</div>
65-
@if (showResizeHandle) {
69+
@if (showResizeHandle()) {
6670
<span
6771
class="resize-handle"
6872
(mousedown)="onMousedown($event)"
@@ -73,88 +77,62 @@ import { nextSortDir } from '../../utils/sort';
7377
styleUrl: './header-cell.component.scss',
7478
changeDetection: ChangeDetectionStrategy.OnPush,
7579
host: {
76-
'class': 'datatable-header-cell',
77-
'[attr.resizeable]': 'showResizeHandle'
80+
'[attr.resizeable]': 'showResizeHandle()',
81+
'[attr.title]': 'name()',
82+
'[attr.tabindex]': 'column().sortable ? 0 : -1',
83+
'[class]': 'columnCssClasses()',
84+
'[style.height.px]': 'headerHeight()',
85+
'[style.minWidth.px]': 'column().minWidth',
86+
'[style.maxWidth.px]': 'column().maxWidth',
87+
'[style.width.px]': 'column().width',
88+
'(contextmenu)': 'onContextmenu($event)',
89+
'(keydown.enter)': 'enter()'
7890
}
7991
})
8092
export class DataTableHeaderCellComponent implements OnInit, OnDestroy {
81-
private cd = inject(ChangeDetectorRef);
82-
83-
@Input() sortType!: SortType;
84-
@Input() sortAscendingIcon?: string;
85-
@Input() sortDescendingIcon?: string;
86-
@Input() sortUnsetIcon?: string;
87-
88-
@Input() isTarget?: boolean;
89-
@Input() showResizeHandle?: boolean = true;
90-
@Input() targetMarkerTemplate?: TemplateRef<any>;
91-
@Input() targetMarkerContext: any;
92-
@Input() enableClearingSortState = false;
93-
@Input() ariaHeaderCheckboxMessage!: string;
94-
95-
private _allRowsSelected?: boolean;
96-
97-
@Input() set allRowsSelected(value) {
98-
this._allRowsSelected = value;
99-
this.cellContext.allRowsSelected = value;
100-
}
101-
get allRowsSelected() {
102-
return this._allRowsSelected;
103-
}
104-
105-
@Input() selectionType?: SelectionType;
106-
107-
@Input() set column(column: TableColumnInternal) {
108-
this._column = column;
109-
this.cellContext.column = column;
110-
this.cd.markForCheck();
111-
}
112-
113-
get column(): TableColumnInternal {
114-
return this._column;
115-
}
116-
117-
@HostBinding('style.height.px')
118-
@Input()
119-
headerHeight!: number;
120-
121-
@Input() set sorts(val: SortPropDir[]) {
122-
this._sorts = val;
123-
this.sortDir = this.calcSortDir(val);
124-
this.cellContext.sortDir = this.sortDir;
125-
this.sortClass = this.calcSortClass(this.sortDir);
126-
this.cd.markForCheck();
127-
}
128-
129-
get sorts(): SortPropDir[] {
130-
return this._sorts;
131-
}
132-
133-
@Output() readonly sort = new EventEmitter<InnerSortEvent>();
134-
@Output() readonly select = new EventEmitter<void>();
135-
@Output() readonly columnContextmenu = new EventEmitter<{
93+
readonly sortType = input.required<SortType>();
94+
readonly sortAscendingIcon = input<string>();
95+
readonly sortDescendingIcon = input<string>();
96+
readonly sortUnsetIcon = input<string>();
97+
98+
readonly isTarget = input<boolean>();
99+
readonly showResizeHandle = input<boolean | undefined>(true);
100+
readonly targetMarkerTemplate = input<TemplateRef<any>>();
101+
readonly targetMarkerContext = input<any>();
102+
readonly enableClearingSortState = input(false);
103+
readonly ariaHeaderCheckboxMessage = input.required<string>();
104+
readonly allRowsSelected = input(false, { transform: booleanAttribute });
105+
readonly selectionType = input<SelectionType>();
106+
readonly column = input.required<TableColumnInternal>();
107+
readonly headerHeight = input.required<number, number>({
108+
transform: numberAttribute
109+
});
110+
readonly sorts = input<SortPropDir[]>([]);
111+
112+
readonly sort = output<InnerSortEvent>();
113+
readonly select = output<void>();
114+
readonly columnContextmenu = output<{
136115
event: MouseEvent;
137116
column: TableColumnInternal;
138-
}>(false);
139-
@Output() readonly resize = new EventEmitter<{ width: number; column: TableColumnInternal }>();
140-
@Output() readonly resizing = new EventEmitter<{ width: number; column: TableColumnInternal }>();
117+
}>();
118+
readonly resize = output<{ width: number; column: TableColumnInternal }>();
119+
readonly resizing = output<{ width: number; column: TableColumnInternal }>();
141120

142-
@HostBinding('class')
143-
protected get columnCssClasses(): string {
121+
protected readonly columnCssClasses = computed(() => {
144122
let cls = 'datatable-header-cell';
145-
146-
if (this.column.sortable) {
123+
const column = this.column();
124+
if (column.sortable) {
147125
cls += ' sortable';
148126
}
149-
if (this.showResizeHandle) {
127+
if (this.showResizeHandle()) {
150128
cls += ' resizeable';
151129
}
152-
if (this.column.headerClass) {
153-
if (typeof this.column.headerClass === 'string') {
154-
cls += ' ' + this.column.headerClass;
155-
} else if (typeof this.column.headerClass === 'function') {
156-
const res = this.column.headerClass({
157-
column: this.column
130+
if (column.headerClass) {
131+
if (typeof column.headerClass === 'string') {
132+
cls += ' ' + column.headerClass;
133+
} else if (typeof column.headerClass === 'function') {
134+
const res = column.headerClass({
135+
column
158136
});
159137

160138
if (typeof res === 'string') {
@@ -170,80 +148,55 @@ export class DataTableHeaderCellComponent implements OnInit, OnDestroy {
170148
}
171149
}
172150

173-
const sortDir = this.sortDir;
151+
const sortDir = this.sortDir();
174152
if (sortDir) {
175153
cls += ` sort-active sort-${sortDir}`;
176154
}
177155

178156
return cls;
179-
}
157+
});
180158

181-
@HostBinding('attr.title')
182-
protected get name(): string | undefined {
159+
protected readonly name = computed(() => {
183160
// guaranteed to have a value by setColumnDefaults() in column-helper.ts
184-
return this.column.headerTemplate === undefined ? this.column.name : undefined;
185-
}
186-
187-
@HostBinding('style.minWidth.px')
188-
protected get minWidth(): number | undefined {
189-
return this.column.minWidth;
190-
}
191-
192-
@HostBinding('style.maxWidth.px')
193-
protected get maxWidth(): number | undefined {
194-
return this.column.maxWidth;
195-
}
196-
197-
@HostBinding('style.width.px')
198-
protected get width(): number {
199-
return this.column.width;
200-
}
201-
202-
@HostBinding('tabindex') protected get tabindex(): number {
203-
return this.column.sortable ? 0 : -1;
204-
}
205-
206-
protected get isCheckboxable(): boolean | undefined {
207-
return this.column.headerCheckboxable;
208-
}
209-
210-
protected sortClass?: string;
211-
private sortDir?: SortDirection;
212-
213-
protected cellContext: HeaderCellContext;
214-
215-
private _column!: TableColumnInternal;
216-
private _sorts!: SortPropDir[];
217-
private element = inject(ElementRef).nativeElement;
218-
private subscription?: Subscription;
219-
220-
constructor() {
221-
this.cellContext = {
222-
column: this.column,
223-
sortDir: this.sortDir,
161+
return this.column().headerTemplate === undefined ? this.column().name : undefined;
162+
});
163+
164+
protected readonly isCheckboxable = computed(() => this.column().headerCheckboxable);
165+
166+
protected readonly sortClass = computed<string | undefined>(() => {
167+
return this.calcSortClass(this.sortDir());
168+
});
169+
private readonly sortDir = computed<SortDirection | undefined>(() => {
170+
return this.calcSortDir(this.sorts());
171+
});
172+
173+
protected readonly cellContext = computed<HeaderCellContext>(() => {
174+
return {
175+
column: this.column(),
176+
sortDir: this.sortDir(),
224177
sortFn: () => this.onSort(),
225-
allRowsSelected: this.allRowsSelected,
178+
allRowsSelected: this.allRowsSelected(),
226179
selectFn: () => this.select.emit()
227180
};
228-
}
181+
});
182+
183+
private element = inject(ElementRef).nativeElement;
184+
private subscription?: Subscription;
229185

230-
@HostListener('contextmenu', ['$event'])
231186
protected onContextmenu($event: MouseEvent): void {
232-
this.columnContextmenu.emit({ event: $event, column: this.column });
233-
if (this.column.draggable) {
187+
this.columnContextmenu.emit({ event: $event, column: this.column() });
188+
if (this.column().draggable) {
234189
$event.preventDefault();
235190
}
236191
}
237192

238-
@HostListener('keydown.enter')
239193
protected enter(): void {
240194
this.onSort();
241195
}
242196

243197
ngOnInit() {
244-
this.sortClass = this.calcSortClass(this.sortDir);
245198
// If there is already a default sort then start the counter with 1.
246-
if (this.sortDir) {
199+
if (this.sortDir()) {
247200
this.totalSortStatesApplied = 1;
248201
}
249202
}
@@ -253,8 +206,8 @@ export class DataTableHeaderCellComponent implements OnInit, OnDestroy {
253206
}
254207

255208
private calcSortDir(sorts: SortPropDir[]): any {
256-
if (sorts && this.column) {
257-
const sort = sorts.find((s: any) => s.prop === this.column.prop);
209+
if (sorts && this.column()) {
210+
const sort = sorts.find((s: any) => s.prop === this.column().prop);
258211

259212
if (sort) {
260213
return sort.dir;
@@ -264,34 +217,34 @@ export class DataTableHeaderCellComponent implements OnInit, OnDestroy {
264217
// Counter to reset sort once user sort asc and desc.
265218
private totalSortStatesApplied = 0;
266219
protected onSort(): void {
267-
if (!this.column.sortable) {
220+
if (!this.column().sortable) {
268221
return;
269222
}
270223

271224
this.totalSortStatesApplied++;
272-
let newValue = nextSortDir(this.sortType, this.sortDir);
225+
let newValue = nextSortDir(this.sortType(), this.sortDir());
273226
// User has done both direction sort so we reset the next sort.
274-
if (this.enableClearingSortState && this.totalSortStatesApplied === 3) {
227+
if (this.enableClearingSortState() && this.totalSortStatesApplied === 3) {
275228
newValue = undefined;
276229
this.totalSortStatesApplied = 0;
277230
}
278231
this.sort.emit({
279-
column: this.column,
280-
prevValue: this.sortDir,
232+
column: this.column() as SortableTableColumnInternal<any>,
233+
prevValue: this.sortDir(),
281234
newValue
282235
});
283236
}
284237

285238
private calcSortClass(sortDir: SortDirection | undefined): string | undefined {
286-
if (!this.cellContext.column.sortable) {
239+
if (!this.cellContext().column.sortable) {
287240
return undefined;
288241
}
289242
if (sortDir === SortDirection.asc) {
290-
return `sort-btn sort-asc ${this.sortAscendingIcon ?? 'datatable-icon-up'}`;
243+
return `sort-btn sort-asc ${this.sortAscendingIcon() ?? 'datatable-icon-up'}`;
291244
} else if (sortDir === SortDirection.desc) {
292-
return `sort-btn sort-desc ${this.sortDescendingIcon ?? 'datatable-icon-down'}`;
245+
return `sort-btn sort-desc ${this.sortDescendingIcon() ?? 'datatable-icon-down'}`;
293246
} else {
294-
return `sort-btn ${this.sortUnsetIcon ?? 'datatable-icon-sort-unset'}`;
247+
return `sort-btn ${this.sortUnsetIcon() ?? 'datatable-icon-sort-unset'}`;
295248
}
296249
}
297250

@@ -317,7 +270,7 @@ export class DataTableHeaderCellComponent implements OnInit, OnDestroy {
317270
private onMouseup(): void {
318271
if (this.subscription && !this.subscription.closed) {
319272
this.destroySubscription();
320-
this.resize.emit({ width: this.element.clientWidth, column: this.column });
273+
this.resize.emit({ width: this.element.clientWidth, column: this.column() });
321274
}
322275
}
323276

@@ -328,7 +281,7 @@ export class DataTableHeaderCellComponent implements OnInit, OnDestroy {
328281
): void {
329282
const movementX = getPositionFromEvent(event).screenX - mouseDownScreenX;
330283
const newWidth = initialWidth + movementX;
331-
this.resizing.emit({ width: newWidth, column: this.column });
284+
this.resizing.emit({ width: newWidth, column: this.column() });
332285
}
333286

334287
private destroySubscription(): void {

0 commit comments

Comments
 (0)