Skip to main content

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:

typescript
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:

typescript
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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
@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:

typescript
@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

typescript
// todo.model.ts
export interface Todo {
id: number;
title: string;
completed: boolean;
}

Step 2: Create the Presentation Components

typescript
// 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

typescript
// 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:

  1. Smart vs. Presentation Components: Separating components by responsibility makes your code more maintainable and testable.

  2. Component Communication Patterns: Using @Input() and @Output() decorators for parent-child communication, and services for sibling communication.

  3. Content Projection: Creating flexible components that can accept content from their parent components.

  4. 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

Exercises

  1. Refactor an existing component in your application to follow the smart/presentation pattern.
  2. Create a reusable card component using content projection that can be used in different parts of your application.
  3. Build a simple shopping cart component that demonstrates parent-child communication using @Input() and @Output().
  4. 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! :)