diff --git a/projects/ngx-datatable/src/lib/components/body/body-cell.component.spec.ts b/projects/ngx-datatable/src/lib/components/body/body-cell.component.spec.ts index fd0ca2b5a..85eb9d748 100644 --- a/projects/ngx-datatable/src/lib/components/body/body-cell.component.spec.ts +++ b/projects/ngx-datatable/src/lib/components/body/body-cell.component.spec.ts @@ -115,7 +115,6 @@ describe('DataTableBodyCellComponent', () => { component.setInput('row', { id: 1 }); const columns = toInternalColumn([{ name: 'Tree', prop: 'id', isTreeColumn: true }]); component.setInput('column', columns[0]); - component.setInput('treeStatus', ''); }); it('should render tree toggle button when isTreeColumn is true', async () => { diff --git a/projects/ngx-datatable/src/lib/components/body/body-cell.component.ts b/projects/ngx-datatable/src/lib/components/body/body-cell.component.ts index e1b4c80bc..e580f9818 100644 --- a/projects/ngx-datatable/src/lib/components/body/body-cell.component.ts +++ b/projects/ngx-datatable/src/lib/components/body/body-cell.component.ts @@ -1,16 +1,16 @@ import { NgTemplateOutlet } from '@angular/common'; import { + booleanAttribute, ChangeDetectionStrategy, - ChangeDetectorRef, Component, + computed, DoCheck, ElementRef, - EventEmitter, - HostBinding, - HostListener, inject, - Input, - Output + input, + linkedSignal, + output, + signal } from '@angular/core'; import { NgxDatatableConfig } from '../../ngx-datatable.config'; @@ -22,20 +22,24 @@ import { ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT, ARROW_UP, ENTER } from '../../util selector: 'datatable-body-cell', imports: [NgTemplateOutlet], template: ` + @let column = this.column(); + @let row = this.row();
- @if (column.checkboxable && (!displayCheck || displayCheck(row, column, value))) { + @let displayCheck = this.displayCheck(); + @if (column.checkboxable && (!displayCheck || displayCheck(row, column, value()))) { } @if (column.isTreeColumn) { @if (!column.treeToggleTemplate) { + @let treeStatus = this.treeStatus() ?? 'collapsed'; } @else { } } @if (!column.cellTemplate) { @if (column.bindAsUnsafeHtml) { - + } @else { - {{ value }} + {{ value() }} } } @else { }
`, styleUrl: './body-cell.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class]': 'columnCssClasses()', + '[style.width.px]': 'column().width', + '[style.minWidth.px]': 'column().minWidth', + '[style.maxWidth.px]': 'column().maxWidth', + '[style.height]': 'height()', + '(focus)': 'onFocus()', + '(blur)': 'onBlur()', + '(click)': 'onClick($event)', + '(dblclick)': 'onDblClick($event)', + '(keydown)': 'onKeyDown($event)' + } }) export class DataTableBodyCellComponent implements DoCheck { - private cd = inject(ChangeDetectorRef); + readonly displayCheck = input<(row: TRow, column: TableColumnInternal, value: any) => boolean>(); - @Input() displayCheck?: (row: TRow, column: TableColumnInternal, value: any) => boolean; + readonly disabled = input(false, { transform: booleanAttribute }); - @Input() set disabled(value: boolean | undefined) { - this.cellContext.disabled = value; - this._disabled = value; - } + readonly group = input(); - get disabled(): boolean | undefined { - return this._disabled; - } + readonly rowHeight = input(0); - @Input() set group(group: TRow[] | undefined) { - this._group = group; - this.cellContext.group = group; - this.checkValueUpdates(); - this.cd.markForCheck(); - } + readonly isSelected = input(false, { transform: booleanAttribute }); - get group() { - return this._group; - } + readonly rowIndex = input(); - @Input() set rowHeight(val: number) { - this._rowHeight = val; - this.cellContext.rowHeight = val; - this.checkValueUpdates(); - this.cd.markForCheck(); - } + readonly column = input.required(); - get rowHeight() { - return this._rowHeight; - } - - @Input() set isSelected(val: boolean | undefined) { - this._isSelected = val; - this.cellContext.isSelected = val; - this.cd.markForCheck(); - } - - get isSelected(): boolean | undefined { - return this._isSelected; - } - - @Input() set expanded(val: boolean | undefined) { - this._expanded = val; - this.cellContext.expanded = val; - this.cd.markForCheck(); - } - - get expanded(): boolean | undefined { - return this._expanded; - } - - @Input() set rowIndex(val: RowIndex) { - this._rowIndex = val; - this.cellContext.rowIndex = val?.index; - this.cellContext.rowInGroupIndex = val?.indexInGroup; - this.checkValueUpdates(); - this.cd.markForCheck(); - } - - get rowIndex(): RowIndex { - return this._rowIndex; - } - - @Input() set column(column: TableColumnInternal) { - this._column = column; - this.cellContext.column = column; - this.checkValueUpdates(); - this.cd.markForCheck(); - } - - get column(): TableColumnInternal { - return this._column; - } - - @Input() set row(row: TRow) { - this._row = row; - this.cellContext.row = row; - this.checkValueUpdates(); - this.cd.markForCheck(); - } + readonly row = input.required(); - get row(): TRow { - return this._row; - } + readonly treeStatus = input('collapsed'); - @Input() set treeStatus(status: TreeStatus | undefined) { - if ( - status !== 'collapsed' && - status !== 'expanded' && - status !== 'loading' && - status !== 'disabled' - ) { - this._treeStatus = 'collapsed'; - } else { - this._treeStatus = status; - } - this.cellContext.treeStatus = this._treeStatus; - this.checkValueUpdates(); - this.cd.markForCheck(); - } + readonly ariaRowCheckboxMessage = input.required(); - get treeStatus(): TreeStatus | undefined { - return this._treeStatus; - } + readonly cssClasses = input.required['cssClasses']>>(); - @Input({ required: true }) ariaRowCheckboxMessage!: string; - @Input({ required: true }) cssClasses!: Partial['cssClasses']>; + readonly expanded = input(false, { transform: booleanAttribute }); - @Output() readonly activate = new EventEmitter>(); + readonly activate = output>(); - @Output() readonly treeAction = new EventEmitter(); + readonly treeAction = output(); - @HostBinding('class') - get columnCssClasses(): string { + protected readonly columnCssClasses = computed(() => { let cls = 'datatable-body-cell'; - if (this.column.cellClass) { - if (typeof this.column.cellClass === 'string') { - cls += ' ' + this.column.cellClass; - } else if (typeof this.column.cellClass === 'function') { - const res = this.column.cellClass({ + const column = this.column(); + if (column.cellClass) { + if (typeof column.cellClass === 'string') { + cls += ' ' + column.cellClass; + } else if (typeof column.cellClass === 'function') { + const res = column.cellClass({ row: this.row, - group: this.group, - column: this.column, - value: this.value, - rowHeight: this.rowHeight + group: this.group(), + column: column, + value: this.value(), + rowHeight: this.rowHeight() }); if (typeof res === 'string') { @@ -224,87 +152,67 @@ export class DataTableBodyCellComponent implements DoChe } } } - if (this.isFocused && !this._disabled) { + if (this.isFocused() && !this.disabled()) { cls += ' active'; } - if (this._disabled) { + if (this.disabled()) { cls += ' row-disabled'; } return cls; - } + }); - @HostBinding('style.width.px') - get width(): number { - return this.column.width; - } - - @HostBinding('style.minWidth.px') - get minWidth(): number | undefined { - return this.column.minWidth; - } - - @HostBinding('style.maxWidth.px') - get maxWidth(): number | undefined { - return this.column.maxWidth; - } - - @HostBinding('style.height') - get height(): string | number { - const height = this.rowHeight; + protected readonly height = computed(() => { + const height = this.rowHeight(); if (isNaN(height)) { return height; } return height + 'px'; - } - - sanitizedValue!: string; - value: any; - isFocused = false; - - cellContext: CellContext; - - private _isSelected?: boolean; - private _column!: TableColumnInternal; - private _row!: TRow; - private _group?: TRow[]; - private _rowHeight!: number; - private _rowIndex!: RowIndex; - private _expanded?: boolean; - private _element = inject>(ElementRef).nativeElement; - private _treeStatus?: TreeStatus; - private _disabled?: boolean; - - constructor() { - this.cellContext = { + }); + + protected readonly sanitizedValue = computed(() => { + const value = this.value(); + return value !== null && value !== undefined ? this.stripHtml(value) : value; + }); + readonly value = linkedSignal(() => this.getComputedValue()); + protected readonly cellContext = computed>(() => { + return { onCheckboxChangeFn: (event: Event) => this.onCheckboxChange(event), activateFn: (event: ActivateEvent) => this.activate.emit(event), - row: this.row, - group: this.group, - value: this.value, - column: this.column, - rowHeight: this.rowHeight, - isSelected: this.isSelected, - rowIndex: this.rowIndex?.index, - rowInGroupIndex: this.rowIndex?.indexInGroup, - treeStatus: this.treeStatus, - disabled: this._disabled, + row: this.row(), + group: this.group(), + value: this.value(), + column: this.column(), + rowHeight: this.rowHeight(), + isSelected: this.isSelected(), + rowIndex: this.rowIndex()?.index ?? 0, + rowInGroupIndex: this.rowIndex()?.indexInGroup, + treeStatus: this.treeStatus() ?? 'collapsed', + disabled: this.disabled(), + expanded: this.expanded(), onTreeAction: () => this.onTreeAction() }; - } + }); + + private readonly isFocused = signal(false); + private _element = inject>(ElementRef).nativeElement; ngDoCheck(): void { - this.checkValueUpdates(); + const value = this.getComputedValue(); + if (value !== this.value()) { + this.value.set(value); + } } - checkValueUpdates(): void { + private getComputedValue(): any { let value = ''; - - if (!this.row || this.column?.prop == undefined) { + const column = this.column(); + const row = this.row(); + if (!row || column.prop == undefined) { value = ''; } else { - const val = this.column.$$valueGetter(this.row, this.column.prop); - const userPipe = this.column.pipe; + const val = column.$$valueGetter(row, column.prop); + const userPipe = column.pipe; if (userPipe) { value = userPipe.transform(val); @@ -312,56 +220,44 @@ export class DataTableBodyCellComponent implements DoChe value = val; } } - - if (this.value !== value) { - this.value = value; - this.cellContext.value = value; - this.cellContext.disabled = this._disabled; - this.sanitizedValue = value !== null && value !== undefined ? this.stripHtml(value) : value; - this.cd.markForCheck(); - } + return value; } - @HostListener('focus') - onFocus(): void { - this.isFocused = true; + protected onFocus(): void { + this.isFocused.set(true); } - @HostListener('blur') - onBlur(): void { - this.isFocused = false; + protected onBlur(): void { + this.isFocused.set(false); } - @HostListener('click', ['$event']) - onClick(event: MouseEvent): void { + protected onClick(event: MouseEvent): void { this.activate.emit({ type: 'click', event, - row: this.row, - group: this.group, - rowHeight: this.rowHeight, - column: this.column, - value: this.value, + row: this.row(), + group: this.group(), + rowHeight: this.rowHeight(), + column: this.column(), + value: this.value(), cellElement: this._element }); } - @HostListener('dblclick', ['$event']) - onDblClick(event: MouseEvent): void { + protected onDblClick(event: MouseEvent): void { this.activate.emit({ type: 'dblclick', event, - row: this.row, - group: this.group, - rowHeight: this.rowHeight, - column: this.column, - value: this.value, + row: this.row(), + group: this.group(), + rowHeight: this.rowHeight(), + column: this.column(), + value: this.value(), cellElement: this._element }); } - @HostListener('keydown', ['$event']) - onKeyDown(event: KeyboardEvent): void { + protected onKeyDown(event: KeyboardEvent): void { const key = event.key; const isTargetCell = event.target === this._element; @@ -379,11 +275,11 @@ export class DataTableBodyCellComponent implements DoChe this.activate.emit({ type: 'keydown', event, - row: this.row, - group: this.group, - rowHeight: this.rowHeight, - column: this.column, - value: this.value, + row: this.row(), + group: this.group(), + rowHeight: this.rowHeight(), + column: this.column(), + value: this.value(), cellElement: this._element }); } @@ -393,11 +289,11 @@ export class DataTableBodyCellComponent implements DoChe this.activate.emit({ type: 'checkbox', event, - row: this.row, - group: this.group, - rowHeight: this.rowHeight, - column: this.column, - value: this.value, + row: this.row(), + group: this.group(), + rowHeight: this.rowHeight(), + column: this.column(), + value: this.value(), cellElement: this._element, treeStatus: 'collapsed' }); @@ -410,8 +306,8 @@ export class DataTableBodyCellComponent implements DoChe return html.replace(/<\/?[^>]+(>|$)/g, ''); } - onTreeAction() { - this.treeAction.emit(this.row); + protected onTreeAction() { + this.treeAction.emit(this.row()); } calcLeftMargin(column: TableColumnInternal, row: RowOrGroup): number {