Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,24 @@ import { NgTemplateOutlet } from '@angular/common';
import {
booleanAttribute,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DoCheck,
ElementRef,
EventEmitter,
HostListener,
inject,
Input,
IterableDiffer,
IterableDiffers,
KeyValueDiffer,
KeyValueDiffers,
OnChanges,
OnInit,
Output,
signal,
SimpleChanges,
ViewChild
input,
output,
viewChild,
linkedSignal
} from '@angular/core';

import { Group, GroupContext, Row, RowDetailContext, RowOrGroup } from '../../types/public.types';
import { Group, Row, RowOrGroup, RowDetailContext, GroupContext } from '../../types/public.types';
import { DATATABLE_COMPONENT_TOKEN } from '../../utils/table-token';
import { DatatableRowDetailDirective } from '../row-detail/row-detail.directive';
import { DatatableGroupHeaderDirective } from './body-group-header.directive';
Expand All @@ -31,40 +28,42 @@ import { DatatableGroupHeaderDirective } from './body-group-header.directive';
selector: 'datatable-row-wrapper',
imports: [NgTemplateOutlet],
template: `
@if (isGroup(row) && groupHeader?.template) {
@let row = this.row();
@let groupHeader = this.groupHeader();
@if (isGroup(row) && groupHeader && groupHeader.template) {
<div
class="datatable-group-header"
[style.height.px]="groupHeaderRowHeight"
[style.width.px]="innerWidth"
[style.height.px]="groupHeaderRowHeight()"
[style.width.px]="innerWidth()"
>
<div class="datatable-group-cell">
@if (groupHeader!.checkboxable) {
@if (groupHeader.checkboxable) {
<div>
<label class="datatable-checkbox">
<input
#select
type="checkbox"
[attr.aria-label]="ariaGroupHeaderCheckboxMessage"
[attr.aria-label]="ariaGroupHeaderCheckboxMessage()"
[checked]="selectedGroupRows().length === row.value.length"
(change)="onCheckboxChange(select.checked, row)"
/>
</label>
</div>
}
<ng-template
[ngTemplateOutlet]="groupHeader!.template!"
[ngTemplateOutletContext]="context"
[ngTemplateOutlet]="groupHeader.template"
[ngTemplateOutletContext]="context()"
/>
</div>
</div>
}
@if ((groupHeader?.template && expanded) || !groupHeader || !groupHeader.template) {
@if ((groupHeader?.template && expanded()) || !groupHeader || !groupHeader?.template) {
<ng-content />
}
@let rowDetailTemplate = rowDetail?.template();
@if (rowDetailTemplate && expanded) {
<div class="datatable-row-detail" [style.height.px]="detailRowHeight">
<ng-template [ngTemplateOutlet]="rowDetailTemplate" [ngTemplateOutletContext]="context" />
@let rowDetailTemplate = rowDetail()?.template();
@if (rowDetailTemplate && expanded()) {
<div class="datatable-row-detail" [style.height.px]="detailRowHeight()">
<ng-template [ngTemplateOutlet]="rowDetailTemplate" [ngTemplateOutletContext]="context()" />
</div>
}
`,
Expand All @@ -74,99 +73,96 @@ import { DatatableGroupHeaderDirective } from './body-group-header.directive';
class: 'datatable-row-wrapper'
}
})
export class DataTableRowWrapperComponent<TRow extends Row = any>
implements DoCheck, OnInit, OnChanges
{
@ViewChild('select') checkBoxInput!: ElementRef<HTMLInputElement>;
@Input() innerWidth!: number;
@Input() rowDetail?: DatatableRowDetailDirective;
@Input() groupHeader?: DatatableGroupHeaderDirective;
@Input() offsetX!: number;
@Input() detailRowHeight!: number;
@Input() groupHeaderRowHeight!: number;
@Input() row!: RowOrGroup<TRow>;
@Input() groupedRows?: Group<TRow>[];
@Input() selected!: TRow[];
@Input() disabled?: boolean;
@Output() readonly rowContextmenu = new EventEmitter<{
export class DataTableRowWrapperComponent<TRow extends Row = any> implements DoCheck, OnInit {
readonly checkBoxInput = viewChild<ElementRef<HTMLInputElement>>('select');
readonly innerWidth = input.required<number>();
readonly rowDetail = input<DatatableRowDetailDirective>();
readonly groupHeader = input<DatatableGroupHeaderDirective>();
readonly offsetX = input.required<number>();
readonly detailRowHeight = input.required<number>();
readonly groupHeaderRowHeight = input.required<number>();
readonly row = input.required<RowOrGroup<TRow>>();
readonly groupedRows = input<Group<TRow>[]>();
readonly selected = input.required<TRow[]>();
readonly disabled = input<boolean>();
readonly rowContextmenu = output<{
event: MouseEvent;
row: RowOrGroup<TRow>;
}>(false);
}>();

@Input() rowIndex!: number;
readonly rowIndex = input.required<number>();

readonly selectedGroupRows = signal<TRow[]>([]);

@Input({ transform: booleanAttribute }) expanded = false;
@Input({ required: true }) ariaGroupHeaderCheckboxMessage!: string;
readonly expanded = input(false, { transform: booleanAttribute });
readonly ariaGroupHeaderCheckboxMessage = input.required<string>();

context!: RowDetailContext<TRow> | GroupContext<TRow>;
readonly context = linkedSignal<RowDetailContext<TRow> | GroupContext<TRow>>(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this can a computed

Copy link
Member Author

@chintankavathia chintankavathia Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I mentioned here #347 (comment)
computed won't trigger with markForCheck()

const row = this.row();
if (this.isGroup(row)) {
return {
group: row,
expanded: this.expanded(),
rowIndex: this.rowIndex()
};
} else {
return {
row,
expanded: this.expanded(),
rowIndex: this.rowIndex(),
disabled: this.disabled()
};
}
});

private rowDiffer: KeyValueDiffer<keyof RowOrGroup<TRow>, any> = inject(KeyValueDiffers)
.find({})
.create();
private iterableDiffers = inject(IterableDiffers);
private selectedRowsDiffer!: IterableDiffer<TRow>;
private tableComponent = inject(DATATABLE_COMPONENT_TOKEN);
private cd = inject(ChangeDetectorRef);

ngOnChanges(changes: SimpleChanges): void {
if (changes.row) {
// this component renders either a group header or a row. Never both.
if (this.isGroup(this.row)) {
this.context = {
group: this.row,
expanded: this.expanded,
rowIndex: this.rowIndex
};
} else {
this.context = {
row: this.row,
expanded: this.expanded,
rowIndex: this.rowIndex,
disabled: this.disabled
};
}
}
if (changes.rowIndex) {
this.context.rowIndex = this.rowIndex;
}
if (changes.expanded) {
this.context.expanded = this.expanded;
}
}

ngOnInit(): void {
this.selectedRowsDiffer = this.iterableDiffers.find(this.selected ?? []).create();
this.selectedRowsDiffer = this.iterableDiffers.find(this.selected() ?? []).create();
}

ngDoCheck(): void {
if (this.rowDiffer.diff(this.row)) {
if ('group' in this.context) {
this.context.group = this.row as Group<TRow>;
const row = this.row();
if (this.rowDiffer.diff(row)) {
if ('group' in this.context()) {
this.context.set({
group: row as Group<TRow>,
expanded: this.expanded(),
rowIndex: this.rowIndex()
});
} else {
this.context.row = this.row as TRow;
this.context.set({
row: row as TRow,
expanded: this.expanded(),
rowIndex: this.rowIndex(),
disabled: this.disabled()
});
}
this.cd.markForCheck();
}
Comment on lines -144 to 146
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can just do:

if (this.rowDiffer.diff(this.row)) {
   this.cd.markForCheck();
}

// When groupheader is used with chechbox we use iterableDiffer
// on currently selected rows to check if it is modified
// if any of the row of this group is not present in `selected` rows array
// mark group header checkbox state as indeterminate
if (
this.isGroup(this.row) &&
this.groupHeader?.checkboxable &&
this.selectedRowsDiffer.diff(this.selected)
this.isGroup(row) &&
this.groupHeader()?.checkboxable &&
this.selectedRowsDiffer.diff(this.selected())
) {
const thisRow = this.row;
const selectedRows = this.selected.filter(row =>
thisRow.value.find((item: TRow) => item === row)
const thisRow = row;
const selectedRows = this.selected().filter(rowItem =>
thisRow.value.find((item: TRow) => item === rowItem)
);
if (this.checkBoxInput) {
if (selectedRows.length && selectedRows.length !== this.row.value.length) {
this.checkBoxInput.nativeElement.indeterminate = true;
const checkBoxInput = this.checkBoxInput();
if (checkBoxInput) {
if (selectedRows.length && selectedRows.length !== row.value.length) {
checkBoxInput.nativeElement.indeterminate = true;
} else {
this.checkBoxInput.nativeElement.indeterminate = false;
checkBoxInput.nativeElement.indeterminate = false;
}
}
this.selectedGroupRows.set(selectedRows);
Expand All @@ -175,27 +171,27 @@ export class DataTableRowWrapperComponent<TRow extends Row = any>

@HostListener('contextmenu', ['$event'])
onContextmenu($event: MouseEvent): void {
this.rowContextmenu.emit({ event: $event, row: this.row });
this.rowContextmenu.emit({ event: $event, row: this.row() });
}

onCheckboxChange(groupSelected: boolean, group: Group<TRow>): void {
// First remove all rows of this group from `selected`
this.selected = [
...this.selected.filter(row => !group.value.find((item: TRow) => item === row))
let selected = [
...this.selected().filter(row => !group.value.find((item: TRow) => item === row))
];
// If checkbox is checked then add all rows of this group in `selected`
if (groupSelected) {
this.selected = [...this.selected, ...group.value];
selected = [...this.selected(), ...group.value];
}
// Update `selected` of DatatableComponent with newly evaluated `selected`
this.tableComponent.selected = [...this.selected];
this.tableComponent.selected = [...selected];
// Emit select event with updated values
this.tableComponent.onBodySelect({
selected: this.selected
selected: [...selected]
});
}

isGroup(row: RowOrGroup<TRow>): row is Group<TRow> {
return !!this.groupHeader;
return !!this.groupHeader();
}
}
Loading