Understand Dom Rendering With Angular Angular

Apr 30th, 2021 - written by Kimserey with .

Change detection and DOM rendering are functionalities handled by Angular framework. When we build applications, most time, we don’t really need to pay attention in how things gets rendered as we trust Angular to do its job. But in some cases, it is necessary to understand how the rendering work, for example when using test tools like Cypress which gets instances of DOM nodes to perform action on them. We can get into situation where the re-rendering of arrays detach DOM nodes which were used by our Cypress specification. In today’s post, we will take a look at how the DOM get updated by Angular when re-rendering nodes on component input updates.

Setup

For this post, we will take the example of the following simple component:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-detail',
  template: `
    <div>
      <div></div>
      <div></div>
      <div></div>

      <ul>
        <li *ngFor="let o of objects">
          <div></div>
        </li>
      </ul>

      <ul>
        <li *ngFor="let v of values">
          <div></div>
        </li>
      </ul>
    </div>
  `,
})
export class TestComponent {
  @Input() name: string;
  @Input() object: { value: string, number: number };

  @Input() values: string[];
  @Input() objects: { value: string, number: number }[];
}

where we have:

  • a simple input,
  • an object input,
  • an array used in *ngFor of simple inputs,
  • an array used in *ngFor of objects.

Primitive Type and Object Inputs

When name changes, the only part re-rendered is the text node of the <div> containing the name binding. The text node can be retrieved using document function document.getElementById("...").firstChild.

When object changes; value is still the same in the new object, but number has changed, then only the text node containing the binding of number would be re-rendered. Note that only the DOM text that changed get re-rendered even though object is a new object, value is not re-rendered if it hasn’t changed.

Array Inputs

When values changes, only new elements get re-rendered. Elements that were already rendered don’t get re-rendered. That means that if the first array was ['test'] and we grab the instance of the first <li> via document.getElementById, if we push ['hello', 'test'] or ['test', 'hello'], the same instance we grabbed will point to the same <li>.

From Angular documentation on NgForOf:

When items are added, moved, or removed in the iterable, the directive must re-render the appropriate DOM nodes. To minimize churn in the DOM, only nodes that have changed are re-rendered.

But “nodes that have changed” is determined by equality and for objects, it is reference equality.

Hence for objects, if we start with [{ value: "test", number: 1 }] and push [{ value: "test", number: 1 }], the <li> will be re-rendered. We can confirm that by grabbing the node via document.getElementById and then pushing the next value and trying to grab the element again and doing an equality comparison, it will return false.

NgForOf TrackBy

Luckily reference equality is not the only way to check for changes with NgForOf. For objects, instead of leaving it to a simple equality check, we can use trackBy. With trackBy, we can tell Angular how we are tracking items added, updated or removed.

We can provide trackBy on the directive:

1
<li *ngFor="let o of objects; trackBy: trackBy">

pointing to a trackBy function in the component:

1
2
3
trackBy(_: number, item: { value: string, number: number }) {
  return item.value;
}

which specify how we want to check if previous and new value are the same. Here the value will define if the element has changed. So if we push [{ value: "test", number: 1 }], it will check "test" against "test" and will not re-render the <li> node.

Now you might think “ok but if we use trackBy and only check the value "test", what if we push [{ value: "test", number: 9876 }], since only the number changed, will it re-render?”

In that case, the <li> will not re-render, but the node within the <li> meaning the <div> content with the binding on the number will get re-rendered. And that’s because by default, Angular runs change detection on every components and checks all the bindings so that change will be caught and because the binding would have changed, Angular will re-render that specific DOM node.

OnPush Change Detection

Because the change detection runs on all components, Angular is able to identify mutation and update the DOM accordingly.

For example if we have:

1
2
3
4
5
6
7
@Component({
  selector: 'app-detail',
  template: `...`,
})
export class TestComponent {
  @Input() object: { value: string, number: number };
}

And we pass object from the parent component, and for example in one function we do:

1
this.object.number = 100;

If that gets triggered from an event, say button click, or setTimeout or setInterval, Angular re-render the DOM nodes that have bindings with object.number. This is known as the default change detection. Angular supports a second change detection mechanism option called OnPush. We can change the change detection mechanism on the component from the directive:

1
2
3
4
5
@Component({
  selector: 'app-detail',
  template: `hello`,
  changeDetection: ChangeDetectionStrategy.OnPush
})

The effect of that is that it stops change detection on the component and child components, and only triggers when input are updated so mutations will no longer be reflected, only reassignments on the inputs would mark the component for check.

So that’s how the DOM rendering works!

Conclusion

Today we saw how Angular re-render elements and what changes on the DOM. We started by setting up a simple component with different types of inputs and then moved on to look at how the DOM gets re-rendered based on simple updates made on inputs. We started by looking at primary type updates, then moved on to object updates and finished the updates with arrays. We also saw how trackBy could be used to avoid re-rendering the whole node when the element of an array hasn’t changed but the array is reassigned. And we completed the post by looking at the variant of change detection OnPush. I hope you liked this post and I see you on the next one!

External Sources

Designed, built and maintained by Kimserey Lam.