Skip to content

Commit 2f020c4

Browse files
committed
Signal-Migration: Matrix Component
No need to call ActivitiesBeingDisplayed every time the filter is changed as dataSource is a computed() value now which depends on other signal() values - levels, filtersTag and filtersDim
1 parent f96e1e0 commit 2f020c4

3 files changed

Lines changed: 86 additions & 117 deletions

File tree

src/app/pages/matrix/matrix.component.html

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<label class="chip-filter-label">Dimension Filter</label>
66
<mat-chip-listbox #chipList selectable multiple>
77
<mat-chip-option
8-
*ngFor="let dim of filtersDim | keyvalue"
8+
*ngFor="let dim of filtersDim() | keyvalue"
99
(selectionChange)="toggleDimensionFilters($event)"
1010
[value]="dim.key"
1111
[selected]="dim.value">
@@ -20,7 +20,7 @@
2020
<label class="chip-filter-label">Activity Tag Filter</label>
2121
<mat-chip-listbox selectable multiple>
2222
<mat-chip-option
23-
*ngFor="let tag of filtersTag | keyvalue"
23+
*ngFor="let tag of filtersTag() | keyvalue"
2424
(selectionChange)="toggleTagFilters($event)"
2525
[value]="tag.key"
2626
[selected]="tag.value">
@@ -36,7 +36,7 @@
3636
<!-- eslint-disable -->
3737
<table
3838
mat-table
39-
[dataSource]="dataSource"
39+
[dataSource]="dataSource()"
4040
class="mat-elevation-z8 matrix-table">
4141

4242
<!-- Category Column -->
@@ -58,7 +58,7 @@
5858
</td>
5959
</ng-container>
6060

61-
<ng-container *ngFor="let level of levels | keyvalue">
61+
<ng-container *ngFor="let level of levels() | keyvalue">
6262
<ng-container matColumnDef="{{ level.key }}">
6363
<th mat-header-cell *matHeaderCellDef>{{ level.value }}</th>
6464
<td mat-cell *matCellDef="let element">
@@ -87,13 +87,13 @@
8787

8888
<!-- No data message -->
8989
<tr class="mat-row" *matNoDataRow>
90-
<td class="mat-cell" [attr.colspan]="columnNames.length">
90+
<td class="mat-cell" [attr.colspan]="columnNames().length">
9191
No activities match the selected filters
9292
</td>
9393
</tr>
9494

95-
<tr mat-header-row *matHeaderRowDef="columnNames"></tr>
96-
<tr mat-row *matRowDef="let row; columns: columnNames"></tr>
95+
<tr mat-header-row *matHeaderRowDef="columnNames()"></tr>
96+
<tr mat-row *matRowDef="let row; columns: columnNames()"></tr>
9797
<tr mat-footer-row></tr>
9898
</table>
9999
</div>

