DEV Community

Cover image for Understanding How Angular Processes Template Bindings
Denis Botov
Denis Botov

Posted on

Understanding How Angular Processes Template Bindings

I’ve always been curious about how Angular works under the hood, and I enjoy diving deeper into seemingly simple concepts. Recently, I started wondering if there’s any difference between these two ways of passing values to attributes or component inputs:

<element data="value" />
<!-- or -->
<element [data]="'value'" />
Enter fullscreen mode Exit fullscreen mode

For many Angular developers, this might seem obvious, but I wanted to explore how Angular processes these expressions under the hood

Template compilation

Let's start by creating a simple component with two different ways of setting the placeholder attribute for an input element:

@Component({
  selector: 'app-my-component',
  standalone: true,
  template: `
    <input placeholder="foo"/>
    <input [placeholder]="'bar'"/>
  `
})
export class MyComponent {}
Enter fullscreen mode Exit fullscreen mode

To better understand the output, we disable build optimization in angular.json (optimization: false) and build the project

After compilation, our component turns into the following:

var MyComponent = class _MyComponent {
  static ɵfac = function MyComponent_Factory(__ngFactoryType__) {
    return new (__ngFactoryType__ || _MyComponent)();
  };
  static ɵcmp = ɵɵdefineComponent({
    type: _MyComponent,
    selectors: [["app-my-component"]],
    standalone: true,
    features: [ɵɵStandaloneFeature],
    decls: 2,
    vars: 1,
    consts: [["placeholder", "foo"], [3, "placeholder"]],
    template: function MyComponent_Template(rf, ctx) {
      if (rf & 1) {
        ɵɵelement(0, "input", 0)(1, "input", 1);
      }
      if (rf & 2) {
        ɵɵadvance();
        ɵɵproperty("placeholder", "bar");
      }
    },
    encapsulation: 2
  });
};
Enter fullscreen mode Exit fullscreen mode

Now, let's focus on this part of the compiled component:

consts: [["placeholder", "foo"], [3, "placeholder"]], 
template: function MyComponent_Template(rf, ctx) {
  if (rf & 1) {
    ɵɵelement(0, "input", 0)(1, "input", 1);
  }
  if (rf & 2) {
    ɵɵadvance();
    ɵɵproperty("placeholder", "bar");
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, we can see that our HTML-like template has been converted into a function with instructions. This function can be executed in two different modes depending on the rf (RenderFlag) value

export const enum RenderFlags {
  /* Whether to run the creation block (e.g. create elements and directives) */
  Create = 0b01,

  /* Whether to run the update block (e.g. refresh bindings) */
  Update = 0b10,
}
Enter fullscreen mode Exit fullscreen mode

The Create block (if (rf & 1) { ... }) runs only once during the component's initial rendering, while the Update block (if (rf & 2) { ... }) runs on every subsequent execution to apply updates

Create

In our case, the Create block is responsible for creating two input elements:

if (rf & 1) {
  ɵɵelement(0, "input", 0)(1, "input", 1);
}
Enter fullscreen mode Exit fullscreen mode

Here ɵɵelement takes three arguments:

  • The element's index
  • The element's name
  • The index of the element's attributes in the consts array

Let's take a closer look at the consts array:

consts: [["placeholder", "foo"], [3, "placeholder"]]
Enter fullscreen mode Exit fullscreen mode

The first input points to ["placeholder", "foo"], which is simply [attributeName, attributeValue]

For the second input, the format is different: [3, "placeholder"]

The first element in this array (3) is an AttributeMarker. It tells Angular that this is not a regular attribute but a binding

At this stage, the second input does not yet have a value assigned to its placeholder attribute

Update

if (rf & 2) {
  ɵɵadvance();
  ɵɵproperty("placeholder", "bar");
}
Enter fullscreen mode Exit fullscreen mode

In the Update block, we see the ɵɵadvance() instruction, which moves the index forward to the second element (so we just skip the first input)

Then, ɵɵproperty("placeholder", "bar") assigns the value "bar" to the placeholder attribute

Let's compare

First input:

<input placeholder="foo"/>
Enter fullscreen mode Exit fullscreen mode

The placeholder attribute is set once during initialization and will never be updated again

Second input:

<input [placeholder]="'bar'"/>
Enter fullscreen mode Exit fullscreen mode

The placeholder attribute is set on each template update

Of course, Angular optimizes updates by comparing the new value with the previous one, preventing unnecessary DOM changes. However, as a general rule, it's better not to use bindings for static values

Component's inputs

So far, we've been dealing with native attributes. But what about component inputs?

If a component expects a string input, there is no difference:

class SomeComponent {
  name = input<string>();
}
Enter fullscreen mode Exit fullscreen mode

And we can pass value like this:

<some-component name="Mike"/>
<!-- or -->
<some-component [name]="'Mike'"/>
Enter fullscreen mode Exit fullscreen mode

Since "Mike" is a static string, the first approach is preferable

However, what if the input expects a number or boolean?

export class SomeComponent {
  count = input<number>();
  isEnabled = input<boolean>();
}
Enter fullscreen mode Exit fullscreen mode

This will cause an error:

<!--  NG2: Type 'string' is not assignable to type 'number' -->
<app-some-component count="4" />

<!-- NG2: Type '"true"' is not assignable to type 'boolean | undefined' -->
<app-some-component isEnabled="true" />

<!-- NG2: Type '""' is not assignable to type 'boolean | undefined' -->
<app-some-component isEnabled />
Enter fullscreen mode Exit fullscreen mode

Input transform

To handle this, we can specify a transform function for the input (available since Angular v16.1).

For example, a simple transformation function for converting a string to a number:

count = input(0, {
  transform: (value: string | undefined) => Number(value)
});
Enter fullscreen mode Exit fullscreen mode

Now, we can pass the value without an error:

<!-- It compiles successfully! -->
<app-some-component count="6" />
Enter fullscreen mode Exit fullscreen mode

But there's an even better way!

Angular provides built-in transformation utilities for common cases:

Let's use them:

export class SomeComponent {
  count = input(0, { transform: numberAttribute});
  isEnabled = input(false, { transform: booleanAttribute });
}
Enter fullscreen mode Exit fullscreen mode

Now, our inputs work correctly:

<app-some-component count="4" />

<app-some-component isEnabled="true" />

<app-some-component isEnabled />
Enter fullscreen mode Exit fullscreen mode

Conclusion

Understanding how Angular compiles templates helps us write more efficient and optimized code. A key takeaway is that bindings should be avoided for static values to prevent unnecessary updates.

When working with component inputs, Angular provides input transformation utilities like numberAttribute and booleanAttribute, making it easier to handle non-string values without manual conversion.

If you're interested in learning more about how the Angular compiler works, I highly recommend checking out:

Top comments (0)