Choosing the Right Template Binding Approach in Angular

This article explores practical strategies for binding data in Angular templates, focusing on common scenarios and how to address both immediate and future challenges efficiently. To follow along, you should already be comfortable with Angular Template Syntax.

Before diving into concrete examples, let’s quickly review how Angular interpolation works for rendering text in templates.

{{title}}
title:string = "Template Syntax Binding";

Adding Logic to Templates

Consider the following snippet, then we'll break it down:

Percentage {{ totalMarks / 600 }}

This type of approach is frequently seen across many templates. However, embedding logic directly in HTML can hurt readability, make maintenance harder, and reduce reusability, especially as templates grow. HTML is not designed to host business logic; it’s better to keep such logic in the TypeScript layer.

A better pattern is to define a getter property in the component and reference that property in the template. Here’s the refactored version:

{{percentage}}
get percentage() {
    return this.totalMarks / 600;
}

This approach allows us to reuse the same logic in multiple places, both in the template and within the component class if needed.

Invoking Methods from the Template

When I first started with Angular, I often called component methods directly from templates in smart components. Since the data came from a parent, calling a method from the HTML felt straightforward and convenient — shortcuts are always tempting. Here’s an example of what I mean:

{{getOffer(amount)}}
@Input() amount:number

getOffer(amount:number){
    if(amount > 3500  && amount < 4999)
       return `You will get 20% off on 5k purchase`;
    else if (amount > 5000)
       return `You will get 30% off on 7k purchase`;
    else
       return `5% off on your existing purchase.`;
}

While Angular docs don’t forbid this, there are hidden risks. If the method contains complex business rules, rendering can become slow and performance may degrade noticeably.

Let’s refactor the above:

{{offerMessage}}
offerMessage: string;

@Input() set amount(value: number) {
    let message: string = '';
    if (value > 5000)
        message = `You will get 30% off on 7k purchase`;
    else if (value > 3500 && value < 4999)
        message = `You will get 20% off on 5k purchase`;
    else
        message = `5% off on your existing purchase.`;
    this.offerMessage = message;
}

In the improved version, we use a setter for the amount property. Whenever the setter runs, it updates offerMessage based on the new value. This removes the need to call a method in the template. You might wonder why not use ngOnChanges instead. That’s a valid option, but as complexity grows, managing many if clauses inside ngOnChanges can become unwieldy and less scalable. Both approaches have their place — if I have multiple @Input properties, I lean toward setters; otherwise, ngOnChanges works fine.

Now, a common question arises: should we use getter properties or methods?

As a rule of thumb, methods represent actions while properties represent data. Getters are ideal when there’s minimal computation, when proxying another object’s value, or when hiding private variables. Methods, on the other hand, are better suited for expensive logic or asynchronous operations.

Here’s a scenario where neither a setter nor ngOnChanges fits well:

The backend supplies a student’s name and total marks, and we need to display both the name and a calculated grade in a table.

For this case, I initially resorted to calling a method from the template:

<table>
  <tr *ngFor="let student of students">
    <td>{{student.name}}</td>
    <td>{{getGrade(student)}}</td>
  </tr>
</table>
students:any[] = [{ id: 1, name: 'John', marks: 65 }, ...]

getGrade(marks: number) {
    let grade: string = 'F';
    if (marks >= 85)
        grade = 'S'
    else if (marks > 60 && marks < 85)
        grade = 'A'
    return grade;
}

To address this, we should apply the Single Responsibility Principle. Below is the restructured solution:

<td>{{student.grade}}</td>
students:Student[] = [new Student({ id: 1, name: 'John', marks: 65 }), ...]
export class Student {
    constructor(data: Partial<StudentModel>) {
        this.id = data.id;
        this.name = data.name;
        this.grade = this.getGrade(data.marks);
        this.marks = data.marks;
    }

    id: number;
    name: string;
    marks: number;
    grade: string;

    private getGrade(marks: number) {
        let grade: string = 'F';
        if (marks >= 85)
            grade = 'S'
        else if (marks > 60 && marks < 85)
            grade = 'A'
        return grade;
    }
}

You might have some questions about this approach.

Why create a class when we could do it without one?

We could, but that would undermine the Single Responsibility Principle. Without a class, grade calculation logic would end up scattered across services, components, or templates, leading to duplication and mess whenever the entity is reused with slight variations in different components. A dedicated class encapsulates the behavior and lets us reuse it consistently across components, improving maintainability and reusability.

Classes shine when we need to initialize properties and methods to build objects or enforce business rules.

Why not use a pure pipe?

Pure pipes are useful and could save some iteration overhead, but pipes aren’t designed for scenarios like this. I’ll cover this in detail in the upcoming article “Using Angular in the right way: Pipes”.

Why skip memoization in the grading method?

Memoization helps with heavy computational tasks and can boost performance significantly. Our case, however, isn’t heavy enough to justify it. Adding memoization here could increase memory usage and complicate the code, especially if multiple template methods are involved.

So far we’ve focused on accessing object properties instead of methods in templates, but that alone isn’t enough for optimal rendering. Why?

Two further improvements can be made: using ngFor with trackBy and applying the OnPush change detection strategy.

Efficient *ngFor Binding

When any row in the student list changes, Angular currently recomputes the entire list. With large datasets, this becomes a performance bottleneck. By using a „`trackBy„` function, we can tell Angular how to identify items in the collection, so it only repaints elements that actually changed. See the modified implementation below:

<tr *ngFor="let student of students; trackBy:trackByFn">
    <td>{{student.name}}</td>
    <td>{{student.grade}}</td>
</tr>
trackByFn(index, item) {
    return item.id;
}

Leveraging OnPush Change Detection

By default, Angular runs change detection on every component whenever anything changes in the app, checking whether any template expression values have shifted. As components grow more complex, this checking becomes costlier. With ChangeDetectionStrategy.OnPush, Angular only checks components when their input references change, rather than evaluating every property value. This can significantly cut down on unnecessary checks and improve performance. When an object is updated, its updated reference is what propagates to the view.

With OnPush, change detection triggers only when:

  • The input reference changes.
  • A native DOM event fires from the component or one of its children.
  • Change detection is invoked manually via the detectChanges method of the ChangeDetectorRef class.
  • An async pipe receives a new value from an observable.

Here’s how to apply it:

@Component({
    selector: 'app-product',
    template: `...`,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductComponent { ... }

For further reading, check out these articles on Change Detection by Max Koretskyi.

Summary

The strategies discussed here let us assemble code modularly based on specific needs. Preferring object properties over methods in templates enhances both readability and performance. There’s much more to explore around bindings and elegant reactive forms, so stay tuned for upcoming posts. In the meantime, Armen Vardanyan’s articles on Angular Forms: Useful Tips and Angular: The unexpected are excellent resources worth checking out.