Reputation: 231
I'm trying to filter an array based on the value of a signal , I think computed signal is the best option for this purpose. My goal is to show only the results that match (via a custom filtering function) the corresponding feedback filter set in another component via a toggle button.
Parent component ts file
export interface Data {
name: string;
total_fb: number;
last30: number;
trend: string;
}
...
dataArray = [
{name: 'Prod XY', total_fb: 4.5, last30: 3.6, trend: 'up'},
{name: 'Prod lorem ipsum Z', total_fb: 4.2, last30: 3.3, trend: 'up'},
{name: 'Prod VBG', total_fb: 3.3, last30: 3.7, trend: 'up'},
{name: 'Prod XYZ', total_fb: 3.7, last30: 2.9, trend: 'down'},
...
];
filtersProd = signal({source: '1', feedback: 'all'});
filteredProdData = computed(() => {
const rating = this.filtersProd().feedback;
return this.dataArray.filter((el) => this.filterByFeedBackRating(el, rating));
});
filterByFeedBackRating = (data: Data, rating_value: string) => {
if (rating_value === 'all') {
return true;
} else if (rating_value === '4+' && data.total_fb >= 4) {
return true;
} else if (rating_value === '3' && data.total_fb >= 3 && data.total_fb < 4) {
return true;
} else if (rating_value === '1/2' && data.total_fb < 3) {
return true;
}
return false;
}
Parent component template
<div class="pre-content">
<pre>source: {{filtersProd().source}} rating: {{filtersProd().feedback}}</pre>
<app-filter-source-feedback [(filters)]="filtersProd"></app-filter-source-feedback>
</div>
<div class="content flex">
<app-table-performance [data]="filteredProdData()"></app-table-performance>
</div>
FilterSourceFeedback component ts file
filters = model.required<{feedback: string|number, source: string}>();
FilterSourceFeedback component template
<div class="toggle">
<label>Feedback</label>
<mat-button-toggle-group [(value)]="filters().feedback" hideSingleSelectionIndicator="true" name="feedback" aria-label="feedback rating">
<mat-button-toggle value="all">all</mat-button-toggle>
<mat-button-toggle value="4+">4+</mat-button-toggle>
<mat-button-toggle value="3">3</mat-button-toggle>
<mat-button-toggle value="1/2">1/2</mat-button-toggle>
</mat-button-toggle-group>
</div>
TablePerformance component ts file
data = input<Data[]>([]);
dataSource = computed(() => new MatTableDataSource(this.data()));
columnsToDisplay = ['name', 'total_fb', 'last30', 'trend'];
TablePerformance component template
<table mat-table [dataSource]="dataSource()">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> Name </th>
<td mat-cell *matCellDef="let tourStat"> {{tourStat.name}} </td>
</ng-container>
<ng-container matColumnDef="total_fb">
<th mat-header-cell *matHeaderCellDef> Total </th>
<td mat-cell *matCellDef="let tourStat"> {{tourStat.total_fb}} </td>
</ng-container>
...
<tr mat-header-row *matHeaderRowDef="columnsToDisplay"></tr>
<tr mat-row *matRowDef="let row; columns: columnsToDisplay"></tr>
</table>
When I change the value of the feedback toggle button group the filtersProd
signal in the parent component is in perfect sync with the model signal filters
in the child component. The problem is that the array returned by the computed signal filteredProdData
is filtered only with the initial value (if I change the initial value I obtain the correct filtered results in the table), subsequent updates do not filter the initial array as if the computed signal don't 'listen' the updates of its dependencies.
Since last time I worked with Angular was a version where the signals feature was not released yet I don't understand where I'm going wrong or what I'm missing.
Upvotes: 0
Views: 132
Reputation: 7871
You have to trigger somewhere your signal-chain.
Your signal-chain looks like this:
filtersProd @Parent
|
v
filteredProdData @Parent // it uses "dataArray" directly
|
v
data @TablePerformance
|
v
dataSource @TablePerformance
So, when your dataArray
changes, then the signal-chain will be not triggered. One option is to turn your dataArray
to a signal. Then you will something like this:
filtersProd @Parent dataArray @Parant
| ______________________|
| |
v v
filteredProdData @Parent
|
v
data @TablePerformance
|
v
dataSource @TablePerformance
Upvotes: 0
Reputation: 231
I thought my problem was the referential equality since my signal is an object (as suggest by @JSON Derulo) , unfortunately I have implemented a custom equality function in the signal
filtersProd = signal(
{source: '1', feedback: 'all'},
{equal: (a, b) => {
return a.feedback === b.feedback && a.source === b.source;
}
}
);
but it still doesn't work.
I solved the issue separating the input value and the output event on the toggle button and updating the values of the object with the update method of the signal within the output handler.
FilterSourceFeedback html
<mat-button-toggle-group (valueChange)="feedbackHandler($event)" [value]="filters().feedback" hideSingleSelectionIndicator="true" name="feedback" aria-label="feedback rating">
<mat-button-toggle value="all">all</mat-button-toggle>
<mat-button-toggle value="4+">4+</mat-button-toggle>
<mat-button-toggle value="3">3</mat-button-toggle>
<mat-button-toggle value="1/2">1/2</mat-button-toggle>
</mat-button-toggle-group>
FilterSourceFeedback component
feedbackHandler(e: string) {
const newFilters = {source: this.filters().source, feedback: e};
this.filters.update(() => newFilters);
}
In this way the computed signal is updated every new selection of the feedback and consequently the result array.
Upvotes: 0
Reputation: 17758
The reason why your code is not working is because by default, Angular signals use referential equality to track whether the value has changed. You are storing objects in your Signal, this means that the value is treated as unchanged as long as the object's reference is the same. If you are just updating a property in your update, like you do when you are binding with [(value)]="filters().feedback"
, the reference remains unchanged. Angular thinks that the value is not changed and thus the recomputation does not happen.
To fix the issue in a clean way, you should have separate signals for the filter's source
and feedback
, instead of storing them both in an object. You can use two-way binding on a signal directly (without reading the value) like the following:
<mat-button-toggle-group [(value)]="filtersFeedback"> ...
Assuming you have a filtersFeedback
signal in the component, as I suggested.
Upvotes: 0