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'" />
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 {}
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
:
<input placeholder="foo"/>
The placeholder attribute is set once during initialization and will never be updated again
Second input
:
<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:
<some-component name="Mike"/>
<!-- or -->
<some-component [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:
<!-- 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 />
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:
<!-- It compiles successfully! -->
<app-some-component 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:
<app-some-component count="4" />
<app-some-component isEnabled="true" />
<app-some-component 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:
Top comments (0)