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: 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: ` ` }) export class MyComponent {} 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 }); }; 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"); } } 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, } 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); } 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"]] 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"); } 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: The placeholder attribute is set once during initialization and will never be updated again Second input: 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(); } And we can pass value like this: 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(); isEnabled = input(); } This will cause an error: 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) }); Now, we can pass the value without an error: But there's an even better way! Angular provides built-in transforma

Mar 11, 2025 - 11:57
 0
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:

 data="value" />

 [data]="'value'" />

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: `
    
    
  `
})
export class MyComponent {}

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
  });
};

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");
  }
}

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,
}

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);
}

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"]]

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");
}

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:

 placeholder="foo"/>

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

Second input:

 [placeholder]="'bar'"/>

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>();
}

And we can pass value like this:

 name="Mike"/>

 [name]="'Mike'"/>

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>();
}

This will cause an error:


 count="4" />


 isEnabled="true" />


 isEnabled />

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)
});

Now, we can pass the value without an error:


 count="6" />

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 });
}

Now, our inputs work correctly:

 count="4" />

 isEnabled="true" />

 isEnabled />

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: