Search icon
Subscription
0
Cart icon
Close icon
You have no products in your basket yet
Arrow left icon
All Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletters
Free Learning
Arrow right icon
Learning Angular - Fourth Edition

You're reading from  Learning Angular - Fourth Edition

Product type Book
Published in Feb 2023
Publisher Packt
ISBN-13 9781803240602
Pages 446 pages
Edition 4th Edition
Languages
Authors (2):
Aristeidis Bampakos Aristeidis Bampakos
Profile icon Aristeidis Bampakos
Pablo Deeleman Pablo Deeleman
Profile icon Pablo Deeleman
View More author details

Table of Contents (17) Chapters

Preface 1. Building Your First Angular Application 2. Introduction to TypeScript 3. Organizing Application into Modules 4. Enabling User Experience with Components 5. Enrich Applications Using Pipes and Directives 6. Managing Complex Tasks with Services 7. Being Reactive Using Observables and RxJS 8. Communicating with Data Services over HTTP 9. Navigating through Application with Routing 10. Collecting User Data with Forms 11. Introduction to Angular Material 12. Unit Test an Angular Application 13. Bringing an Application to Production 14. Handling Errors and Application Debugging 15. Other Books You May Enjoy
16. Index

Decorators

Decorators are a very cool functionality that allows us to add metadata to class declarations for further use. By creating decorators, we are defining special annotations that may impact how our classes, methods, or functions behave or simply altering the data we define in fields or parameters. They are a powerful way to augment our type’s native functionalities without creating subclasses or inheriting from other types. It is, by far, one of the most exciting features of TypeScript. It is extensively used in Angular when designing modules and components or managing dependency injection, as we will learn later in Chapter 6, Managing Complex Tasks with Services.

The @ prefix recognizes decorators in their name, which are standalone statements above the element they decorate. We can define up to four different types of decorators, depending on what element each type is meant to decorate:

  • Class decorators
  • Property decorators
  • Method decorators
  • Parameter decorators

The Angular framework defines custom decorators, which we will use during the development of an application.

We’ll look at the previous types of decorators in the following subsections.

Class decorators

Class decorators allow us to augment a class or perform operations on its members. The decorator statement is executed before the class gets instantiated. Creating a class decorator requires defining a plain function, whose signature is a pointer to the constructor belonging to the class we want to decorate. The formal declaration defines a class decorator as follows:

declare type ClassDecorator = <TFunction extends Function>(Target:TFunction) => TFunction | void;

Let’s see how we can use a class decorator through a simple example:

function Banana(target: Function): void {
    target.prototype.banana = function(): void {
        console.log('We have bananas!');
    }
}
@Banana
class FruitBasket {}
const basket = new FruitBasket();
basket.banana();

In the preceding snippet, we use the banana method, which was not initially defined in the FruitBasket class. However, we decorate it with the @Banana decorator. It is worth mentioning, though, that this won’t compile. The compiler will complain that FruitBasket does not have a banana method, and rightfully so because TypeScript is typed. So, at this point, we need to tell the compiler that this is valid. So, how do we do that? One way is that, when we create our basket instance, we give it the type any:

const basket: any = new FruitBasket();

The compiler will not complain about the method now, and the compilation of our code will complete successfully.

Extending a class decorator

Sometimes, we might need to customize how a decorator operates upon instantiating it. We can design our decorators with custom signatures and then have them return a function with the same signature we defined without parameters. The following piece of code illustrates the same functionality as the previous example, but it allows us to customize the message:

function Banana(message: string) {
    return function(target: Function) {
        target.prototype.banana = function(): void {
            console.log(message);
        }
    }
}
@Banana('Bananas are yellow!')
class FruitBasket {}
const basket: any = new FruitBasket();
basket.banana();

If we run the preceding code, the browser console will print the following message:

Bananas are yellow!

As a rule of thumb, decorators that accept parameters require a function whose signature matches the parameters we want to configure. The function also returns another function that matches the signature of the decorator.

Property decorators

Property decorators are applied to class fields and are defined by creating a function whose signature takes two parameters:

  • target: The prototype of the class we want to decorate
  • key: The name of the property we want to decorate

