berno
berno

Reputation: 231

Updating of an Angular computed signal occurs only once but not for future dependencies update

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

Answers (3)

akop
akop

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

berno
berno

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

JSON Derulo
JSON Derulo

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

Related Questions