Skip to main content
Advertisement

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:

  1. Class (TypeScript) — data and logic
  2. Template (HTML) — UI structure
  3. 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:

VariableTypeDescription
$indexnumberCurrent index (starts at 0)
$countnumberTotal number of items
$firstbooleanWhether it is the first item
$lastbooleanWhether it is the last item
$evenbooleanWhether the index is even
$oddbooleanWhether 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>
}
Advertisement