src/app/pages/matrix/matrix.component.spec.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -65,35 +65,35 @@ describe('MatrixComponent', () => {
6565

6666
it('should build matrix data', () => {
6767
// Verify the data was loaded
68-
expect(component.MATRIX_DATA).toBeTruthy();
69-
expect(component.MATRIX_DATA.length).toBeGreaterThan(0);
70-
expect(component.MATRIX_DATA[0].Category).toBe('Test Category');
71-
expect(component.MATRIX_DATA[0].Dimension).toBe('Test Dimension');
72-
expect(component.MATRIX_DATA[0].level1.length).toBe(2);
68+
expect(component.MATRIX_DATA()).toBeTruthy();
69+
expect(component.MATRIX_DATA().length).toBeGreaterThan(0);
70+
expect(component.MATRIX_DATA()[0].Category).toBe('Test Category');
71+
expect(component.MATRIX_DATA()[0].Dimension).toBe('Test Dimension');
72+
expect(component.MATRIX_DATA()[0].level1.length).toBe(2);
7373

7474
// Verify filters were initialized
75-
expect(Object.keys(component.filtersTag)).toContain('tag1');
76-
expect(Object.keys(component.filtersDim)).toContain('Test Dimension');
75+
expect(Object.keys(component.filtersTag())).toContain('tag1');
76+
expect(Object.keys(component.filtersDim())).toContain('Test Dimension');
7777
});
7878

7979
it('should filter data when tag filter is selected', fakeAsync(() => {
80-
expect(component.dataSource.data.length).toBe(2);
81-
expect(component.dataSource.data[0].level1.length).toBe(2);
80+
expect(component.dataSource().length).toBe(2);
81+
expect(component.dataSource()[0].level1.length).toBe(2);
8282

8383
// Toggle tag filter on
8484
console.log('Turn chip filter on');
8585
component.toggleTagFilters(createChipSelectionEvent('tag1', true));
8686
tick(); // flush the setTimeout inside toggleTagFilters
87-
expect(component.filtersTag['tag1']).toBeTrue();
88-
expect(component.dataSource.data.length).toBe(1);
89-
expect(component.dataSource.data[0].level1.length).toBe(1);
87+
expect(component.filtersTag()['tag1']).toBeTrue();
88+
expect(component.dataSource().length).toBe(1);
89+
expect(component.dataSource()[0].level1.length).toBe(1);
9090

9191
// Toggle tag filter off again
9292
console.log('Turn chip filter off');
9393
component.toggleTagFilters(createChipSelectionEvent('tag1', false));
9494
tick(); // flush the setTimeout inside toggleTagFilters
95-
expect(component.filtersTag['tag1']).toBeFalse();
96-
expect(component.dataSource.data.length).toBe(2);
97-
expect(component.dataSource.data[0].level1.length).toBe(2);
95+
expect(component.filtersTag()['tag1']).toBeFalse();
96+
expect(component.dataSource().length).toBe(2);
97+
expect(component.dataSource()[0].level1.length).toBe(2);
9898
}));
9999
});

src/app/pages/matrix/matrix.component.ts

