The ambiguity surrounding the timing of decorator invocation in TypeScript

My understanding was that decorators in TypeScript are invoked after the constructor of a class. However, I recently learned otherwise. For example, the primary response on this thread suggests that Decorators are called when the class is declared—not when an object is instantiated. Furthermore, an instructor for an Angular course I was taking mentioned that decorators in Typescript actually run before property initialization.

Despite this information, my own tests seem to indicate a different outcome. Consider this basic code snippet from an Angular project involving 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>

Upon running the code, the console output reads "default string" instead of "altered string". This observation suggests that decorators are indeed executed after the constructor completes its task.

I am now seeking a definitive answer on when exactly decorators are invoked. The information I find online contradicts the results of my experiments. Thank you for any clarity you can provide!

Answer №1

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.

Answer №2

The source of your perplexity does not lie in the functioning of Decorators, but rather in how Angular refreshes its input-bound properties. You can verify this for yourself by

ngOnInit() {
  console.log(this.testString) // observe the updated value
}

This occurs because ngOnInit is executed after the initial ngOnChanges call, and ngOnChanges handles the updates to your input.

Answer №3

If you examine the code generated, you will find:

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

This results in:

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

By observing the code, you can see that the __decorate function is invoked on the property during class declaration time. This modifies the property based on the decorator logic. In the case of Angular, it may involve setting metadata for the class inputs. However, in this scenario, it directly assigns the value.

Hence, the property's value has already been altered within the constructor.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the process of obtaining User properties through a URL and utilizing them as variables in JavaScript?

I need to retrieve the city properties: 918 using req.params.userMosque from the URL '/shalat/:userMosque'. I want to assign it to the variable city for customizing my API url request. However, it seems like it's not working as expected. I h ...

Steps for passing a JSON object as a PathVariable in a Spring controller

Currently, I am in the process of developing a spring application using AngularJS. My goal is to pass a JSON object as a @PathVariable to the spring controller. However, with my existing code, I am facing an issue where when attempting to pass the JSON obj ...

The implementation of the data source in ag grid is not functioning

Implemented an ag-grid and configured a data source. However, the data source is not being triggered. How can we execute the data source properly? HTML Code: <div class="col-md-12" *ngIf="rowData.length > 0"> <ag-grid-angular #agGrid s ...

Retrieve information from a text

I retrieved this data as a string from a webpage using jQuery and need help parsing it. When I attempted to use jQuery.parseJSON, I encountered the error Uncaught SyntaxError: Unexpected token n. The specific values I am looking for are "surl" and "imgur ...

Error Occurs While Getting Request Parameters with Ajax Post and Express.js

When utilizing ajax to send data from a JavaScript frontend to an Express.js backend server, I am having trouble retrieving the posted data in the API method of my express.js. Below is the code where I attempt to parse the request data. Your assistance i ...

Strange behavior observed when transclusion is used without cloning

During my experimentation with transclusion, I wanted to test whether the transcluded directive could successfully locate its required parent directive controller after being transcluded under it. The directives used in this experiment are as follows: - Th ...

Tips for saving the visibility status of a <div> in your browser bookmarks?

Currently, I am in the process of developing a single webpage (index.html). The navigation bar at the top includes links to hash references (DIV IDs). If JavaScript is enabled, clicking on these links triggers a JavaScript function with the onclick attribu ...

Enhancing jQuery Mobile listview with voting buttons on each item row

I am looking to incorporate 2 vote buttons within a jQuery mobile listview, positioned on the left-hand side and centered within each list item. While I have managed to achieve this using javascript, my goal is to accomplish it without any additional scrip ...

HTML5 input placeholder adapts its size and position dynamically as data is being

During my interaction with the input fields on my bank's website, I noticed that the placeholder text undergoes a unique transformation. It shrinks in size and moves to the upper-left corner of the input field while I am entering information. Unlike t ...

Tips on connecting data within a jQuery element to a table of data

I am currently developing a program that involves searching the source code to list out element names and their corresponding IDs. Instead of displaying this information in alert popups, I would like to present it neatly within a data table. <script> ...

Utilizing the ref received from the Composition API within the Options API

My current approach involves utilizing a setup() method to bring in an external component that exclusively supports the Options API. Once I have imported this component, I need to set it up using the Options API data. The challenge I face is accessing the ...

Sending image to the server with the help of JavaScript

Curious if there is a method to upload an image to the server using javascript or jQuery and then save the image path/name into a database. I am working on a Windows platform server in asp.net 1.1, revamping a web page that is 10 years old. Unfortunately, ...

Implementing a secure route in React that asynchronously checks user authentication status

Currently, I have a React app utilizing Auth from the aws-amplify package (version 5.3.8) and react-router-dom (version 5.3.0). Although there is a version specifically for React available, it necessitates using at least version 5.0.0 of react-scripts, whe ...

TSLint is encountering the error code TS2459: The module "@azure/core-tracing" claims to have a local declaration of "Span" but does not export it, along with additional errors

I'm completely lost on how to tackle this error. The message I'm getting doesn't provide much insight, other than indicating an issue with the installation of '@azure/ai-text-analytics'. I've gone through the process of uninst ...

What are some ways to execute a script prior to installing an npm package?

I need help creating a test for npm packages in my project. I want to ensure that every time I attempt to install a module using npm install <module>, a script runs before the module is installed. However, I've noticed that the preinstall script ...

The functionality of res.send is not working correctly

Attempting to send a response from my express application to the front-end in order to display an alert. Here is what I have attempted in my app: if (info.messageId) { res.redirect('back'); res.send({ success: true }); } ...

Identifying JavaScript Errors in a Web Page: A Guide to Cypress

Currently, I am working on creating a spec file that contains N it(s), with each it visiting a specific page within the application and returning the number of errors/warnings present. I have also shared my query here: https://github.com/cypress-io/cypres ...

Disabling the scrollbar in Selenium screenshots

When using Chromedriver to capture screenshots of webpages, my code effectively does the job. However, I am now facing an issue with removing the unsightly scrollbars from the image. Is it feasible to inject CSS into the webpage in order to achieve this? W ...

Identifying the precise image dimensions required by the browser

When using the picture tag with srcset, I can specify different image sources based on viewport widths. However, what I really need is to define image sources based on the actual width of the space the image occupies after the page has been rendered by th ...

What steps should I take to address conflicting type identifiers between Cypress and jQuery?

Currently, I am tasked with writing TypeScript end-to-end tests for an Angular 11 application. Following the recommended practices of Cypress, my test setup is encountering a conflict due to existing jQuery dependencies (3.5.1) in the app and Cypress (8.4. ...