Angular Metadata
In Angular, metadata plays a crucial role in telling the framework how to process a class. Metadata is information that describes a class, its behavior, and how it should interact with other parts of an application. Let's dive deep into Angular metadata to understand how it works and why it's fundamental to Angular development.
Introduction to Angular Metadata
Metadata in Angular is additional information attached to classes that Angular uses to understand what the class is and how it should work within the application. This metadata is specified using decorators - special kinds of declarations that are attached to classes, methods, properties, or parameters.
Angular uses these decorators to mark TypeScript classes as Angular components, services, modules, or other Angular constructs, providing the framework with crucial information about how these elements work together.
Angular Decorators
Decorators are the primary way to add metadata to your TypeScript code in Angular. They are functions that modify JavaScript classes, properties, methods, or method parameters.
Here are the most common decorators in Angular:
@Component
- For defining components@NgModule
- For defining modules@Injectable
- For defining services@Input
and@Output
- For component communication@Directive
- For creating directives
Let's examine each one in detail.
@Component Decorator
The @Component
decorator turns a class into an Angular component. It provides Angular with metadata that tells the framework how to create and present the component in the application.
import { Component } from '@angular/core';
@Component({
selector: 'app-hello-world',
template: '<h1>Hello, World!</h1>',
styles: [`
h1 { color: blue; }
`]
})
export class HelloWorldComponent {
// Component logic goes here
}
The component metadata includes:
- selector: A CSS selector that identifies this component in a template
- template/templateUrl: The HTML template that defines the component's view
- styles/styleUrls: CSS styles for the component
- providers: Services that the component requires
- animations: List of animations used in the component
- and many more properties
@NgModule Decorator
The @NgModule
decorator marks a class as an Angular module and provides metadata about the module.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
The NgModule metadata includes:
- declarations: Components, directives, and pipes that belong to this module
- imports: Other modules whose exported components, directives, or pipes are needed
- exports: Components, directives, and pipes that can be used in templates of other modules
- providers: Services that the module contributes to the global collection of services
- bootstrap: The main component that should be bootstrapped
@Injectable Decorator
The @Injectable
decorator marks a class as a service that can be injected into components and other services.
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root' // This ensures a singleton instance is available throughout the application
})
export class DataService {
getData() {
return ['Item 1', 'Item 2', 'Item 3'];
}
}
The providedIn
property specifies where the service should be provided. Setting it to 'root' makes the service available throughout the application with a single instance (singleton).
@Input and @Output Decorators
These decorators are used for component communication.
@Input()
allows a parent component to pass data to a child component@Output()
allows a child component to emit events to the parent component
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-child',
template: `
<div>
<p>{{ childData }}</p>
<button (click)="sendMessage()">Send Message to Parent</button>
</div>
`
})
export class ChildComponent {
@Input() childData: string;
@Output() messageEvent = new EventEmitter<string>();
sendMessage() {
this.messageEvent.emit('Hello from child component!');
}
}
In the parent component:
import { Component } from '@angular/core';
@Component({
selector: 'app-parent',
template: `
<app-child
[childData]="parentData"
(messageEvent)="receiveMessage($event)">
</app-child>
<p>{{ message }}</p>
`
})
export class ParentComponent {
parentData = 'Data from parent';
message = '';
receiveMessage(msg: string) {
this.message = msg;
}
}
@Directive Decorator
This decorator is used to create custom directives in Angular.
import { Directive, ElementRef, HostListener } from '@angular/core';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
constructor(private el: ElementRef) { }
@HostListener('mouseenter') onMouseEnter() {
this.highlight('yellow');
}
@HostListener('mouseleave') onMouseLeave() {
this.highlight(null);
}
private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
}
Real-World Application of Metadata
Let's build a simple task management component to illustrate how metadata is used in a real-world application.
First, let's create a service to manage tasks:
import { Injectable } from '@angular/core';
interface Task {
id: number;
title: string;
completed: boolean;
}
@Injectable({
providedIn: 'root'
})
export class TaskService {
private tasks: Task[] = [
{ id: 1, title: 'Learn Angular', completed: false },
{ id: 2, title: 'Build an app', completed: false }
];
getTasks(): Task[] {
return this.tasks;
}
addTask(title: string): void {
const id = this.tasks.length ? Math.max(...this.tasks.map(t => t.id)) + 1 : 1;
this.tasks.push({ id, title, completed: false });
}
toggleComplete(id: number): void {
const task = this.tasks.find(t => t.id === id);
if (task) {
task.completed = !task.completed;
}
}
}
Now, let's create a task list component:
import { Component, OnInit } from '@angular/core';
import { TaskService } from '../task.service';
@Component({
selector: 'app-task-list',
template: `
<div class="task-container">
<h2>Task Manager</h2>
<div class="add-task">
<input [(ngModel)]="newTaskTitle" placeholder="New task..." />
<button (click)="addTask()">Add Task</button>
</div>
<div class="task-list">
<div
*ngFor="let task of tasks"
class="task-item"
[class.completed]="task.completed">
<input
type="checkbox"
[checked]="task.completed"
(change)="toggleComplete(task.id)"
/>
<span>{{ task.title }}</span>
</div>
</div>
<div class="task-stats">
<p>{{ completedCount }} of {{ tasks.length }} tasks completed</p>
</div>
</div>
`,
styles: [`
.task-container {
max-width: 500px;
margin: 0 auto;
padding: 20px;
}
.add-task {
display: flex;
margin-bottom: 20px;
}
.add-task input {
flex-grow: 1;
padding: 8px;
margin-right: 10px;
}
.task-item {
padding: 10px;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
}
.task-item.completed span {
text-decoration: line-through;
color: #888;
}
.task-item input {
margin-right: 10px;
}
.task-stats {
margin-top: 20px;
color: #666;
}
`]
})
export class TaskListComponent implements OnInit {
tasks: any[] = [];
newTaskTitle = '';
constructor(private taskService: TaskService) { }
ngOnInit() {
this.tasks = this.taskService.getTasks();
}
addTask() {
if (this.newTaskTitle.trim()) {
this.taskService.addTask(this.newTaskTitle);
this.newTaskTitle = '';
}
}
toggleComplete(id: number) {
this.taskService.toggleComplete(id);
}
get completedCount() {
return this.tasks.filter(task => task.completed).length;
}
}
In this example:
- We use the
@Injectable
decorator to create a service for managing tasks - We use the
@Component
decorator to create a TaskListComponent with metadata that defines its selector, template, and styles - The component uses the injected TaskService to get data and perform operations
To use this component, you would need to add it to a module:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { TaskListComponent } from './task-list/task-list.component';
@NgModule({
declarations: [
AppComponent,
TaskListComponent
],
imports: [
BrowserModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Then, you can use the task list component in your app component:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<div class="container">
<h1>My Task Management App</h1>
<app-task-list></app-task-list>
</div>
`,
styles: [`
.container {
padding: 20px;
}
h1 {
color: #333;
text-align: center;
}
`]
})
export class AppComponent { }
Advanced Metadata Concepts
Custom Decorators
In Angular, you can create custom decorators for more specialized use cases:
// Creating a custom log decorator
function Log() {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling ${propertyKey} with:`, args);
const result = originalMethod.apply(this, args);
console.log(`${propertyKey} returned:`, result);
return result;
};
return descriptor;
};
}
// Using the decorator
@Component({
selector: 'app-example',
template: '<button (click)="doSomething()">Click me</button>'
})
export class ExampleComponent {
@Log()
doSomething() {
return 'Task done!';
}
}
When doSomething()
is called, it will log information about the call and return value.
View Encapsulation
The @Component
decorator allows you to control how styles defined in a component are applied by setting the encapsulation
property:
import { Component, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-example',
template: `<h1>Hello World</h1>`,
styles: ['h1 { color: red; }'],
encapsulation: ViewEncapsulation.Emulated // default
})
export class ExampleComponent { }
Options for encapsulation
are:
- ViewEncapsulation.Emulated - Angular emulates shadow DOM (default)
- ViewEncapsulation.None - No encapsulation, styles apply globally
- ViewEncapsulation.ShadowDom - Uses browser's native shadow DOM
Change Detection Strategy
You can control how change detection works in components:
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-optimized',
template: `<div>{{ data }}</div>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OptimizedComponent {
data = 'Some data';
}
Using ChangeDetectionStrategy.OnPush
makes the component only check for changes when:
- An input property changes
- An event handler is fired
- An observable bound to the template via the async pipe emits
Summary
Angular metadata is a fundamental concept that enables the framework to understand how your application should be structured and behave. Through decorators like @Component
, @NgModule
, @Injectable
, @Input
, and @Output
, you can provide Angular with the information it needs to wire everything together correctly.
Key points to remember:
- Metadata is provided through TypeScript decorators
- Decorators are functions that modify classes and their members
- The most common decorators are
@Component
,@NgModule
, and@Injectable
- Component metadata includes selector, template, and styles
- Module metadata specifies declarations, imports, exports, and providers
- Services use
@Injectable
to become available for dependency injection @Input
and@Output
enable component communication
Understanding metadata is essential for effective Angular development as it forms the foundation of how components, directives, services, and modules interact with each other in your application.
Exercises
- Create a simple component with the
@Component
decorator that displays a greeting message. - Create a parent and child component that communicate using
@Input
and@Output
decorators. - Create a service with the
@Injectable
decorator and inject it into a component. - Create a custom directive with the
@Directive
decorator that changes the background color of an element on hover. - Create a module with the
@NgModule
decorator that imports and exports components.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)