Learn ASP.NET Core 3.1 with mini project : MVC, Razor Pages, Web API, Entity Framework Core, and Blazor. Hands-on online training course starting in August 2020. Click here to know more.

Create class decorators in TypeScript

In the previous part of this article and video series you learned about TypeScript inheritance. In this article you will learn about a feature of TypeScript that allows you to annotate and modify classes and class members. This feature is called Decorators. Decorators is an experimental feature and you need to enable them in your TypeScript configuration file. You will find TypeScript decorators analogous to C# attributes in that they form the metadata of a class and class members.

Enabling decorators

As mentioned earlier, decorators is an experimental feature and you need to enable them in TypeScript configuration. To do so, open tsconfig.json file from the wwwroot/TypeScript folder and modify it as shown below:

{
  "compilerOptions": {
    "noImplicitAny": false,
    "noEmitOnError": true,
    "removeComments": false,
    "sourceMap": true,
    "target": "es5",
    "experimentalDecorators": true,
    "outDir": "./Output"
  },
  "compileOnSave": true,
  "exclude": [
    "node_modules",
    "wwwroot"
  ]
}

If you are following this article series, the above settings should look familiar to you. Notice the setting marked in bold letters. The experimentalDecorators key is set to true indicating that decorators are enabled for your project.

Types of decorators

Before developing our own decorators let me quickly show you how they look like so that you get an idea. Take a look at the following class declaration:

@Theme
class Employee{

  @Required
  employeeID : number;

  @Required
  fullName : string;

  @Track
  showDetails() : void {

  }  
}

Notice the code marked in bold letters. They are decorators. You can see that Employee class is annotated with @Theme decorator. The employeeID and fullName properties are annotated with @Required decorator. And showDetails() method is annotated with @Track decorator. At code level decorators are functions. So, in the above code somewhere Theme, Required, and Track functions exist that do the intended processing.

In the above code decorators are added to class, properties, and methods. You can also add them to accessors and function parameters. Depending on which entity you want to decorate, you have various flavors of decorators such as class decorators, property decorators, and method decorators.

Class decorators

Now that you have some idea about what decorators are, let's create our first class decorator. In the following example you will create a class decorator called Theme. The Theme decorator simply adds a set of properties to the class definition that hold certain CSS classes.

Firstly, add a new TypeScript file to the TypeScript folder named ClassDecorators.ts.

Then add the following Employee class definition to it.

class Employee {
    employeeID: number;
    fullName: string;
    employeeID_class: string;
    fullName_class: string;


    constructor(id,name) {
        this.employeeID = id;
        this.fullName = name;
        this.employeeID_class = "class1";
        this.fullName_class = "class2";
    }


    showDetails(): void {
        document.getElementById("employeeID").
className = this.employeeID_class;
        document.getElementById("employeeID").
innerHTML = this.employeeID.toString();

        document.getElementById("fullName").
className = this.fullName_class;
        document.getElementById("fullName").
innerHTML = this.fullName;
    }
}

As you can see the Employee class contains four properties - employeeID, fullName, employeeID_class, and fullName_class. The first two properties contain the respective values whereas the last two properties hold names of CSS classes to be used while displaying these values. These four properties are assigned values in the constructor.

The showDetails() method picks certain HTML elements from the HTML page (shown later) and sets their className and innerHTML properties.

The HTML page housing these elements (ClassDecorator.html) looks like this:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
    <link href="/TypeScript/StyleSheet.css" 
rel="stylesheet" />
</head>
<body onload="DecoratorDemo()">
    <div id="employeeID"></div>
    <div id="fullName"></div>
    <script src="/TypeScript/Output/ClassDecorator.js">
    </script>
</body>
</html>

I am not discussing the content of StyleSheet.css here because it just contains class1 and class2 CSS classes.

The DecoratorDemo() function that is called upon body load is shown below:

function DecoratorDemo() {
    let emp = new Employee(1,'Nancy Davolio');
    emp.showDetails();
}

The above code simply creates an object of Employee class by passing employeeID and fullName values. It then calls the showDetails() function. A sample run of the page is shown below:

So far so good.

Modifying class properties

Now let's create a decorator that replaces the employeeID_class and fullName_class properties with new values.

