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

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: