Book Image

Learning Angular - Fourth Edition

By : Aristeidis Bampakos, Pablo Deeleman
5 (1)
Book Image

Learning Angular - Fourth Edition

5 (1)
By: Aristeidis Bampakos, Pablo Deeleman

Overview of this book

As Angular continues to reign as one of the top JavaScript frameworks, more developers are seeking out the best way to get started with this extraordinarily flexible and secure framework. Learning Angular, now in its fourth edition, will show you how you can use it to achieve cross-platform high performance with the latest web techniques, extensive integration with modern web standards, and integrated development environments (IDEs). The book is especially useful for those new to Angular and will help you to get to grips with the bare bones of the framework to start developing Angular apps. You'll learn how to develop apps by harnessing the power of the Angular command-line interface (CLI), write unit tests, style your apps by following the Material Design guidelines, and finally, deploy them to a hosting provider. Updated for Angular 15, this new edition covers lots of new features and tutorials that address the current frontend web development challenges. You’ll find a new dedicated chapter on observables and RxJS, more on error handling and debugging in Angular, and new real-life examples. By the end of this book, you’ll not only be able to create Angular applications with TypeScript from scratch, but also enhance your coding skills with best practices.
Table of Contents (17 chapters)
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.