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.
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 likeng-if
andng-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
Angular comes preinstalled with rxjs
by default. We can implement the power of Observables
to solve this problem.
We can implement something like
Obsevables
Observable
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)
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()
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
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>
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