Lines changed: 64 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import { Component, OnInit, ElementRef, ViewChild } from '@angular/core';
1+
import { Component, OnInit, ElementRef, ViewChild, signal, computed } from '@angular/core';
22
import { FormControl } from '@angular/forms';
3-
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
3+
import { MatTableModule } from '@angular/material/table';
44
import { Router, NavigationExtras } from '@angular/router';
55
import { LoaderService } from 'src/app/service/loader/data-loader.service';
66
import { Activity, ActivityStore, Data } from 'src/app/model/activity-store';
77
import { UntilDestroy } from '@ngneat/until-destroy';
88
import { MatChipListbox, MatChipSelectionChange, MatChipsModule } from '@angular/material/chips';
9-
import { deepCopy } from 'src/app/util/util';
109
import { DataStore } from 'src/app/model/data-store';
1110
import { perfNow } from 'src/app/util/util';
1211
import { SettingsService } from 'src/app/service/settings/settings.service';
@@ -50,14 +49,52 @@ export class MatrixComponent implements OnInit {
5049
Routing: string = '/activity-description';
5150
dataStore: DataStore = new DataStore();
5251
data: Data = {};
53-
levels: Partial<Record<LevelKey, string>> = {};
54-
filtersTag: Record<string, boolean> = {};
55-
filtersDim: Record<string, boolean> = {};
56-
columnNames: string[] = [];
57-
allCategoryNames: string[] = [];
58-
allDimensionNames: string[] = [];
59-
MATRIX_DATA: MatrixRow[] = [];
60-
dataSource: any = new MatTableDataSource<MatrixRow>(this.MATRIX_DATA);
52+
levels = signal<Partial<Record<LevelKey, string>>>({});
53+
filtersTag = signal<Record<string, boolean>>({});
54+
filtersDim = signal<Record<string, boolean>>({});
55+
columnNames = signal<string[]>([]);
56+
MATRIX_DATA = signal<MatrixRow[]>([]);
57+
58+
dataSource = computed((): MatrixRow[] => {
59+
const hasDimFilter = Object.values(this.filtersDim()).some(v => v === true);
60+
const hasTagFilter = Object.values(this.filtersTag()).some(v => v === true);
61+
62+
if (!hasTagFilter && !hasDimFilter) {
63+
return this.MATRIX_DATA();
64+
}
65+
66+
let itemsStage1: MatrixRow[];
67+
if (!hasDimFilter) {
68+
itemsStage1 = this.MATRIX_DATA();
69+
} else {
70+
itemsStage1 = this.MATRIX_DATA().filter(srcItem => this.filtersDim()[srcItem.Dimension]);
71+
}
72+
73+
if (!hasTagFilter) {
74+
return itemsStage1;
75+
}
76+
77+
const itemsStage2: MatrixRow[] = [];
78+
for (const srcItem of itemsStage1) {
79+
let hasContent = false;
80+
const trgItem: Partial<MatrixRow> = {};
81+
82+
for (const lvl of Object.keys(this.levels()) as LevelKey[]) {
83+
const tmp = srcItem[lvl].filter(activity => this.hasTag(activity));
84+
if (tmp.length > 0) {
85+
trgItem[lvl] = tmp;
86+
hasContent = true;
87+
}
88+
}
89+
90+
if (hasContent) {
91+
trgItem.Category = srcItem.Category;
92+
trgItem.Dimension = srcItem.Dimension;
93+
itemsStage2.push(trgItem as MatrixRow);
94+
}
95+
}
96+
return itemsStage2;
97+
});
6198

6299
/* eslint-disable */
63100
constructor(
@@ -69,13 +106,8 @@ export class MatrixComponent implements OnInit {
69106
/* eslint-enable */
70107

71108
reset() {
72-
for (let dim in this.filtersDim) {
73-
this.filtersDim[dim] = false;
74-
}
75-
for (let tag in this.filtersTag) {
76-
this.filtersTag[tag] = false;
77-
}
78-
this.updateActivitiesBeingDisplayed();
109+
this.filtersDim.set(Object.fromEntries(Object.keys(this.filtersDim()).map(k => [k, false])));
110+
this.filtersTag.set(Object.fromEntries(Object.keys(this.filtersTag()).map(k => [k, false])));
79111
}
80112

81113
ngOnInit(): void {
@@ -99,18 +131,15 @@ export class MatrixComponent implements OnInit {
99131
if (!dataStore.activityStore) {
100132
return;
101133
}
102-
// this.data = this.activities.getData();
103-
this.allCategoryNames = dataStore?.activityStore?.getAllCategoryNames() || [];
104-
this.allDimensionNames = dataStore?.activityStore?.getAllDimensionNames() || [];
105-
106-
this.MATRIX_DATA = this.buildMatrixData(dataStore.activityStore);
107-
this.levels = this.buildLevels(dataStore.getLevelTitles(this.settings.getMaxLevel())); // eslint-disable-line
108-
this.filtersTag = this.buildFiltersForTag(dataStore.activityStore.getAllActivities()); // eslint-disable-line
109-
this.filtersDim = this.buildFiltersForDim(this.MATRIX_DATA);
110-
this.columnNames = ['Category', 'Dimension'];
111-
this.columnNames.push(...Object.keys(this.levels));
112-
113-
this.dataSource.data = deepCopy(this.MATRIX_DATA);
134+
const allCategoryNames = dataStore?.activityStore?.getAllCategoryNames() || [];
135+
const allDimensionNames = dataStore?.activityStore?.getAllDimensionNames() || [];
136+
137+
this.MATRIX_DATA.set(this.buildMatrixData(dataStore.activityStore, allDimensionNames));
138+
const levelsObj = this.buildLevels(dataStore.getLevelTitles(this.settings.getMaxLevel())); // eslint-disable-line
139+
this.levels.set(levelsObj);
140+
this.filtersTag.set(this.buildFiltersForTag(dataStore.activityStore.getAllActivities())); // eslint-disable-line
141+
this.filtersDim.set(this.buildFiltersForDim(this.MATRIX_DATA()));
142+
this.columnNames.set(['Category', 'Dimension', ...Object.keys(levelsObj)]);
114143
}
115144

116145
buildFiltersForTag(activities: Activity[]): Record<string, boolean> {
@@ -145,9 +174,9 @@ export class MatrixComponent implements OnInit {
145174
return levels;
146175
}
147176

148-
buildMatrixData(activityStore: ActivityStore): MatrixRow[] {
177+
buildMatrixData(activityStore: ActivityStore, allDimensionNames: string[]): MatrixRow[] {
149178
let matrixData: MatrixRow[] = [];
150-
for (let dim of this.allDimensionNames) {
179+
for (let dim of allDimensionNames) {
151180
let matrixRow: Partial<MatrixRow> = {};
152181
for (let level = 1; level <= activityStore.getMaxLevel(); level++) {
153182
let activities: Activity[] = activityStore.getActivities(dim, level);
@@ -174,9 +203,8 @@ export class MatrixComponent implements OnInit {
174203
const selected = event.selected;
175204

176205
setTimeout(() => {
177-
this.filtersTag[value] = selected;
206+
this.filtersTag.update(f => ({ ...f, [value]: selected }));
178207
console.log(`${perfNow()}: Matrix: Chip flip Tag '${value}: ${selected}`);
179-
this.updateActivitiesBeingDisplayed();
180208
});
181209
}
182210

@@ -187,77 +215,18 @@ export class MatrixComponent implements OnInit {
187215
const selected = event.selected;
188216

189217
setTimeout(() => {
190-
this.filtersDim[value] = selected;
218+
this.filtersDim.update(f => ({ ...f, [value]: selected }));
191219
console.log(`${perfNow()}: Matrix: Chip flip Dim '${value}: ${selected}`);
192-
this.updateActivitiesBeingDisplayed();
193220
});
194221
}
195222

196223
@ViewChild('rowInput') rowInput!: ElementRef<HTMLInputElement>;
197224
@ViewChild('activityInput') activityInput!: ElementRef<HTMLInputElement>;
198225

199-
updateActivitiesBeingDisplayed(): void {
200-
let hasDimFilter = Object.values(this.filtersDim).some(v => v === true);
201-
let hasTagFilter = Object.values(this.filtersTag).some(v => v === true);
202-
203-
if (!hasTagFilter && !hasDimFilter) {
204-
this.dataSource.data = this.MATRIX_DATA;
205-
return;
206-
}
207-
208-
// Apply dimension filters
209-
let itemsStage1: MatrixRow[] = [];
210-
if (!hasDimFilter) {
211-
itemsStage1 = this.MATRIX_DATA;
212-
} else {
213-
for (let srcItem of this.MATRIX_DATA) {
214-
if (this.filtersDim[srcItem.Dimension]) {
215-
itemsStage1.push(srcItem as MatrixRow);
216-
}
217-
}
218-
}
219-
220-
// Apply tag filters
221-
let itemsStage2: MatrixRow[];
222-
if (!hasTagFilter) {
223-
itemsStage2 = itemsStage1;
224-
} else {
225-
itemsStage2 = [];
226-
for (let srcItem of itemsStage1) {
227-
let hasContent = false;
228-
229-
let trgItem: Partial<MatrixRow> = {};
230-
if (hasTagFilter) {
231-
// Include activities withing each level, that match the tag filter
232-
233-
// If tag filter is active, filter activities by tags
234-
for (let lvl of Object.keys(this.levels) as LevelKey[]) {
235-
let tmp: Activity[];
236-
tmp = srcItem[lvl].filter(activity => this.hasTag(activity));
237-
if (tmp.length > 0) {
238-
trgItem[lvl] = tmp;
239-
hasContent = true;
240-
}
241-
}
242-
243-
// Only include the row if it has any activities after tag filtering
244-
if (hasContent) {
245-
// Copy metadata, since the element has remaining activities after filtering
246-
trgItem.Category = srcItem.Category;
247-
trgItem.Dimension = srcItem.Dimension;
248-
249-
itemsStage2.push(trgItem as MatrixRow);
250-
}
251-
}
252-
}
253-
}
254-
this.dataSource.data = itemsStage2;
255-
}
256-
257226
hasTag(activity: Activity): boolean {
258227
if (activity.tags) {
259228
for (let tagName of activity.tags) {
260-
if (this.filtersTag[tagName]) return true;
229+
if (this.filtersTag()[tagName]) return true;
261230
}
262231
}
263232
return false;

0 commit comments

Comments
 (0)