Which is an appropriate way to use the same expression to evaluate multiple directives?

103 views Asked by At

I'm currently building a table which has a particular column where I need to proccess de data a bit to get the actual value I want to display; the value in the row's object is an ID number, so I then have to look for that ID inside an array of objects I have in a variable.

Like this:

findIndustry(industry: string) {
if (this.industries.find(x => x._id === parseInt(industry, 10))) {
  const industryResult = this.industries.find(x => x._id === parseInt(industry, 10));
  return `${industryResult.category} | ${industryResult.subcategory}`;
}
  return '(Not Set)';
}

After getting the result I can just display it on the table like this:

<ng-container matColumnDef="parentName">
      <th mat-header-cell *matHeaderCellDef mat-sort-header> Industry </th>
      <td mat-cell *matCellDef="let client" class="industry-cell">{{ findIndustry(client.industry) }}</td>
</ng-container>

But now comes the issue; based on the returned value, I want to show a title attribute, and I also want to add an ngClass directive to show the text in grey color when (Not Set) is the value:

<ng-container matColumnDef="parentName">
     <th mat-header-cell *matHeaderCellDef mat-sort-header> Industry </th>
     <td mat-cell *matCellDef="let client"
         [ngClass]="findIndustry(client.industry) === '(Not Set)' ? 'text-muted' : ''"
         class="industry-cell"
         [title]="findIndustry(client.industry)">{{ findIndustry(client.industry) }}</td>
</ng-container>

I've read that using function calls in Angular templates is a bad idea, since it would run the function on every change detection, which can be a lot of times; I'm wondering what would be an efficient way to avoid running the function so many times, or even better just avoid using the function at all; I do need that same value to apply different properties, that's the only reason I'm using it on every directive and attribute.

2

There are 2 answers

0
Owen Kelvin On BEST ANSWER

As you have stated, having a function in your html is a very bad idea...

Below is an article about how a function is called in the html

Angular’s Digest Cycle

The digest cycle is how Angular’s auto-update magic works – it’s the reason that typing into an input box automatically updates anything that refers to its value.

When the digest cycle runs, it effectively redraws everything that might have changed on the page.

Angular uses some tricks to find “everything that might have changed”, and the main technique is watchers. These watchers are created automatically when you use directives like ng-if and ng-class, and when you use bindings like {{ yourBindingHere }}.

Each one of those things registers a watcher. When Angular’s digest cycle runs, every watcher is asked to update its state. In the case of ng-class, it will re-run the function bound to it, to see if anything needs to change. This is why your controller function runs multiple times, and it’ll run again each time something changes on the page.
Source Controller Function Is Executed Multiple Times

Reactive Programming to the rescue

Angular comes preinstalled with rxjs by default. We can implement the power of Observables to solve this problem.

We can implement something like

  • Define an observable with the data
  • Define another observable to track industry id
  • Combine the two Obsevables
  • Subscribe to the resultant Observable
  • Define a way to trigger change in the industry id

Define an observable with the data

On many occasions this would be simply a response from an http request

industries$ = this.http.get<IIndustries[]>('some/api')

We can also generate an observable using from and of operators from rxjs

industries: IIndustries[] = [{ ... }, { ... }, ...]
industries$ = of(this.industries)

Define another observable to track industry id

Next step is to define a way to track the selected industry. I will use a BehaviorSubject that will emit a value on initial load and every time we call the next() function on the Subject

selectedIdSubject$ = new BehaviorSubject<number>(1) // By default 1 will be selected
selectedId$ = this.selectedIdSubject$.asObservable()

Combine the two Obsevables

Next we need to combine the two Observables. We will use combineLatest from rxjs From the official documentation

This operator is best used when you have multiple, long-lived observables that rely on each other for some calculation or determination.
Source Why use combineLatest?

industry$ = combineLatest([this.industries$, this.selectedId]).pipe(
  map(([industries, industry]) => {
   if (industries.find(x => x._id === parseInt(industry, 10))) {
     const industryResult = industries.find(x => x._id === parseInt(industry, 10));
     return `${industryResult.category} | ${industryResult.subcategory}`;
   }
     return '(Not Set)';
   }
 })
)

We are piping the Observable stream and using map operator from rxjs/operators to transform the data

Almost Done, Subscribe to the resultant Observable

We will the async pipe to perform this purpose

<ng-container matColumnDef="parentName">
      <th mat-header-cell *matHeaderCellDef mat-sort-header> Industry </th>
      <td mat-cell *matCellDef="let client" class="industry-cell">{{ industry$ | async }}</td>
</ng-container>

Finally... Define a way to trigger change in the industry id

To trigger change of the selected item, we simply call the next method on the Subject

changeSelectedId (id) {
  this.selectedIdSubject$.next(id)
}

With the above approach, angular change detection will only be triggered when changeSelectedId () function is called

0
acincognito On

What I like to do is save it in a big object like so:

findIndustry() {
//
 this.object[x._id] =  `${industryResult.category} | ${industryResult.subcategory}`;
//
}

And then in your template:

<ng-container matColumnDef="parentName">
  <th mat-header-cell *matHeaderCellDef mat-sort-header> Industry </th>
  <td mat-cell *matCellDef="let client" class="industry-cell">{{ object[client._id] }}</td>
</ng-container>

I hope it isnt too abstract (if you had a stackblitz implementation it would help).