Add the Theme() function in the ClassDecorator.ts file as shown below:

function Theme<T extends { 
new(...args: any[]): {} }>(constructor: T) {

    return class extends constructor {
        employeeID_class = "class3";
        fullName_class = "class4";
    }
}

The Theme() function receives the constructor of the class being decorated (Employee in this case) as its parameter.

Inside, it extends the supplied constructor and assigns some different values to the employeeID_class and fullName_class properties.

Next, apply Theme decorator to the Employee class as shown below:

@Theme
class Employee {
...
...
}

Notice that decorators take the form of @ followed by the decorator function name.

The following figure shows a sample run of the page after @Theme decorator has been applied.

As you can see, the @Theme decorator has overridden the CSS class names and hence <div> elements are shown in different color.

Decorator factories

In the above example the @Theme decorator doesn't accept any parameters. What if we want to pass the new CSS class names as decorator parameters? You can accomplish this by creating a decorator factory.

Let's see how.

function ThemeEx(settings: any) {
    let func = function <T extends 
{ new(...args: any[]): {} }>(constructor: T) {
        return class extends constructor {
            employeeID_class = settings.employeeID_class;
            fullName_class = settings.fullName_class;
        }
    }
    return func;
}

We created another decorator called ThemeEx. This time the decorator function accepts a JavaScript object containing the required CSS class names. Inside, we create an anonymous function that extends the class constructor and overrides the two properties as before. This code is identical to the Theme() function written earlier except that instead of hard-coding the CSS class names, it picks them from the outer function's settings parameter. Thus ThemeEx() function acts like a factory that creates the actual decorator function.

To apply @ThemeEx decorator to the Employee class you can write the following:

@ThemeEx({
    employeeID_class: "class3",
    fullName_class: "class6"

})
class Employee {
...
...
}

As you can see, we use @ThemeEx decorator and pass a JavaScript object with two properties employeeID_class and fullName_class.

The output will be identical to the previous run of the page.

Adding new properties

In the example just discussed we overrode the Employee class properties. It is also possible to add new properties to the class using decorators. Consider the following modified Theme decorator:

function Theme<T extends 
{ new(...args: any[]): {} }>(constructor: T) {

    return class extends constructor {
        employeeID_class = "class3";
        fullName_class = "class6";
        border_class = "class7";
    }
}

Here, we added a new property named border_class and assigned some CSS class name to it.

Since border_class is not a part of the design time Employee class you can't access it like other properties of Employee. For example, the following code gives error: 

However, you can access the border_class by casting it as any type. Consider this code:

showDetails(): void {
    document.getElementById("employeeID").
className = this.employeeID_class;
    document.getElementById("employeeID").
innerHTML = this.employeeID.toString();

    document.getElementById("fullName").
className = this.fullName_class;
    document.getElementById("fullName").
innerHTML = this.fullName;

    if ((<any>this).hasOwnProperty
("border_class")) {
    document.documentElement.className = 
(<any>this).border_class;
 }
}

Notice  the code shown in bold letters. It uses hasOwnProperty() JavaScript method to determine whether border_class property exists in the class. If you don't add the @Theme decorator border_class won't be there and hence this check is required. Then you access the border_class property and assign the className of the documentElement.

A sample run of the above code is shown below:

You can also watch the companion video here. In the next part of this series you will learn about property and method decorators.

That's it for now! Keep coding!!


Bipin Joshi is an independent software consultant, trainer, author, yoga mentor, and meditation teacher. He has been programming, meditating, and teaching for 24+ years. He conducts instructor-led online training courses in ASP.NET family of technologies for individuals and small groups. He is a published author and has authored or co-authored books for Apress and Wrox press. Having embraced the Yoga way of life he also teaches Ajapa Yoga to interested individuals. To know more about him click here.

Get connected : Facebook  Twitter  LinkedIn  YouTube

Posted On : 18 May 2020


Tags : ASP.NET ASP.NET Core MVC .NET Framework C# JavaScript Visual Studio


Subscribe to our newsletter

Get monthly email updates about new articles, tutorials, code samples, and how-tos getting added to our knowledge base.

  

Receive Weekly Updates