Skip to content

Commit f538da5

Browse files
committed
refactor: convert rows to signal input
This is just an intermediate solution, so that we can turn the `rows` into a signal input. Since we internally mutate the rows, `_internalRows` has to be a `linkedSignal`. We should consider alternative approaches that only use computed.
1 parent 49d1760 commit f538da5

File tree

3 files changed

+71
-109
lines changed

3 files changed

+71
-109
lines changed

projects/ngx-datatable/src/lib/components/datatable.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
role="rowgroup"
3535
[groupRowsBy]="groupRowsBy"
3636
[groupedRows]="groupedRows"
37-
[rows]="_internalRows"
37+
[rows]="_internalRows()"
3838
[groupExpansionDefault]="groupExpansionDefault()"
3939
[scrollbarV]="scrollbarV()"
4040
[scrollbarH]="scrollbarH()"

projects/ngx-datatable/src/lib/components/datatable.component.spec.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { By } from '@angular/platform-browser';
44

55
import { SortPropDir } from '../types/public.types';
66
import { TableColumn } from '../types/table-column.type';
7+
import { toInternalColumn } from '../utils/column-helper';
8+
import { sortRows } from '../utils/sort';
79
import { DataTableBodyCellComponent } from './body/body-cell.component';
810
import { DataTableBodyRowComponent } from './body/body-row.component';
911
import { DataTableColumnCellDirective } from './columns/column-cell.directive';
@@ -238,10 +240,10 @@ describe('DatatableComponent', () => {
238240
sortBy({ column: 2 }, fixture);
239241
fixture.detectChanges();
240242

241-
expect(textContent({ row: 1, column: 1 }, fixture)).toContain('amet');
242-
expect(textContent({ row: 2, column: 1 }, fixture)).toContain('dolor');
243-
expect(textContent({ row: 3, column: 1 }, fixture)).toContain('ipsum');
244-
expect(textContent({ row: 4, column: 1 }, fixture)).toContain('lorem');
243+
expect(textContent({ row: 1, column: 1 }, fixture)).toContain('dolor');
244+
expect(textContent({ row: 2, column: 1 }, fixture)).toContain('ipsum');
245+
expect(textContent({ row: 3, column: 1 }, fixture)).toContain('lorem');
246+
expect(textContent({ row: 4, column: 1 }, fixture)).toContain('amet');
245247
expect(textContent({ row: 5, column: 1 }, fixture)).toContain('maecennas');
246248
expect(textContent({ row: 6, column: 1 }, fixture)).toContain('sed');
247249
expect(textContent({ row: 7, column: 1 }, fixture)).toContain('foo');
@@ -295,16 +297,32 @@ describe('DatatableComponent', () => {
295297
component.rows = additionalRows;
296298
fixture.detectChanges();
297299

300+
console.log('with new rows');
301+
// console.log('Bob', JSON.stringify(additionalRows));
302+
console.log(
303+
JSON.stringify(
304+
additionalRows
305+
.slice()
306+
.sort(({ state: nameA }, { state: nameB }) => nameB.localeCompare(nameA))
307+
)
308+
);
309+
console.log(
310+
JSON.stringify(
311+
sortRows(additionalRows, toInternalColumn(component.columns), [
312+
{ prop: 'state', dir: 'desc' }
313+
])
314+
)
315+
);
298316
// sort by `state` descending
299317
sortBy({ column: 2 }, fixture);
300318
fixture.detectChanges();
301319
sortBy({ column: 2 }, fixture);
302320
fixture.detectChanges();
303321

304-
expect(textContent({ row: 1, column: 1 }, fixture)).toContain('amet');
305-
expect(textContent({ row: 2, column: 1 }, fixture)).toContain('dolor');
306-
expect(textContent({ row: 3, column: 1 }, fixture)).toContain('ipsum');
307-
expect(textContent({ row: 4, column: 1 }, fixture)).toContain('lorem');
322+
expect(textContent({ row: 1, column: 1 }, fixture)).toContain('dolor');
323+
expect(textContent({ row: 2, column: 1 }, fixture)).toContain('ipsum');
324+
expect(textContent({ row: 3, column: 1 }, fixture)).toContain('lorem');
325+
expect(textContent({ row: 4, column: 1 }, fixture)).toContain('amet');
308326
expect(textContent({ row: 5, column: 1 }, fixture)).toContain('maecennas');
309327
expect(textContent({ row: 6, column: 1 }, fixture)).toContain('sed');
310328
expect(textContent({ row: 7, column: 1 }, fixture)).toContain('foo');

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

Lines changed: 44 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -117,23 +117,7 @@ export class DatatableComponent<TRow extends Row = any>
117117
/**
118118
* Rows that are displayed in the table.
119119
*/
120-
@Input() set rows(val: (TRow | undefined)[] | null | undefined) {
121-
this._rows = val ?? [];
122-
if (val) {
123-
// This will ensure that datatable detects changes on doing like this rows = [...rows];
124-
if (val.length) {
125-
this.rowDiffer.diff([]);
126-
}
127-
this._internalRows = [...val];
128-
}
129-
}
130-
131-
/**
132-
* Gets the rows.
133-
*/
134-
get rows(): (TRow | undefined)[] {
135-
return this._rows;
136-
}
120+
readonly rows = input<(TRow | undefined)[] | null | undefined>();
137121

138122
/**
139123
* This attribute allows the user to set the name of the column to group the data with
@@ -143,7 +127,7 @@ export class DatatableComponent<TRow extends Row = any>
143127
this._groupRowsBy = val;
144128
if (this._groupRowsBy) {
145129
// creates a new array with the data grouped
146-
this.groupedRows = this.groupArrayBy(this._rows, this._groupRowsBy);
130+
this.groupedRows = this.groupArrayBy(this.rows() ?? [], this._groupRowsBy);
147131
}
148132
}
149133
}
@@ -670,15 +654,15 @@ export class DatatableComponent<TRow extends Row = any>
670654
*/
671655
get allRowsSelected(): boolean {
672656
const selected = this.selected();
673-
let allRowsSelected = this.rows && selected && selected.length === this.rows.length;
657+
let allRowsSelected = this.rows() && selected && selected.length === this.rows()!.length;
674658

675659
if (this.bodyComponent && this.selectAllRowsOnPage()) {
676660
const indexes = this.bodyComponent.indexes;
677661
const rowsOnPage = indexes().last - indexes().first;
678662
allRowsSelected = selected.length === rowsOnPage;
679663
}
680664

681-
return selected && this.rows?.length !== 0 && allRowsSelected;
665+
return !!(selected && this.rows()?.length !== 0 && allRowsSelected);
682666
}
683667

684668
element = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;
@@ -687,12 +671,34 @@ export class DatatableComponent<TRow extends Row = any>
687671
bodyHeight!: number;
688672
rowCount = 0;
689673
rowDiffer: IterableDiffer<TRow | undefined> = inject(IterableDiffers).find([]).create();
674+
private readonly _rowDiffCount = signal(0);
690675

691676
_offsetX = 0;
692677
_offset = 0;
693-
_rows: (TRow | undefined)[] = [];
694678
_groupRowsBy?: keyof TRow;
695-
_internalRows: (TRow | undefined)[] = [];
679+
// TODO: should be a computed
680+
readonly _internalRows = linkedSignal(() => {
681+
this._rowDiffCount(); // to trigger recalculation when row differ detects a change
682+
let rows = this.rows()?.slice() ?? [];
683+
684+
const sorts = this.sorts();
685+
if (sorts.length && !this.externalSorting()) {
686+
rows = sortRows(rows, this._internalColumns(), this.sorts());
687+
} else if (this.treeFromRelation() && this.treeToRelation()) {
688+
rows = groupRowsByParents(
689+
rows,
690+
optionalGetterForProp(this.treeFromRelation()),
691+
optionalGetterForProp(this.treeToRelation())
692+
);
693+
}
694+
695+
if (this.ghostLoadingIndicator() && this.scrollbarV() && !this.externalPaging()) {
696+
// in case where we don't have predefined total page length
697+
rows.push(undefined); // undefined row will render ghost cell row at the end of the page
698+
}
699+
700+
return rows;
701+
});
696702
// TODO: consider removing internal modifications of the columns.
697703
// This requires a different strategy for certain properties like width.
698704
readonly _internalColumns = linkedSignal(() =>
@@ -721,26 +727,7 @@ export class DatatableComponent<TRow extends Row = any>
721727
private readonly _rowInitDone = signal(false);
722728

723729
constructor() {
724-
// Effect to handle column template changes
725730
// TODO: This should be a computed signal.
726-
effect(() => {
727-
const columns = this.columnTemplates().map(c => c.column());
728-
// Ensure that we do not listen to other properties (yet).
729-
untracked(() => this.translateColumns(columns));
730-
});
731-
732-
// Effect to handle ghost loading indicator changes
733-
effect(() => {
734-
const ghostLoading = this.ghostLoadingIndicator();
735-
// Use untracked to only subscribe to ghostLoadingIndicator changes
736-
untracked(() => {
737-
if (ghostLoading && this.scrollbarV() && !this.externalPaging()) {
738-
// in case where we don't have predefined total page length
739-
this.rows = [...this.rows, undefined]; // undefined row will render ghost cell row at the end of the page
740-
}
741-
});
742-
});
743-
744731
// Effect to handle recalculate when limit or count changes
745732
effect(() => {
746733
// Track limit and count changes
@@ -766,29 +753,21 @@ export class DatatableComponent<TRow extends Row = any>
766753
* Lifecycle hook that is called when Angular dirty checks a directive.
767754
*/
768755
ngDoCheck(): void {
769-
const rowDiffers = this.rowDiffer.diff(this.rows);
756+
const rowDiffers = this.rowDiffer.diff(this.rows());
770757
if (rowDiffers || this.disableRowCheck()) {
758+
this._rowDiffCount.update(count => count + 1);
771759
// we don't sort again when ghost loader adds a dummy row
772760
if (
773761
!this.ghostLoadingIndicator() &&
774762
!this.externalSorting() &&
775763
this._internalColumns().length
776764
) {
777765
this.sortInternalRows();
778-
} else {
779-
this._internalRows = [...this.rows];
780766
}
781767

782-
// auto group by parent on new update
783-
this._internalRows = groupRowsByParents(
784-
this._internalRows,
785-
optionalGetterForProp(this.treeFromRelation()),
786-
optionalGetterForProp(this.treeToRelation())
787-
);
788-
789768
if (this._groupRowsBy && rowDiffers) {
790769
// If a column has been specified in _groupRowsBy create a new array with the data grouped by that row
791-
this.groupedRows = this.groupArrayBy(this._rows, this._groupRowsBy);
770+
this.groupedRows = this.groupArrayBy(this.rows() ?? [], this._groupRowsBy);
792771
}
793772
if (rowDiffers) {
794773
queueMicrotask(() => {
@@ -846,18 +825,6 @@ export class DatatableComponent<TRow extends Row = any>
846825
}
847826
};
848827

849-
/**
850-
* Translates the templates to the column objects
851-
*/
852-
translateColumns(columns: TableColumn<TRow>[]) {
853-
if (columns.length) {
854-
if (!this.externalSorting() && this.rows?.length) {
855-
this.sortInternalRows();
856-
}
857-
this.cd.markForCheck();
858-
}
859-
}
860-
861828
/**
862829
* Creates a map with the data grouped by the user choice of grouping index
863830
*
@@ -1059,12 +1026,7 @@ export class DatatableComponent<TRow extends Row = any>
10591026
}
10601027

10611028
// otherwise use row length
1062-
if (this.rows) {
1063-
return this.rows.length;
1064-
}
1065-
1066-
// other empty :(
1067-
return 0;
1029+
return this.rows()?.length ?? 0;
10681030
}
10691031

10701032
/**
@@ -1074,10 +1036,8 @@ export class DatatableComponent<TRow extends Row = any>
10741036
if (!this.externalPaging()) {
10751037
if (this.groupedRows) {
10761038
return this.groupedRows.length;
1077-
} else if (this.treeFromRelation() != null && this.treeToRelation() != null) {
1078-
return this._internalRows.length;
10791039
} else {
1080-
return this.rows.length;
1040+
return this._internalRows().length;
10811041
}
10821042
}
10831043

@@ -1188,13 +1148,6 @@ export class DatatableComponent<TRow extends Row = any>
11881148
this.sortInternalRows();
11891149
}
11901150

1191-
// auto group by parent on new update
1192-
this._internalRows = groupRowsByParents(
1193-
this._internalRows,
1194-
optionalGetterForProp(this.treeFromRelation()),
1195-
optionalGetterForProp(this.treeToRelation())
1196-
);
1197-
11981151
// Always go to first page when sorting to see the newly sorted data
11991152
this.offset = 0;
12001153
this.bodyComponent.updateOffsetY(this.offset);
@@ -1221,19 +1174,23 @@ export class DatatableComponent<TRow extends Row = any>
12211174

12221175
// do the opposite here
12231176
if (!allSelected) {
1224-
this.selected.set(this._internalRows.slice(first, last).filter(row => !!row) as TRow[]);
1177+
this.selected.set(
1178+
this._internalRows()
1179+
.slice(first, last)
1180+
.filter(row => !!row) as TRow[]
1181+
);
12251182
} else {
12261183
this.selected.set([]);
12271184
}
12281185
} else {
12291186
let relevantRows: TRow[];
12301187
const disableRowCheckFn = this.disableRowCheck();
12311188
if (disableRowCheckFn) {
1232-
relevantRows = this.rows.filter(
1189+
relevantRows = (this.rows() ?? []).filter(
12331190
(row => row && !disableRowCheckFn(row)) as (row: TRow | undefined) => row is TRow
12341191
);
12351192
} else {
1236-
relevantRows = this.rows.filter(row => !!row);
1193+
relevantRows = (this.rows() ?? []).filter(row => !!row);
12371194
}
12381195
// before we splice, chk if we currently have all selected
12391196
const allSelected = this.selected().length === relevantRows.length;
@@ -1264,7 +1221,9 @@ export class DatatableComponent<TRow extends Row = any>
12641221
const row = event.row;
12651222
// TODO: For duplicated items this will not work
12661223
const treeToRel = this.treeToRelation();
1267-
const rowIndex = this._rows.findIndex(r => r && r[treeToRel!] === event.row[treeToRel!]);
1224+
const rowIndex = (this.rows() ?? []).findIndex(
1225+
r => r && r[treeToRel!] === event.row[treeToRel!]
1226+
);
12681227
this.treeAction.emit({ row, rowIndex });
12691228
}
12701229

@@ -1273,32 +1232,17 @@ export class DatatableComponent<TRow extends Row = any>
12731232
}
12741233

12751234
private sortInternalRows(): void {
1276-
// if there are no sort criteria we reset the rows with original rows
1277-
if (!this.sorts()?.length) {
1278-
this._internalRows = this._rows;
1279-
// if there is any tree relation then re-group rows accordingly
1280-
if (this.treeFromRelation() && this.treeToRelation()) {
1281-
this._internalRows = groupRowsByParents(
1282-
this._internalRows,
1283-
optionalGetterForProp(this.treeFromRelation()),
1284-
optionalGetterForProp(this.treeToRelation())
1285-
);
1286-
}
1287-
}
12881235
if (this.groupedRows?.length) {
12891236
const sortOnGroupHeader = this.sorts()?.find(
12901237
sortColumns => sortColumns.prop === this._groupRowsBy
12911238
);
1292-
this.groupedRows = this.groupArrayBy(this._rows, this._groupRowsBy!);
1239+
this.groupedRows = this.groupArrayBy(this.rows() ?? [], this._groupRowsBy!);
12931240
this.groupedRows = sortGroupedRows(
12941241
this.groupedRows,
12951242
this._internalColumns(),
12961243
this.sorts(),
12971244
sortOnGroupHeader
12981245
);
1299-
this._internalRows = [...this._internalRows];
1300-
} else {
1301-
this._internalRows = sortRows(this._internalRows, this._internalColumns(), this.sorts());
13021246
}
13031247
}
13041248
}

0 commit comments

Comments
 (0)