Possible use cases for this decorator are logging the values assigned to class fields when instantiating objects or reacting to data changes in such fields. Let’s see an actual example that showcases both behaviors:

function Jedi(target: Object, key: string) {
    let propertyValue: string = target[key];
    if (delete target[key]) {
        Object.defineProperty(target, key, {
            get: function() {
                return propertyValue;
            },
            set: function(newValue){
                propertyValue = newValue;
                console.log(`${propertyValue} is a Jedi`);
            }
        });
    }
}
class Character {
    @Jedi
    name: string;
}
const character = new Character();
character.name = 'Luke';

The preceding snippet follows the same logic as for parameterized class decorators. However, the signature of the returned function is slightly different to match that of the parameterless decorator declaration we saw earlier.

Let’s now see an example that depicts how we can log changes on a given class property using a property decorator:

function NameChanger(callbackObject: any): Function {
    return function(target: Object, key: string): void {
        let propertyValue: string = target[key];
        if (delete target[key]) {
            Object.defineProperty(target, key, {
                get: function() {
                    return propertyValue;
                },
                set: function(newValue) {
                    propertyValue = newValue;
                    callbackObject.changeName.call(this, propertyValue);
                }
            });
        }
    }
}

The NameChanger decorator can be applied in a class to be executed when the name property is modified:

class Character {
    @NameChanger ({
        changeName: function(newValue: string): void {
            console.log(`You are now known as ${newValue}`);
        }
    })
    name: string;
}
var character = new Character();
character.name = 'Anakin';

In the preceding snippet, the changeName function is triggered when the value of the property changes in the character instance.

Method decorators

A method decorator can detect, log, and intervene in how methods are executed. To do so, we need to define a function whose payload takes the following parameters:

  • target: Represents the decorated method.
  • key: The actual name of the decorated method.
  • descriptor: A property descriptor of the given method. It is a hash object containing, among other things, a property named value that references the method itself.

In the following example, we’re creating a decorator that displays how a method is called:

function Log(){
    return function(target, key: string, descriptor: PropertyDescriptor) {
        const oldMethod = descriptor.value;
        descriptor.value = function newFunc(...args:any[]){
            let result = oldMethod.apply(this, args);
            console.log(`${key} is called with ${args.join(',')} and result ${result}`);
            return result;
        }
    }
}
class Hero {
    @Log()
    attack(...args:[]) { return args.join(); }
}
const hero = new Hero();
hero.attack();

The preceding snippet also illustrates what the arguments were upon calling the method and the result of the method’s invocation.

Parameter decorator

A parameter decorator, the last one we will learn about, taps into parameters located in function signatures. It is not intended to alter the parameter information or the function behavior but to look into the parameter value and perform operations such as logging or replicating data. It accepts the following parameters:

  • target: The object prototype where the function, whose parameters are decorated, usually belongs to a class
  • key: The name of the function whose signature contains the decorated parameter
  • index: The index where this decorator has been applied in the parameter’s array

The following example shows a working example of a parameter decorator:

function Log(target: Function, key: string, index: number) {
    const functionLogged = key || target.prototype.constructor.name;
    console.log(`The parameter in position ${index} at ${functionLogged} has been decorated`);
}
class Greeter {
    greeting: string;
    
    constructor (@Log phrase: string) {
        this.greeting = phrase;
    }
}

In the preceding snippet, we declare the functionLogged variable in that way because the value of the target parameter varies depending on the function whose parameters are decorated. Therefore, decorating a constructor or a method parameter is different. The former returns a reference to the class prototype, while the latter returns a reference to the constructor function. The same applies to the key parameter, which is undefined when decorating the constructor parameters.

Parameter decorators do not modify the value of the parameters decorated or alter the behavior of the methods or constructors where these parameters live. They usually log or prepare the object created to implement additional layers of abstraction or functionality through higher-level decorators, such as a method or class decorator. Common use case scenarios for this encompass logging component behavior or managing dependency injection.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $15.99/month. Cancel anytime}