Angular Component Patterns
Introduction
Component design is fundamental to Angular application architecture. Using the right component patterns helps you create applications that are easier to maintain, test, and extend. In this guide, we'll explore common Angular component patterns that will help beginners structure their applications more effectively.
Components are the building blocks of Angular applications. They encapsulate the template, data, and behavior of a view. Learning how to properly structure these components will make your code more reusable, testable, and maintainable.
Core Component Patterns
Smart vs. Presentation Components
One of the most powerful patterns in Angular is the separation of components into two categories:
Presentation Components (Dumb Components)
Presentation components focus solely on displaying data and emitting events when users interact with them. They:
- Receive data via
@Input()
decorators - Emit events via
@Output()
decorators - Don't interact with services directly
- Are highly reusable
- Are easier to test due to fewer dependencies
Let's create a simple presentation component:
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-user-card',
template: `
<div class="user-card">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
<button (click)="onViewDetails()">View Details</button>
</div>
`,
styles: [`
.user-card {
padding: 16px;
border: 1px solid #ccc;
border-radius: 4px;
margin-bottom: 16px;
}
`]
})
export class UserCardComponent {
@Input() user: { name: string; email: string; id: number };
@Output() viewDetails = new EventEmitter<number>();
onViewDetails() {
this.viewDetails.emit(this.user.id);
}
}
Smart Components (Container Components)
Smart components handle data fetching and business logic. They:
- Use services to fetch data
- Manage state
- Pass data down to presentation components
- Handle events from presentation components
- Coordinate between multiple presentation components
Here's an example of a smart component that uses our presentation component:
import { Component, OnInit } from '@angular/core';
import { UserService } from '../services/user.service';
@Component({
selector: 'app-user-list',
template: `
<div>
<h1>User List</h1>
<div *ngIf="loading">Loading users...</div>
<app-user-card
*ngFor="let user of users"
[user]="user"
(viewDetails)="navigateToDetails($event)">
</app-user-card>
</div>
`
})
export class UserListComponent implements OnInit {
users = [];
loading = false;
constructor(private userService: UserService, private router: Router) {}
ngOnInit() {
this.loading = true;
this.userService.getUsers().subscribe(users => {
this.users = users;
this.loading = false;
});
}
navigateToDetails(userId: number) {
this.router.navigate(['/users', userId]);
}
}
Component Communication Patterns
Components often need to communicate with each other. Here are the main patterns for component communication:
Parent to Child: Input Properties
Using @Input()
decorators allows parent components to pass data to child components:
// Child component
@Component({
selector: 'app-child',
template: `<div>{{ message }}</div>`
})
export class ChildComponent {
@Input() message: string;
}
// Parent component
@Component({
selector: 'app-parent',
template: `
<app-child [message]="parentMessage"></app-child>
`
})
export class ParentComponent {
parentMessage = 'Hello from parent!';
}
Child to Parent: Output Events
Using @Output()
decorators allows child components to emit events to parent components:
// Child component
@Component({
selector: 'app-child',
template: `
<button (click)="sendMessage()">Send Message to Parent</button>
`
})
export class ChildComponent {
@Output() messageEvent = new EventEmitter<string>();
sendMessage() {
this.messageEvent.emit('Hello from child!');
}
}
// Parent component
@Component({
selector: 'app-parent',
template: `
<app-child (messageEvent)="receiveMessage($event)"></app-child>
<p>{{ message }}</p>
`
})
export class ParentComponent {
message = '';
receiveMessage(msg: string) {
this.message = msg;
}
}
Sibling Communication: Service Pattern
When components don't have a direct parent-child relationship, a shared service can facilitate communication:
// Shared service
@Injectable({
providedIn: 'root'
})
export class CommunicationService {
private messageSource = new BehaviorSubject<string>('Default message');
currentMessage = this.messageSource.asObservable();
changeMessage(message: string) {
this.messageSource.next(message);
}
}
// First component
@Component({
selector: 'app-sender',
template: `
<button (click)="sendMessage()">Send Message</button>
`
})
export class SenderComponent {
constructor(private comService: CommunicationService) {}
sendMessage() {
this.comService.changeMessage('Hello from sender component!');
}
}
// Second component
@Component({
selector: 'app-receiver',
template: `<p>{{ message }}</p>`
})
export class ReceiverComponent implements OnInit {
message: string;
constructor(private comService: CommunicationService) {}
ngOnInit() {
this.comService.currentMessage.subscribe(message => {
this.message = message;
});
}
}
Advanced Component Patterns
Component Composition with Content Projection
Content projection allows you to create more flexible components by projecting content from a parent component into designated spots in a child component.
Basic content projection:
// Child component
@Component({
selector: 'app-card',
template: `
<div class="card">
<div class="card-header">
<ng-content select="[card-header]"></ng-content>
</div>
<div class="card-body">
<ng-content select="[card-body]"></ng-content>
</div>
<div class="card-footer">
<ng-content select="[card-footer]"></ng-content>
</div>
</div>
`,
styles: [`
.card {
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 20px;
}
.card-header {
background-color: #f5f5f5;
padding: 10px 15px;
border-bottom: 1px solid #ddd;
}
.card-body {
padding: 15px;
}
.card-footer {
background-color: #f5f5f5;
padding: 10px 15px;
border-top: 1px solid #ddd;
}
`]
})
export class CardComponent {}
// Usage in parent
@Component({
selector: 'app-parent',
template: `
<app-card>
<h3 card-header>User Profile</h3>
<div card-body>
<p>Name: John Doe</p>
<p>Email: [email protected]</p>
</div>
<div card-footer>
<button>Edit Profile</button>
</div>
</app-card>
`
})
export class ParentComponent {}
This pattern allows you to create versatile container components that can be used in various contexts.
Stateless and Stateful Components
Stateless Components
Stateless components (often presentation components) don't manage their own state. They simply receive inputs and emit outputs:
@Component({
selector: 'app-button',
template: `
<button [disabled]="disabled" [class]="btnClass" (click)="onClick()">
{{ label }}
</button>
`,
styles: [`
.primary { background-color: blue; color: white; }
.secondary { background-color: gray; color: white; }
.danger { background-color: red; color: white; }
`]
})
export class ButtonComponent {
@Input() label: string;
@Input() disabled = false;
@Input() type: 'primary' | 'secondary' | 'danger' = 'primary';
@Output() buttonClick = new EventEmitter<void>();
get btnClass(): string {
return this.type;
}
onClick() {
this.buttonClick.emit();
}
}
Stateful Components
Stateful components manage their internal state and often contain business logic:
@Component({
selector: 'app-counter',
template: `
<div>
<h2>Count: {{ count }}</h2>
<button (click)="increment()">+</button>
<button (click)="decrement()">-</button>
<button (click)="reset()">Reset</button>
</div>
`
})
export class CounterComponent {
count = 0;
increment() {
this.count++;
}
decrement() {
if (this.count > 0) {
this.count--;
}
}
reset() {
this.count = 0;
}
}
Real-World Example: Building a Todo List Application
Let's apply these patterns to build a simple todo list application:
Step 1: Create a Todo Item Interface
// todo.model.ts
export interface Todo {
id: number;
title: string;
completed: boolean;
}
Step 2: Create the Presentation Components
// todo-item.component.ts
@Component({
selector: 'app-todo-item',
template: `
<div class="todo-item" [class.completed]="todo.completed">
<input
type="checkbox"
[checked]="todo.completed"
(change)="onToggle()"
/>
<span>{{ todo.title }}</span>
<button (click)="onDelete()">Delete</button>
</div>
`,
styles: [`
.todo-item {
display: flex;
align-items: center;
padding: 8px;
margin-bottom: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.todo-item.completed span {
text-decoration: line-through;
color: #888;
}
.todo-item button {
margin-left: auto;
}
`]
})
export class TodoItemComponent {
@Input() todo: Todo;
@Output() toggle = new EventEmitter<number>();
@Output() delete = new EventEmitter<number>();
onToggle() {
this.toggle.emit(this.todo.id);
}
onDelete() {
this.delete.emit(this.todo.id);
}
}
// todo-form.component.ts
@Component({
selector: 'app-todo-form',
template: `
<form (ngSubmit)="onSubmit()">
<input
type="text"
[(ngModel)]="newTodo"
name="newTodo"
placeholder="Add a new todo"
required
/>
<button type="submit" [disabled]="!newTodo">Add</button>
</form>
`,
styles: [`
form {
display: flex;
margin-bottom: 20px;
}
input {
flex-grow: 1;
padding: 8px;
margin-right: 8px;
}
`]
})
export class TodoFormComponent {
@Output() add = new EventEmitter<string>();
newTodo = '';
onSubmit() {
if (this.newTodo.trim()) {
this.add.emit(this.newTodo.trim());
this.newTodo = '';
}
}
}
Step 3: Create the Smart Component
// todo-list.component.ts
@Component({
selector: 'app-todo-list',
template: `
<div class="todo-container">
<h1>Todo List</h1>
<app-todo-form (add)="addTodo($event)"></app-todo-form>
<div *ngIf="todos.length === 0">No todos yet! Add one above.</div>
<app-todo-item
*ngFor="let todo of todos"
[todo]="todo"
(toggle)="toggleTodo($event)"
(delete)="deleteTodo($event)"
></app-todo-item>
</div>
`,
styles: [`
.todo-container {
max-width: 500px;
margin: 0 auto;
padding: 20px;
}
h1 {
text-align: center;
margin-bottom: 20px;
}
`]
})
export class TodoListComponent {
todos: Todo[] = [
{ id: 1, title: 'Learn Angular Components', completed: false },
{ id: 2, title: 'Practice component patterns', completed: false },
{ id: 3, title: 'Build a todo app', completed: true }
];
nextId = 4;
addTodo(title: string) {
this.todos.push({
id: this.nextId++,
title,
completed: false
});
}
toggleTodo(id: number) {
this.todos = this.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
);
}
deleteTodo(id: number) {
this.todos = this.todos.filter(todo => todo.id !== id);
}
}
This example demonstrates:
- Smart vs. Presentation components separation
- Parent-to-child communication using
@Input()
- Child-to-parent communication using
@Output()
- Stateful and stateless components
Summary
In this guide, we've covered essential Angular component patterns:
-
Smart vs. Presentation Components: Separating components by responsibility makes your code more maintainable and testable.
-
Component Communication Patterns: Using
@Input()
and@Output()
decorators for parent-child communication, and services for sibling communication. -
Content Projection: Creating flexible components that can accept content from their parent components.
-
Stateless and Stateful Components: Distinguishing between components that manage state and those that are purely presentational.
By applying these patterns, you'll create Angular applications that are easier to maintain, test, and extend. These patterns form the foundation of Angular best practices and will help you structure your components effectively as your applications grow in complexity.
Additional Resources
- Angular Official Documentation on Components
- Angular Style Guide
- NgRx for State Management - For more advanced state management patterns
Exercises
- Refactor an existing component in your application to follow the smart/presentation pattern.
- Create a reusable card component using content projection that can be used in different parts of your application.
- Build a simple shopping cart component that demonstrates parent-child communication using
@Input()
and@Output()
. - Create a service to enable communication between unrelated components and implement it in a simple application.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)