16.3 Components & Templates — Decorators, 4 Types of Data Binding, Directives
Component Basics
In Angular, a Component is the fundamental unit of UI. One component consists of three elements:
- Class (TypeScript) — data and logic
- Template (HTML) — UI structure
- Styles (CSS/SCSS) — styling
@Component Decorator
import { Component, OnInit, input, output, signal } from '@angular/core';
@Component({
// Tag name used in HTML
selector: 'app-user-card',
// Standalone component (Angular 17+ default)
standalone: true,
// Modules/components needed by this component
imports: [],
// Inline template (suitable for small components)
template: `<p>{{ name() }}</p>`,
// Or reference an external file
// templateUrl: './user-card.component.html',
// Inline styles
styles: [`p { color: blue; }`],
// Or external style
// styleUrl: './user-card.component.scss',
// Change detection strategy (performance optimization)
// changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent implements OnInit {
name = input.required<string>(); // Required input
ngOnInit() {
console.log('Component initialized:', this.name());
}
}
4 Types of Data Binding
The heart of Angular is data binding — four ways to connect class data with the template.
1. Interpolation {{ }}
Displays class values as text.
@Component({
standalone: true,
template: `
<h1>{{ title }}</h1>
<p>Price: {{ price | currency:'USD' }}</p>
<p>Date: {{ today | date:'yyyy-MM-dd' }}</p>
<p>Expression: {{ 1 + 2 }}</p>
<p>Method: {{ getGreeting() }}</p>
`
})
export class DemoComponent {
title = 'Angular Binding';
price = 29.99;
today = new Date();
getGreeting(): string {
return 'Hello!';
}
}
2. Property Binding [property]="value"
Binds values to DOM properties or component inputs.
@Component({
standalone: true,
template: `
<!-- DOM property binding -->
<img [src]="imageUrl" [alt]="imageAlt">
<input [value]="username" [disabled]="isDisabled">
<button [disabled]="isLoading">
{{ isLoading ? 'Processing...' : 'Submit' }}
</button>
<!-- Component property binding -->
<app-user-card [userId]="selectedUserId" [showAvatar]="true" />
<!-- Class binding -->
<div [class.active]="isActive" [class.error]="hasError">Status</div>
<!-- Style binding -->
<div [style.color]="textColor" [style.fontSize.px]="fontSize">
Style binding
</div>
<!-- Attr binding (when no DOM property exists) -->
<td [attr.colspan]="columnSpan">Total</td>
`
})
export class PropertyBindingComponent {
imageUrl = 'https://example.com/avatar.jpg';
imageAlt = 'User avatar';
username = 'alice';
isDisabled = false;
isLoading = false;
selectedUserId = 42;
isActive = true;
hasError = false;
textColor = 'blue';
fontSize = 16;
columnSpan = 3;
}
3. Event Binding (event)="handler()"
Connects user events to class methods.
@Component({
standalone: true,
template: `
<!-- Basic click event -->
<button (click)="onClick()">Click</button>
<!-- Passing event object -->
<button (click)="onClickWithEvent($event)">With event</button>
<!-- Input event -->
<input (input)="onInput($event)" (keyup.enter)="onEnter()">
<!-- Mouse events -->
<div
(mouseenter)="onMouseEnter()"
(mouseleave)="onMouseLeave()"
[class.hovered]="isHovered"
>
Hover over me
</div>
<!-- Form submit event -->
<form (submit)="onSubmit($event)">
<input [(ngModel)]="formValue">
<button type="submit">Submit</button>
</form>
`
})
export class EventBindingComponent {
isHovered = false;
formValue = '';
onClick() {
console.log('Button clicked!');
}
onClickWithEvent(event: MouseEvent) {
console.log('Click position:', event.clientX, event.clientY);
}
onInput(event: Event) {
const value = (event.target as HTMLInputElement).value;
console.log('Input value:', value);
}
onEnter() {
console.log('Enter key pressed!');
}
onMouseEnter() { this.isHovered = true; }
onMouseLeave() { this.isHovered = false; }
onSubmit(event: Event) {
event.preventDefault();
console.log('Form submitted:', this.formValue);
}
}
4. Two-Way Binding [(ngModel)]
Synchronizes input fields with class properties in both directions.
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms'; // Required for ngModel
@Component({
standalone: true,
imports: [FormsModule],
template: `
<input [(ngModel)]="username" placeholder="Username">
<p>Typed name: {{ username }}</p>
<!-- Two-way binding is shorthand for -->
<!-- property binding + event binding combined -->
<input
[ngModel]="email"
(ngModelChange)="email = $event"
placeholder="Email"
>
`
})
export class TwoWayBindingComponent {
username = '';
email = '';
}
Built-In Control Flow Syntax (Angular 17+)
From Angular 17, use new block syntax instead of *ngIf and *ngFor.
@if — Conditional Rendering
@Component({
standalone: true,
template: `
<!-- Basic @if -->
@if (isLoggedIn) {
<p>You are logged in.</p>
}
<!-- @if / @else if / @else -->
@if (userRole === 'admin') {
<p>Admin panel</p>
} @else if (userRole === 'manager') {
<p>Manager dashboard</p>
} @else {
<p>Regular user home</p>
}
<!-- With null check -->
@if (user; as u) {
<p>{{ u.name }} ({{ u.email }})</p>
} @else {
<p>No user information</p>
}
`
})
export class IfDirectiveComponent {
isLoggedIn = true;
userRole: 'admin' | 'manager' | 'user' = 'admin';
user: { name: string; email: string } | null = {
name: 'Alice',
email: 'alice@example.com'
};
}
@for — Iterative Rendering
@Component({
standalone: true,
template: `
<!-- Basic @for — track is required -->
<ul>
@for (item of items; track item.id) {
<li>{{ item.name }}</li>
}
</ul>
<!-- Using built-in variables: $index, $first, $last, etc. -->
<ul>
@for (item of items; track item.id; let i = $index; let first = $first; let last = $last) {
<li [class.first]="first" [class.last]="last">
{{ i + 1 }}. {{ item.name }}
</li>
} @empty {
<li>No items found.</li>
}
</ul>
`
})
export class ForDirectiveComponent {
items = [
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Orange' },
];
}
Built-in @for variables:
| Variable | Type | Description |
|---|---|---|
$index | number | Current index (starts at 0) |
$count | number | Total number of items |
$first | boolean | Whether it is the first item |
$last | boolean | Whether it is the last item |
$even | boolean | Whether the index is even |
$odd | boolean | Whether the index is odd |
@switch — Multi-Condition Rendering
@Component({
standalone: true,
template: `
@switch (status) {
@case ('loading') {
<div class="spinner">Loading...</div>
}
@case ('success') {
<div class="success">Done!</div>
}
@case ('error') {
<div class="error">An error occurred</div>
}
@default {
<div>Unknown status</div>
}
}
`
})
export class SwitchDirectiveComponent {
status: 'loading' | 'success' | 'error' | 'idle' = 'loading';
}
Attribute Directives — NgClass, NgStyle
import { Component } from '@angular/core';
import { NgClass, NgStyle } from '@angular/common';
@Component({
standalone: true,
imports: [NgClass, NgStyle],
template: `
<!-- NgClass: conditionally apply multiple classes -->
<div [ngClass]="{
'active': isActive,
'disabled': isDisabled,
'highlighted': isHighlighted
}">
NgClass example
</div>
<!-- Array syntax -->
<div [ngClass]="['btn', isActive ? 'btn-primary' : 'btn-secondary']">
Button
</div>
<!-- NgStyle: dynamically apply multiple styles -->
<div [ngStyle]="{
'color': textColor,
'font-size': fontSize + 'px',
'background-color': bgColor
}">
NgStyle example
</div>
`
})
export class DirectivesComponent {
isActive = true;
isDisabled = false;
isHighlighted = true;
textColor = '#333';
fontSize = 16;
bgColor = '#f0f0f0';
}
Custom Directives
// directives/highlight.directive.ts
import { Directive, ElementRef, HostListener, input } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective {
// Receive highlight color as input
appHighlight = input('yellow');
constructor(private el: ElementRef) {}
@HostListener('mouseenter')
onMouseEnter() {
this.el.nativeElement.style.backgroundColor = this.appHighlight();
}
@HostListener('mouseleave')
onMouseLeave() {
this.el.nativeElement.style.backgroundColor = '';
}
}
// Usage
@Component({
standalone: true,
imports: [HighlightDirective],
template: `
<p appHighlight>Default yellow highlight</p>
<p [appHighlight]="'lightblue'">Blue highlight</p>
<p [appHighlight]="'#ffcccc'">Red highlight</p>
`
})
export class DemoComponent {}
Component Lifecycle Hooks
import {
Component, OnInit, OnChanges, DoCheck,
AfterContentInit, AfterContentChecked,
AfterViewInit, AfterViewChecked,
OnDestroy, SimpleChanges, input
} from '@angular/core';
import { Subject } from 'rxjs';
@Component({
selector: 'app-lifecycle-demo',
standalone: true,
template: `<p>Lifecycle demo: {{ data() }}</p>`
})
export class LifecycleDemoComponent
implements OnInit, OnChanges, OnDestroy, AfterViewInit {
data = input<string>('');
private destroy$ = new Subject<void>();
// 1. Right after component creation (before Input initialization)
// constructor: DI only, no logic
// 2. When Input values change (also called before ngOnInit)
ngOnChanges(changes: SimpleChanges) {
console.log('Input changed:', changes);
// changes['data'].currentValue — new value
// changes['data'].previousValue — previous value
// changes['data'].firstChange — whether first change
}
// 3. Component initialization (runs once) — main initialization logic
ngOnInit() {
console.log('Component initialized');
// HTTP requests, subscription setup, etc. go here
}
// 4. View initialization complete (DOM accessible)
ngAfterViewInit() {
console.log('View initialized — DOM accessible');
}
// 5. Component destruction (prevent memory leaks)
ngOnDestroy() {
console.log('Component destroyed');
this.destroy$.next();
this.destroy$.complete();
}
}
Lifecycle hook order:
constructor → ngOnChanges → ngOnInit → ngDoCheck
→ ngAfterContentInit → ngAfterContentChecked
→ ngAfterViewInit → ngAfterViewChecked
→ (ngDoCheck, ngAfterContentChecked, ngAfterViewChecked repeat per change detection)
→ ngOnDestroy
Input / Output
Angular 17.1+ recommends the input() and output() function-based approach.
Modern Approach (Angular 17.1+)
// child.component.ts
import { Component, input, output } from '@angular/core';
interface Product {
id: number;
name: string;
price: number;
}
@Component({
selector: 'app-product-card',
standalone: true,
template: `
<div class="card">
<h3>{{ product().name }}</h3>
<p>{{ product().price | currency }}</p>
<button (click)="addToCart()">Add to Cart</button>
<button (click)="onFavoriteToggle()">
{{ isFavorite() ? '♥' : '♡' }}
</button>
</div>
`
})
export class ProductCardComponent {
// Required input
product = input.required<Product>();
// Optional input (with default)
isFavorite = input(false);
// Output events
addedToCart = output<Product>();
favoriteToggled = output<{ id: number; favorite: boolean }>();
addToCart() {
this.addedToCart.emit(this.product());
}
onFavoriteToggle() {
this.favoriteToggled.emit({
id: this.product().id,
favorite: !this.isFavorite()
});
}
}
// parent.component.ts
import { Component, signal } from '@angular/core';
import { CurrencyPipe } from '@angular/common';
import { ProductCardComponent } from './product-card.component';
@Component({
standalone: true,
imports: [ProductCardComponent, CurrencyPipe],
template: `
<div class="product-grid">
@for (product of products(); track product.id) {
<app-product-card
[product]="product"
[isFavorite]="favorites().has(product.id)"
(addedToCart)="onAddToCart($event)"
(favoriteToggled)="onFavoriteToggle($event)"
/>
}
</div>
<div class="cart-summary">
Cart: {{ cartItems().length }} items
</div>
`
})
export class ParentComponent {
products = signal([
{ id: 1, name: 'Laptop', price: 1200 },
{ id: 2, name: 'Mouse', price: 35 },
]);
cartItems = signal<{ id: number; name: string; price: number }[]>([]);
favorites = signal(new Set<number>());
onAddToCart(product: { id: number; name: string; price: number }) {
this.cartItems.update(items => [...items, product]);
}
onFavoriteToggle(event: { id: number; favorite: boolean }) {
this.favorites.update(favs => {
const newFavs = new Set(favs);
if (event.favorite) {
newFavs.add(event.id);
} else {
newFavs.delete(event.id);
}
return newFavs;
});
}
}
Real-World Example — Todo List Component
// todo/todo.component.ts
import { Component, signal, computed } from '@angular/core';
import { FormsModule } from '@angular/forms';
interface Todo {
id: number;
text: string;
completed: boolean;
createdAt: Date;
}
type FilterType = 'all' | 'active' | 'completed';
@Component({
selector: 'app-todo',
standalone: true,
imports: [FormsModule],
template: `
<div class="todo-app">
<h1>Todo List</h1>
<!-- Input form -->
<div class="add-todo">
<input
[(ngModel)]="newTodoText"
(keyup.enter)="addTodo()"
placeholder="Enter a new task..."
[disabled]="isAdding()"
>
<button (click)="addTodo()" [disabled]="!newTodoText.trim()">
Add
</button>
</div>
<!-- Filter buttons -->
<div class="filters">
@for (filter of filters; track filter) {
<button
[class.active]="currentFilter() === filter"
(click)="setFilter(filter)"
>
{{ filterLabels[filter] }}
</button>
}
</div>
<!-- Stats -->
<p class="stats">
Total {{ totalCount() }} | Completed {{ completedCount() }} | Remaining {{ activeCount() }}
</p>
<!-- Todo list -->
<ul class="todo-list">
@for (todo of filteredTodos(); track todo.id) {
<li [class.completed]="todo.completed">
<input
type="checkbox"
[checked]="todo.completed"
(change)="toggleTodo(todo.id)"
>
<span>{{ todo.text }}</span>
<small>{{ todo.createdAt | date:'HH:mm' }}</small>
<button (click)="deleteTodo(todo.id)" class="delete-btn">✕</button>
</li>
} @empty {
<li class="empty">No todos! 🎉</li>
}
</ul>
<!-- Bulk actions -->
@if (totalCount() > 0) {
<div class="actions">
<button (click)="toggleAll()">
{{ allCompleted() ? 'Mark all incomplete' : 'Mark all complete' }}
</button>
@if (completedCount() > 0) {
<button (click)="clearCompleted()" class="danger">
Clear completed ({{ completedCount() }})
</button>
}
</div>
}
</div>
`
})
export class TodoComponent {
newTodoText = '';
isAdding = signal(false);
currentFilter = signal<FilterType>('all');
private nextId = signal(1);
private todos = signal<Todo[]>([
{ id: 1, text: 'Learn Angular basics', completed: true, createdAt: new Date() },
{ id: 2, text: 'Build a component', completed: false, createdAt: new Date() },
{ id: 3, text: 'Understand service patterns', completed: false, createdAt: new Date() },
]);
// Computed signals
totalCount = computed(() => this.todos().length);
completedCount = computed(() => this.todos().filter(t => t.completed).length);
activeCount = computed(() => this.todos().filter(t => !t.completed).length);
allCompleted = computed(() => this.totalCount() > 0 && this.activeCount() === 0);
filteredTodos = computed(() => {
const filter = this.currentFilter();
const todos = this.todos();
switch (filter) {
case 'active': return todos.filter(t => !t.completed);
case 'completed': return todos.filter(t => t.completed);
default: return todos;
}
});
filters: FilterType[] = ['all', 'active', 'completed'];
filterLabels: Record<FilterType, string> = {
all: 'All',
active: 'Active',
completed: 'Completed'
};
addTodo() {
const text = this.newTodoText.trim();
if (!text) return;
const newTodo: Todo = {
id: this.nextId(),
text,
completed: false,
createdAt: new Date()
};
this.todos.update(todos => [...todos, newTodo]);
this.nextId.update(id => id + 1);
this.newTodoText = '';
}
toggleTodo(id: number) {
this.todos.update(todos =>
todos.map(t => t.id === id ? { ...t, completed: !t.completed } : t)
);
}
deleteTodo(id: number) {
this.todos.update(todos => todos.filter(t => t.id !== id));
}
toggleAll() {
const shouldComplete = !this.allCompleted();
this.todos.update(todos =>
todos.map(t => ({ ...t, completed: shouldComplete }))
);
}
clearCompleted() {
this.todos.update(todos => todos.filter(t => !t.completed));
}
setFilter(filter: FilterType) {
this.currentFilter.set(filter);
}
}
Pro Tips
Tip 1: Optimize track
The track in @for helps Angular identify which items changed. Using a unique ID greatly improves performance.
<!-- Good: track by unique ID -->
@for (item of items; track item.id) { ... }
<!-- Bad: track by index (inefficient when list order changes) -->
@for (item of items; track $index) { ... }
Tip 2: Component Separation Principle
Follow the single responsibility principle:
- Components handle UI display and event handling only
- Extract business logic into services
- Consider splitting when a component exceeds 100 lines
Tip 3: OnPush Change Detection
import { ChangeDetectionStrategy } from '@angular/core';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
// Re-renders only when Input reference changes or a Signal changes
// Essential for performance optimization
})
Tip 4: @defer for Lazy Loading
<!-- Only load when the element enters the viewport -->
@defer (on viewport) {
<app-heavy-chart />
} @placeholder {
<div class="skeleton">Loading chart...</div>
}