When a class is declared, decorators are invoked—not when an object is created.
That statement is accurate.
As previously mentioned by @H.B., the proof lies within the transpilation of the code.
var TestComponent = /** @class */ (function () {
function TestComponent() {
this.testString = "default string";
console.log(this.testString);
}
__decorate([
core_1.Input(),
__metadata("design:type", String)
], TestComponent.prototype, "testString", void 0);
TestComponent = __decorate([
core_1.Component({
selector: 'app-test',
template: '{{testString}}'
}),
__metadata("design:paramtypes", [])
], TestComponent);
return TestComponent;
}());
We will now proceed to unpack where the misunderstanding occurred.
Step 1. Metadata Provision
The outcome of running the code shows "default string" in the console instead of
"altered string". This denotes that decorators are called after the
class constructor runs.
Before jumping to conclusions, understanding the purpose of the @Input()
decorator is crucial.
The Angular @Input
decorator simply enhances component properties with information.
This information is merely metadata, which will be stored in the TestComponent.__prop__metadata__
property.
Object.defineProperty(constructor, PROP_METADATA, {value: {}})[PROP_METADATA]
https://i.sstatic.net/DOcvf.png
Step 2. Angular Compiler Process
Subsequently, the Angular compiler gathers all component details, including @Input
metadata, to generate the component factory. The prepared metadata appears as follows:
{
"selector": "app-test",
"changeDetection": 1,
"inputs": [
"testString"
],
...
"outputs": [],
"host": {},
"queries": {},
"template": "{{testString}}"
}
(Note: As the Angular TemplateParser navigates the template, it utilizes this metadata to verify if the directive has an input named testString
)
Bearing the metadata in mind, the compiler creates updateDirective expressions:
if (dirAst.inputs.length || (flags & (NodeFlags.DoCheck | NodeFlags.OnInit)) > 0) {
updateDirectiveExpressions =
dirAst.inputs.map((input, bindingIndex) => this._preprocessUpdateExpression({
nodeIndex,
bindingIndex,
sourceSpan: input.sourceSpan,
context: COMP_VAR,
value: input.value
}));
}
These expressions are integrated into the produced factory:
https://i.sstatic.net/9hkJq.png
The aforementioned depicts that update expressions are generated in the parent view (AppComponent).
Step 3. Change Detection Mechanics
Following the initialization of all objects and factories, Angular initiates a cycle of change detection from the top view down to the child views.
During this process, Angular invokes the checkAndUpdateView function, which includes calling the updateDirectiveFn:
export function checkAndUpdateView(view: ViewData) {
if (view.state & ViewState.BeforeFirstCheck) {
view.state &= ~ViewState.BeforeFirstCheck;
view.state |= ViewState.FirstCheck;
} else {
view.state &= ~ViewState.FirstCheck;
}
shiftInitState(view, ViewState.InitState_BeforeInit, ViewState.InitState_CallingOnInit);
markProjectedViewsForCheck(view);
Services.updateDirectives(view, CheckType.CheckAndUpdate); <====
This marks the initial point at which your @Input
property receives a value:
providerData.instance[propName] = value;
if (def.flags & NodeFlags.OnChanges) {
changes = changes || {};
const oldValue = WrappedValue.unwrap(view.oldValues[def.bindingIndex + bindingIdx]);
const binding = def.bindings[bindingIdx];
changes[binding.nonMinifiedName !] =
new SimpleChange(oldValue, value, (view.state & ViewState.FirstCheck) !== 0);
}
Evidently, this occurs prior to the execution of the ngOnChanges
hook.
Conclusion
It's essential to grasp that Angular does not update an @Input
property's value during decorator invocation. The responsibility falls upon the change detection mechanism for such tasks.