Understanding Decorators in Angular
We’ve already been exposed to decorators a little bit. Specifically, we have
seen @Component and @Directive decorators. A decorator in Angular looks like
this:
@Component({ someThing: 'some-value', someOtherThing: [Some, Other, Values]})export class SomeComponent {}They definitely look a little weird — they are like little hats that sit on top of our classes — but they play an important role. Their role in an Angular application is to provide some metadata about the class you are defining, i.e. they talk about your class rather than define the class.
The concept of a decorator is not an Angular specific concept. This is a common design pattern in Object Oriented Programming. It isn’t necessary for us to discuss the general applicability of the decorator pattern, but let’s consider what this is achieving for Angular.
I think that a @Component decorator shows this quite well:
@Component({ selector: 'my-component', template: `<p>Hello</p>`, styles: [ ` .some-class { display: none; } ` ]})export class MyComponent {}If you prefer to define your templates and styles in separate files you can do this instead:
@Component({ selector: 'my-component', templateUrl: 'my-component.component.html', styleUrls: ['my-component.component.scss']})export class MyComponent {
}We have the class itself which handles all of our logic for us, but that is not all Angular needs to know to create the component. Consider that we are able to add a component to the template of another component like this:
<my-component></my-component>Angular needs to know what name to give that tag and that is what the
selector property in the decorator allows us to do. We also need a template
associated with our class, and the decorator allows us to do that with the
template property (or the templateUrl property if you want to link out to
a separate template file rather than defining it inline in the decorator). The
same thing again with the styles property. In this case, this decorator is
telling Angular that:
- This class is a component (by virtue of the fact that we are using the
@Componentdecorator) - It should have a selector of
my-component - This is the template for the component (or this is where to find it)
- This is the styles for this component (or this is where to find it)
With all of that extra information, Angular can do its job and create the component.
@Directive
We already know about the @Component decorator, now let’s cover all of the
common decorators you will be using one at a time.
The @Directive decorator allows you to create your own custom directives. We’ve touched on the concept of a directive briefly, but basically, it allows you to attach some behaviour to a particular component/element. Typically, the decorator would look something like this:
@Directive({ selector: '[my-selector]'})Then in your template, you could use that selector to trigger the behaviour of the directive you have created by adding it to an element:
<some-element my-selector></some-element>We talked before about a directive that would make the background colour of an
element red. Let’s take that a step further and create a directive that will
change the background colour to a random colour.
I am going to create this directive for the home page component in the example app we have been working with. If your root component no longer displays your home component, I am going to leave it to you to figure out how to get that displaying correctly again.
import { Directive, HostBinding } from '@angular/core';
@Directive({ selector: '[randomColor]',})export class RandomColor { @HostBinding('style.backgroundColor') color = `#${Math.floor( Math.random() * 16777215 ).toString(16)}`;}I am sneaking in an Angular feature here that we haven’t talked about yet. The
idea with a HostBinding is that it binds to the host which means the
element/component that the directive is attached to (remember the comparison
I made about the directive being like a parasite, well that analogy holds here
because a parasite attaches to a host). We can use this host binding to change
some property of the host.
We have made this directive standalone so we do not need to worry
about an @NgModule. However, just so that you are somewhat familiar with the
NgModule approach… if you were using modules and wanted to use this directive
in a component, it would need to be declared within the same module as that
component, for example:
import { NgModule } from '@angular/core';import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';import { AppComponent } from './app.component';import { HomeComponent } from './home/home.component';import { RandomColor } from './home/ui/random-color.directive';import { WelcomeComponent } from './home/ui/welcome.component';import { SettingsComponent } from './settings/settings.component';
@NgModule({ declarations: [ AppComponent, HomeComponent, SettingsComponent, WelcomeComponent, RandomColor, ], imports: [BrowserModule, AppRoutingModule], providers: [], bootstrap: [AppComponent],})export class AppModule {}This would then allow us to use the directive within the HomeComponent. But,
since we are using a standalone directive we can just add it to the imports of
our standalone component:
import { Component } from '@angular/core';import { RandomColor } from './ui/random-color.directive';import { WelcomeComponent } from './ui/welcome.component';
@Component({ selector: 'app-home', template: ` <app-welcome /> <p>I am the home component</p> <p randomColor>I am stylish</p> `, imports: [WelcomeComponent, RandomColor],})export class HomeComponent {}If we were to take a look at our application now we would see that every time we refresh the page one paragraph has its background colour change, and the other doesn’t.
This is a pretty silly/simplistic example, however, we can create much more advanced functionality in directives that do more than just change some styles.
@Pipe
@Pipe allows you to create your own custom pipes to filter data that is displayed to the user, which can be very useful. The decorator might look something like this:
@Pipe({ name: 'myPipe'})To demonstrate what a pipe does, let’s again create an example for our home component.
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'reverse',})export class ReversePipe implements PipeTransform { transform(value: string) { return value.split('').reverse().join(''); }}The idea is that a value is passed into the pipe, we transform it in some way, and then we return it. In this case, we are creating a pipe to reverse the order of a string.
This is the first time we have seen the implements syntax. This is a feature
of classes and OOP, and it is often utilised in Angular. When we say that
a class implements another class/interface, we are saying that the class we
are writing will conform to the interface of the class it is implementing. That
might sound a bit complex, but the basic idea here is that the interface for
PipeTransform defines a transform method. For our class to conform, it needs
to have a transform method. If you are following along with this example, you
should notice that if you delete the transform method your editor will
complain that ReversePipe incorrectly implements the PipeTransform
interface.
Now let’s try using our pipe!
We can use our custom pipe, or any of the default Angular pipes (like async,
json, currency), like this:
import { Component } from '@angular/core';import { RandomColor } from './ui/random-color.directive';import { WelcomeComponent } from './ui/welcome.component';import { ReversePipe } from './ui/reverse.pipe';
@Component({ selector: 'app-home', template: ` <app-welcome></app-welcome> <p>I am the home component</p> <p>Time for a little: {{ magic | reverse }}</p> `, imports: [WelcomeComponent, RandomColor, ReversePipe],})export class HomeComponent { magic = 'reverse me';}Now magic would be run through our custom reverse pipe before the value is
output to the user. The main role of a pipe is to take some data in (reverse me in this case) and then return that data in a different format. Although this
is a bit of a silly example, a common example would be doing things like
converting the format of currencies or dates to display in a more friendly
manner (e.g. 29th of July, 2023 rather than 2023/07/29).
A pipe that is built into Angular that is often used is the async pipe,
which works quite differently to most typical pipes. Instead of displaying data
differently, it will allow us to pass it an observable or promise and it will
pull the value out of that observable/promise for us. In a strict sense, it is
still doing the same thing — some type of data goes in, gets transformed, and
then is output — the transform is just a bit more complex in this case.
…but! As you will see later in this course, now that we have signals
available we generally won’t be utilising this | async pipe approach. You will
however likely come across it a lot in existing Angular codebases.
@Injectable
We will use @Injectable to create a service for our classes to use. Injectables tie in closely to the idea of Dependency Injection in Angular which we will be talking about in another lesson. It also ties into some application architecture concerns, like the single responsibility principle but, again, we are going to talk about that later. For now, we will just keep things surface level.
Consider a situation where our home component displays some data in a list. That data might come from an API. We could have our component launch an HTTP request to fetch that data, but this might not be ideal for a couple of reasons:
- It adds additional responsibilities to the component. We primarily want our home component to just be concerned with rendering the component, not running business logic and launching HTTP requests
- What if some other component also needs access to that data? If we do this in the home component then the other component won’t be able to access the same data, and will have to launch its own request
An injectable service allows us to define business logic and cache data in a singleton object (meaning, usually, that our entire application will share the same instance of a single class). We can then inject that service into whatever components need to utilise its methods or data. We might define an injectable service like this:
@Injectable({ providedIn: 'root'})export class DataService { someData = 'multiple components can share/access me!'
someMethod(){ // Any component that injects me can access me! // I might just return some data // or I might update this.someData so that all of the // components who inject me can get the updated data! }}Notice that we supply a providedIn property. By providing this service in
root a single instance of it will be shared with the entire application. That
means if one component triggers a change to someData all the other components
will be able to access that same updated data. Again, this ties into
Dependency Injection which we are going to discuss in detail later.
An injectable service is different to declarables like the component,
directive, and pipe that all need to either be declared in a module, or imported
as standalone. Technically, an injectable needs to be added to the
providers array of a module or component, but the providedIn property above
takes care of this for us automatically. If we did not specify the
providedIn property we would need to supply it directly in the providers
array of the component we want to use it in:
@Component({ selector: 'app-home', template: ` <p>I am the home component</p> `, providers: [DataService]})To use the service in any component, we would inject it like this:
import { Component } from '@angular/core';import { DataService } from './data-access/data.service';
@Component({ selector: 'app-home', template: ` <p>I am the home component</p> `,})export class HomeComponent { constructor(dataService: DataService) { console.log(dataService.someData); }}We inject it using our constructor — we will talk about this in detail later,
but the basic idea here is we give dataService (a name of our choosing) a type
that represents the thing we want to inject. Now dataService will be an
instance of whatever that type token represents.
Again, there is now some new syntax available for injecting dependencies. Throughout this course, we will be using this syntax:
import { Component } from '@angular/core';import { DataService } from './data-access/data.service';
@Component({ selector: 'app-home', template: ` <p>I am the home component</p> `,})export class HomeComponent { dataService = inject(DataService)}There are some nuanced differences between the two approaches, but they essentially work the same and either approach is fine.
We are going to discuss what is happening in more detail in the Dependency Injection lesson.
@NgModule
We’ve already dealt with @NgModule a little bit, it is what is used to define
our Angular modules. Again — we will not be using NgModules but we will
still continue to cover the important concepts as you may still come across them
in other applications.
We are going to talk more about how Angular modules work in its own lesson, but we can see that the same general concepts of decorators are being applied here:
@NgModule({ declarations: [ AppComponent, HomeComponent, SettingsComponent, WelcomeComponent, RandomColor, ReversePipe, ], imports: [BrowserModule, AppRoutingModule], providers: [], bootstrap: [AppComponent],})export class AppModule {}We have a class with a decorator and that decorator has some properties that allow us to define additional information about the class.