Change Detection (Part 1)
The variable binding we explored in the last section showed us how easy it is to display variables from our TypeScript classes on our HTML page. With very little effort on our part, Angular is able to display updates to our web page as changes happen.
This is accomplished through Angular change detection. Change detection happens in a loop that is constantly occurring. In this loop every object on your page is checked for changes on every cycle of the loop. This way, if the value of a variable has changed, anything displaying that value also changes.
Let's demonstrate the loop in action.
-
Open your app.component.ts file and add the following method.
public get counter(): number { this._counter += 1; return this._counter; }
-
Add the following line of code to the bottom of app.component.html.
<p>Counter = {{ counter }}</p>
- You are probably expecting to see a counter rapidly increase on screen, however if you view your page now, you will see that the counter variable is equal to 3. If you open up the "Developer Tools" on your browser you will see an NG0100 error indicating that the value of the variable was updated so fast that Angular did not have time to finish updating the display before the value was updated again. This error only occurs on the development server and your compiled final application won't report this problem.
-
We can fix this so the error doesn't happen by wrapping our counter increment with a setTimeout method. The
setTimeout method can optionally have a timer set on it to delay processing of our code but in this case we
just want to allow the display time to update between increments. Change your get counter() function so it
appears as follows and view your page again.
public get counter(): number { setTimeout(() => { this._counter += 1; }); return this._counter; }
- You should now see the counter increase. If you wait long enough your page will crash as you will eventually reach the highest number possible and adding one to it will fail.
Our counter isn't very useful, however it does do a great job of demonstrating Angular change detection in action and highlights one of the biggest pitfalls that Angular developers must be aware of.
Performing expensive calculations repeatedly in the change detection loop can slow down your Angular application. This is true whether you are performing a single resource intensive block of code or several not-so-intensive blocks of code.
If you search online for Angular change detection the most common posts you will see instruct you to switch to onPush change detection in your components. This does greatly reduce the workload on your Angular application, but it isn't always the best solution and requires additional effort on your part to implement. We have developed many high performing MPages without ever modifying the change detection settings using the technique described below.
We will be discussing the onPush change detection strategy later in the Change Detection (Part 2) section of this lesson.
The simplest method to reduce the workload in your change detection loop is to use variables that rarely change. Displaying a variable has a very small performance cost however performing calculations and returning the results of those calculations as a variable increases the performance cost.
The example below should demonstrate this however it does use some functionality we have not yet discussed including array filters, custom interfaces (IAddress) and arrow functions (x: xType) => { code }.
// Return array of addresses filtered by City Name public addressByCity(cityName: string, addresses: IAddress[]): IAddress[] { return addresses.filter((address: IAddress) => { return address.city === cityName} ); }
The array filter operation takes place every time addressByCity is called, resulting in a potential performance impact. Imagine if the incoming addresses array has thousands of entries, and you can quickly see how this can slow down your application.
By creating a private address object in your class and populating it each time the incoming city name changes you can reduce the number of times the filter is executed.
export class AppComponent { private _addressByCity = { cityName: '', addresses: [] as IAddress[] } // Return array of addresses filtered by City Name public addressByCity(cityName: string, addresses: IAddress[]): IAddress[] { if (this._addressByCity.cityName !== cityName) { this._addressByCity.cityName = cityName; this._addressByCity.addresses = addresses.filter((address: IAddress) => { return address.city === cityName} ); } return this._addressByCity.addresses; } }
As we move into components and services we will demonstrate other ways to keep your application performing as desired. Often our content is derived from data loaded from an external source such as CCL and content is assigned to internal objects that can be directly used without relying on the method previously described. The example above would be used in cases where you need to view a subset of data you have loaded in memory.
More on setTimeout
As mentioned earlier, the setTimeout method lets you run a block of code after a specific time span has passed. It is extremely useful for returning control to your application right before a resource intensive operation takes place.
We have developed MPages that generate Excel documents in real-time which use a third-party JavaScript library. Depending on the size of the spreadsheet, the document creation can take 10-15 seconds where the MPage is completely unresponsive. By wrapping the document creation in a setTimeout method we are able to notify our users that the document is being updated as shown in the example below.
HTML
<div *ngIf="wait">Please wait... working.</div>
TypeScript
public createExcelDocument(): void { this.wait = true; setTimeOut(() => { [Code to create Excel Document] this.wait = false; }); }
In the example above, if the "wait" variable is equal to true we will display the "please wait" message. If false, the div is not shown.
When our createExcelDocument method is run the "wait" variable is immediately assigned to true allowing our HTML to update with the new content. This does not happen until the next loop cycle, so we put the Excel creation inside the time-out to indicate it should run on the next cycle. The last action in the time-out block is to set "wait" back to false which results in the message vanishing on the screen.
setTimeOut has three parameters however we will only discuss the first two. The first parameter should either be a method name or an arrow function. Arrow functions are blocks of code that only exist in the scope of where they are being called and do not have a callable name like a method does.
The second parameter is the time delay in milliseconds. If we wanted to have our code above wait 5 seconds before running we would have added 5000 as the second parameter.
setTimeOut(() => { ... your code... }, 5000);
What is "this"?
You may have noticed the use of the word "this" when referring to our class variables. "this" refers to the scope of variables and methods in our class. Anything prefixed with "this." indicates that we are using the value that is scoped at the top level of our class.
In the example below we create a private variable called "_name" which contains the text "John Simpson". The name method will return this value as it returns "this._name" which points to the class level variable.
The myName method only uses the "_name" variable scoped inside the myName method, so it will return "Bob Smith". The "_name" variable in this method only exists inside the myName method and cannot be accessed anywhere else in the code.
Even though the "_name" variable in the myName method can't be accessed outside myName, the myName method can be called anywhere inside the class. This is what happens in the bothNames method which returns values from both the name() and myName() methods.
export class AppComponent { private _name = 'John Simpson'; // Will return 'John Simpson' public get name(): string { return this._name; } // Returns 'Bob Smith' public get myName(): string { const _name = 'Bob Smith'; return _name; } // Returns 'John Simpson and Bob Smith' public get bothNames(): string { return this.name() + ' and ' + this.myName(); } }