I have been under the impression that decorators in TypeScript are called after the constructor of a class. However, I was told otherwise, for instance, the top answer of this post claims that Decorators are called when the class is declared—not when an object is instantiated. A Udemy instructor of an Angular course I was enrolled in also told me that decorators in Typescript run before property initialization.
However, my experiments on this subject seem to indicate otherwise. For instance, this is a simple Angular code with property binding:
test.component.ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-test',
template: '{{testString}}'
})
export class TestComponent{
@Input() testString:string ="default string";
constructor() {
console.log(this.testString);
}
}
app.component.html
<app-test testString="altered string"></app-test>
When I execute the code, the console logs "default string" instead of "altered string". This proves that decorators are called after the constructor of a class executes.
Can somebody give me a definite answer of when decorators are called? Because my research online are contradicting the experiments I make. Thank you!
Decorators provide a way to add both annotations and a meta-programming syntax for class declarations and members. Decorators are a stage 2 proposal for JavaScript and are available as an experimental feature of TypeScript. NOTE Decorators are an experimental feature that may change in future releases.
A Decorator is a special kind of declaration that can be applied to classes, methods, accessor, property, or parameter.
In TypeScript, you can create decorators using the special syntax @expression , where expression is a function that will be called automatically during runtime with details about the target of the decorator. The target of a decorator depends on where you add them.
It's possible to use as many decorators on the same piece of code as you desire, and they'll be applied in the order that you declare them. This defines a class and applies three decorators — two to the class itself, and one to a property of the class: @log could log all access to the class.
Decorators are called when the class is declared—not when an object is instantiated.
That's correct.
As @H.B. has already said, we can prove it by looking at the transpiled 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;
}());
Now, let's go through the next steps to understand where you was wrong.
When I execute the code, the console logs "default string" instead of "altered string". This proves that decorators are called after the constructor of a class executes.
You can't be sure until you know what @Input()
decorator does.
Angular @Input
decorator just adorns component property with some information.
It's just metadata, that will be stored in TestComponent.__prop__metadata__
property.
Object.defineProperty(constructor, PROP_METADATA, {value: {}})[PROP_METADATA]
Now its time when angular compiler collects all information about component including @Input
metadatas to produce component factory. Prepared metadata looks like:
{
"selector": "app-test",
"changeDetection": 1,
"inputs": [
"testString"
],
...
"outputs": [],
"host": {},
"queries": {},
"template": "{{testString}}"
}
(Note: When Angular TemplateParser walk through template it uses this metadata to check whether directive has input with name testString
)
Based on the metadata compiler constructs 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
}));
}
that will be included in producing factory:
We can notice above that update expressions are generated in parent view(AppComponent).
After angular produced all factories and inititialized all necessary objects it runs change detection cycly from top view through all child view.
During this process angular calls checkAndUpdateView function, where it also calls 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); <====
That's the first place where your @Input
property gets 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);
}
As you can see It happens before ngOnChanges
hook.
Angular doesn't update @Input
property value during decorator execution. Change detection mechanism is responsible for such things.
The reason of your confusion is not caused by how Decorators works, but how Angular updates it's input-bound properties. You can prove yourself and
ngOnInit() {
console.log(this.testString) // see updated value
}
This happens because ngOnInit
is called after the first ngOnChanges
and ngOnChanges
updates your input.
You can just look at the generated code, e.g.
const defaultValue = (value: any) =>
(target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
target[propertyKey] = value;
};
class Test
{
@defaultValue("steven")
myProperty: string;
constructor()
{
console.log(this.myProperty);
}
}
new Test();
Will produce this:
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var defaultValue = function (value) {
return function (target, propertyKey, descriptor) {
target[propertyKey] = value;
};
};
var Test = /** @class */ (function () {
function Test() {
console.log(this.myProperty);
}
__decorate([
defaultValue("steven")
], Test.prototype, "myProperty", void 0);
return Test;
}());
new Test();
As you an see the __decorate
function is called on the property at class declaration time. This redefines the property according to the decorator code. For Angular this probably just sets some metadata, priming the class for inputs. Here it directly sets the value.
Thus, here the property will already have changed in the constructor.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With