Components: The Building Blocks of Angular
Components are the fundamental building blocks of Angular applications. They control views (patches of screen) and encapsulate the data, logic, and behavior needed to render them.
Think of components like prefabricated, self-contained sections of a building. Just as prefab sections can be assembled to create a complete structure, components can be combined to build a complete application.
Benefits of Component-Based Architecture
- Reusability: Components can be reused throughout the application, promoting DRY (Don't Repeat Yourself) principles
- Maintainability: Each component has a single responsibility, making the codebase easier to maintain
- Testability: Components can be tested in isolation, simplifying unit testing
- Readability: Component-based code tends to be more readable and self-documenting
- Collaboration: Different team members can work on different components simultaneously
- Scalability: Applications can grow by adding more components, not by making existing code more complex
Anatomy of an Angular Component
Angular components consist of several parts working together:
Component Decorator
The @Component decorator adds metadata to the component class:
@Component({
selector: 'app-product-list', // The HTML tag used to insert this component
templateUrl: './product-list.component.html', // External template file
// OR template: `<div>Inline template content</div>`, // Inline template
styleUrls: ['./product-list.component.css'], // External styles
// OR styles: [`h1 { color: blue; }`], // Inline styles
providers: [ProductService], // Component-specific service providers
encapsulation: ViewEncapsulation.Emulated, // Style encapsulation mode
changeDetection: ChangeDetectionStrategy.OnPush // Change detection strategy
})
Component Class
The TypeScript class that defines the component's behavior:
export class ProductListComponent implements OnInit, OnDestroy {
// Properties (state)
products: Product[] = [];
selectedProduct: Product | null = null;
loading = false;
error: string | null = null;
private subscription: Subscription | null = null;
// Constructor for dependency injection
constructor(
private productService: ProductService,
private router: Router,
private changeDetectorRef: ChangeDetectorRef
) { }
// Lifecycle hooks
ngOnInit(): void {
this.loadProducts();
}
ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
// Methods (behavior)
loadProducts(): void {
this.loading = true;
this.error = null;
this.subscription = this.productService.getProducts()
.pipe(
finalize(() => {
this.loading = false;
this.changeDetectorRef.markForCheck();
})
)
.subscribe({
next: (products) => this.products = products,
error: (err) => this.error = 'Failed to load products: ' + err.message
});
}
selectProduct(product: Product): void {
this.selectedProduct = product;
}
viewProductDetails(id: number): void {
this.router.navigate(['/products', id]);
}
}
Component Template
The HTML template that defines the component's view:
<!-- product-list.component.html -->
<div class="product-list-container">
<h2>Products</h2>
<div *ngIf="loading" class="loading-spinner">
Loading...
</div>
<div *ngIf="error" class="error-message">
{{ error }}
<button (click)="loadProducts()">Try Again</button>
</div>
<div *ngIf="!loading && !error">
<div *ngIf="products.length === 0" class="no-products">
No products found.
</div>
<div *ngIf="products.length > 0" class="product-grid">
<div *ngFor="let product of products"
class="product-card"
[class.selected]="product === selectedProduct"
(click)="selectProduct(product)">
<img [src]="product.imageUrl" [alt]="product.name">
<h3>{{ product.name }}</h3>
<p class="price">{{ product.price | currency }}</p>
<button (click)="viewProductDetails(product.id); $event.stopPropagation()">
View Details
</button>
</div>
</div>
</div>
</div>
Component Styles
CSS styles for the component:
/* product-list.component.css */
.product-list-container {
padding: 20px;
}
.loading-spinner {
display: flex;
justify-content: center;
padding: 20px;
}
.error-message {
color: red;
padding: 10px;
border: 1px solid red;
border-radius: 4px;
margin-bottom: 20px;
}
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
}
.product-card {
border: 1px solid #ddd;
border-radius: 4px;
padding: 15px;
transition: transform 0.3s ease, box-shadow 0.3s ease;
cursor: pointer;
}
.product-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.product-card.selected {
border-color: #dd0031;
box-shadow: 0 0 0 2px rgba(221, 0, 49, 0.3);
}
.product-card img {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 4px;
}
.product-card h3 {
margin: 10px 0;
}
.product-card .price {
font-weight: bold;
color: #333;
}
Creating Components
There are two main ways to create components in Angular:
Using Angular CLI (Recommended)
// Generate a component
ng generate component products/product-list
// or shorthand
ng g c products/product-list
The CLI generates four files:
product-list.component.ts- The component class fileproduct-list.component.html- The template fileproduct-list.component.css- The styles fileproduct-list.component.spec.ts- The test file
And it updates the nearest module to include your component.
Manual Creation
You can also create components manually:
- Create the component files (TS, HTML, CSS)
- Add the
@Componentdecorator to the class - Export the component class
- Register the component in a module's
declarationsarray
Generating Components with Options
// Generate a component without spec (test) file
ng g c products/product-detail --skip-tests
// Generate a component with inline template and styles
ng g c shared/button --inline-template --inline-style
// or shorthand
ng g c shared/button -t -s
// Generate a component with a specific module
ng g c admin/user-list --module=admin
// Generate a component with change detection strategy
ng g c products/product-card --change-detection=OnPush
Component Lifecycle Hooks
Angular components have a lifecycle that you can tap into with hooks:
Key Lifecycle Hooks
- constructor: Called before any lifecycle hooks; used for dependency injection
- ngOnChanges: Called when input properties change
- ngOnInit: Called once after the first ngOnChanges; used for initialization
- ngDoCheck: Called during every change detection run
- ngAfterContentInit: Called after content (ng-content) has been projected
- ngAfterContentChecked: Called after every check of projected content
- ngAfterViewInit: Called after component's view and child views are initialized
- ngAfterViewChecked: Called after every check of the component's view
- ngOnDestroy: Called before the component is destroyed; used for cleanup
import {
Component, OnInit, OnDestroy, OnChanges,
AfterViewInit, SimpleChanges, Input
} from '@angular/core';
@Component({
selector: 'app-lifecycle-demo',
template: `<p>Value: {{ value }}</p>`
})
export class LifecycleDemoComponent implements OnInit, OnChanges, AfterViewInit, OnDestroy {
@Input() value: string = '';
constructor() {
console.log('Constructor called');
// Do dependecy injection here
// DON'T do complex initialization here
}
ngOnChanges(changes: SimpleChanges): void {
console.log('ngOnChanges called', changes);
// Respond to input property changes
}
ngOnInit(): void {
console.log('ngOnInit called');
// Do initialization that requires input properties
// Fetch initial data
// Set up subscriptions
}
ngAfterViewInit(): void {
console.log('ngAfterViewInit called');
// Do operations that need the view to be fully initialized
// Manipulate DOM elements
// Initialize third-party UI libraries
}
ngOnDestroy(): void {
console.log('ngOnDestroy called');
// Clean up resources
// Unsubscribe from observables
// Remove event listeners
// Prevent memory leaks
}
}
Common Use Cases for Lifecycle Hooks
| Lifecycle Hook | Common Use Cases |
|---|---|
| constructor |
|
| ngOnInit |
|
| ngOnChanges |
|
| ngAfterViewInit |
|
| ngOnDestroy |
|
Component Interaction
Angular components can interact in several ways:
Parent to Child: @Input
Parents pass data to children using input properties:
// child.component.ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-child',
template: `<p>Hello, {{ name }}!</p>`
})
export class ChildComponent {
@Input() name: string = ''; // Input property decorated with @Input()
}
// parent.component.html
<app-child [name]="parentName"></app-child>
// parent.component.ts
@Component({
selector: 'app-parent',
templateUrl: './parent.component.html'
})
export class ParentComponent {
parentName = 'John';
}
Child to Parent: @Output and EventEmitter
Children emit events to communicate with parents:
// child.component.ts
import { Component, Output, EventEmitter } from '@angular/core';
@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.html
<app-child (messageEvent)="receiveMessage($event)"></app-child>
<p>Message from child: {{ message }}</p>
// parent.component.ts
@Component({
selector: 'app-parent',
templateUrl: './parent.component.html'
})
export class ParentComponent {
message = '';
receiveMessage(msg: string) {
this.message = msg;
}
}
Two-Way Binding with ngModel
Angular allows for two-way binding with the banana-in-a-box syntax [(ngModel)]:
// custom-input.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-custom-input',
template: `
<input
[value]="value"
(input)="valueChange.emit($event.target.value)"
>
`
})
export class CustomInputComponent {
@Input() value: string = '';
@Output() valueChange = new EventEmitter<string>();
}
// parent.component.html
<app-custom-input [(value)]="name"></app-custom-input>
<p>Current name: {{ name }}</p>
// This is equivalent to:
<app-custom-input [value]="name" (valueChange)="name = $event"></app-custom-input>
Parent Accessing Child: ViewChild
Parents can access child component instances using @ViewChild:
// timer.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-timer',
template: `<p>{{ seconds }} seconds have passed</p>`
})
export class TimerComponent {
seconds = 0;
start() {
this.seconds = 0;
setInterval(() => this.seconds++, 1000);
}
stop() {
// Code to stop timer
}
}
// parent.component.ts
import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { TimerComponent } from './timer.component';
@Component({
selector: 'app-parent',
template: `
<app-timer></app-timer>
<button (click)="startTimer()">Start Timer</button>
`
})
export class ParentComponent implements AfterViewInit {
@ViewChild(TimerComponent) timerComponent!: TimerComponent;
ngAfterViewInit() {
// Child component is available after view is initialized
console.log('Timer component reference:', this.timerComponent);
}
startTimer() {
this.timerComponent.start();
}
}
Child Accessing Parent: Dependency Injection
Children can access parent components through the constructor with dependency injection:
// parent.component.ts
@Component({
selector: 'app-parent',
template: `
<app-child></app-child>
`
})
export class ParentComponent {
parentMethod() {
return 'Data from parent';
}
}
// child.component.ts
@Component({
selector: 'app-child',
template: `<p>{{ parentData }}</p>`
})
export class ChildComponent implements OnInit {
parentData: string = '';
constructor(private parent: ParentComponent) { }
ngOnInit() {
this.parentData = this.parent.parentMethod();
}
}
Component Projection with ng-content
Angular allows you to project content from a parent component into a child component using ng-content:
// card.component.ts
@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></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 {
padding: 10px;
background-color: #f5f5f5;
border-bottom: 1px solid #ddd;
}
.card-body {
padding: 15px;
}
.card-footer {
padding: 10px;
background-color: #f5f5f5;
border-top: 1px solid #ddd;
}
`]
})
export class CardComponent { }
// usage in parent.component.html
<app-card>
<h2 card-header>Product Details</h2>
<div>
<p>This is the main content of the card.</p>
<p>It will be projected into the default ng-content.</p>
</div>
<div card-footer>
<button>Add to Cart</button>
<button>View Details</button>
</div>
</app-card>
Content projection allows for more flexible and reusable components, similar to the "slots" concept in other frameworks.
Multiple Content Projections
Using the select attribute, you can project content to specific locations in the component:
select="[attribute]": Matches elements with a specific attributeselect=".class-name": Matches elements with a specific classselect="element-name": Matches specific elementsselect="selector, another-selector": Matches multiple selectors
Conditional Content Projection
You can check if content is projected using @ContentChild:
// card.component.ts
import { Component, ContentChild, ElementRef, AfterContentInit } from '@angular/core';
@Component({
selector: 'app-card',
template: `
<div class="card">
<div *ngIf="hasHeader" class="card-header">
<ng-content select="[card-header]"></ng-content>
</div>
<div class="card-body">
<ng-content></ng-content>
</div>
<div *ngIf="hasFooter" class="card-footer">
<ng-content select="[card-footer]"></ng-content>
</div>
</div>
`
})
export class CardComponent implements AfterContentInit {
@ContentChild('[card-header]') headerContent: ElementRef | undefined;
@ContentChild('[card-footer]') footerContent: ElementRef | undefined;
hasHeader = false;
hasFooter = false;
ngAfterContentInit() {
this.hasHeader = !!this.headerContent;
this.hasFooter = !!this.footerContent;
}
}
Smart vs. Presentational Components
A common pattern in Angular applications is to distinguish between two types of components:
Smart Components (Container Components)
- Focus on how things work
- Fetch data and maintain state
- Pass data to presentational components
- Respond to events from presentational components
- Typically connected to services
- May contain other smart or presentational components
Presentational Components (Dumb Components)
- Focus on how things look
- Receive data through @Input
- Emit events through @Output
- Don't fetch data or maintain complex state
- Can be reused in different contexts
- Often have css styles and animations
Example: Smart Component
// product-list.component.ts
@Component({
selector: 'app-product-list',
template: `
<div>
<h2>Products</h2>
<div *ngIf="loading">Loading...</div>
<div *ngIf="error">{{ error }}</div>
<div class="products-grid">
<app-product-card
*ngFor="let product of products"
[product]="product"
(select)="onSelectProduct($event)"
(addToCart)="onAddToCart($event)"
></app-product-card>
</div>
</div>
`
})
export class ProductListComponent implements OnInit {
products: Product[] = [];
loading = false;
error: string | null = null;
constructor(private productService: ProductService, private cartService: CartService) { }
ngOnInit() {
this.loading = true;
this.productService.getProducts().subscribe({
next: (products) => {
this.products = products;
this.loading = false;
},
error: (err) => {
this.error = 'Failed to load products';
this.loading = false;
console.error(err);
}
});
}
onSelectProduct(product: Product) {
// Handle product selection
}
onAddToCart(product: Product) {
this.cartService.addToCart(product);
}
}
Example: Presentational Component
// product-card.component.ts
@Component({
selector: 'app-product-card',
template: `
<div class="product-card">
<img [src]="product.imageUrl" [alt]="product.name">
<h3>{{ product.name }}</h3>
<p class="price">{{ product.price | currency }}</p>
<div class="actions">
<button (click)="onSelect()">View Details</button>
<button (click)="onAddToCart()">Add to Cart</button>
</div>
</div>
`,
styleUrls: ['./product-card.component.css']
})
export class ProductCardComponent {
@Input() product!: Product;
@Output() select = new EventEmitter<Product>();
@Output() addToCart = new EventEmitter<Product>();
onSelect() {
this.select.emit(this.product);
}
onAddToCart() {
this.addToCart.emit(this.product);
}
}
Benefits of This Pattern
- Separation of Concerns: Logic and presentation are separated
- Reusability: Presentational components can be reused in different contexts
- Testability: Components are easier to test when they have a single responsibility
- Maintainability: Changes to business logic don't affect UI components and vice versa
Change Detection
Angular's change detection determines when to update the DOM based on data changes:
Default Change Detection
By default, Angular uses a "check everything" approach:
- Every component is checked on any potential change (event, timer, XHR, etc.)
- Changes flow from parent to child (unidirectional)
- Results in predictable but potentially less efficient updates
OnPush Change Detection
For better performance, you can use OnPush strategy to update only when:
- Input references change (not just their properties)
- An event occurs within the component or its children
- You explicitly mark the component for checking
- An observable bound with the async pipe emits a new value
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
@Component({
selector: 'app-user-profile',
template: `
<div class="user-profile">
<h2>{{ user.name }}</h2>
<p>Email: {{ user.email }}</p>
<button (click)="updateLastSeen()">Update Last Seen</button>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserProfileComponent {
@Input() user!: User;
constructor(private changeDetectorRef: ChangeDetectorRef) {}
updateLastSeen() {
// This won't update the view unless we explicitly mark it for checking
// because we're modifying a property of an existing object
this.user.lastSeen = new Date();
// Force change detection
this.changeDetectorRef.markForCheck();
}
}
Immutable Data with OnPush
For the most efficient OnPush usage, use immutable objects:
// In the parent component
updateUser() {
// Don't modify the existing user object
// this.user.name = 'New Name'; // This won't trigger OnPush change detection
// Instead, create a new user object
this.user = { ...this.user, name: 'New Name' };
}
Using the Async Pipe
The async pipe automatically triggers change detection when Observables emit new values:
@Component({
selector: 'app-user-profile',
template: `
<div *ngIf="user$ | async as user" class="user-profile">
<h2>{{ user.name }}</h2>
<p>Email: {{ user.email }}</p>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserProfileComponent {
user$: Observable<User>;
constructor(private userService: UserService) {
this.user$ = this.userService.getCurrentUser();
}
}
Component Styles and Encapsulation
Angular provides style encapsulation to prevent styles from leaking between components:
Style Encapsulation Modes
- ViewEncapsulation.Emulated (default): Scopes styles to component without native Shadow DOM
- ViewEncapsulation.ShadowDom: Uses browser's native Shadow DOM (more powerful but less supported)
- ViewEncapsulation.None: No encapsulation, styles are global
import { Component, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-styled-component',
template: `<p class="text">This text is styled</p>`,
styles: [`
.text { color: blue; }
`],
encapsulation: ViewEncapsulation.Emulated // This is the default
})
export class StyledComponent { }
Style Inheritance
Some styles still inherit regardless of encapsulation:
- Font family, font size, and other inheritable CSS properties
:hostselector targets the component's host element::ng-deepforces styles to apply to child components
@Component({
selector: 'app-styled-parent',
template: `
<div class="container">
<h2>Parent Component</h2>
<app-styled-child></app-styled-child>
</div>
`,
styles: [`
/* Styles the host element (app-styled-parent) */
:host {
display: block;
border: 1px solid #ccc;
padding: 15px;
}
/* Regular styles, scoped to this component */
h2 { color: #dd0031; }
/* Forces style to apply to child components too */
::ng-deep .important {
font-weight: bold;
color: red;
}
`]
})
export class StyledParentComponent { }
Practice Activity
Building a Component Hierarchy
Create a product management system with multiple components:
- Set up a new Angular project if you haven't already:
ng new product-management cd product-management - Create the necessary models:
ng g interface models/product ng g interface models/categoryDefine the interfaces:
// models/product.ts export interface Product { id: number; name: string; description: string; price: number; imageUrl: string; categoryId: number; inStock: boolean; } // models/category.ts export interface Category { id: number; name: string; } - Create a data service:
ng g service services/productImplement the service:
// services/product.service.ts import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { Product } from '../models/product'; import { Category } from '../models/category'; @Injectable({ providedIn: 'root' }) export class ProductService { private products: Product[] = [ // Add sample products here ]; private categories: Category[] = [ // Add sample categories here ]; getProducts(): Observable<Product[]> { return of(this.products); } getCategories(): Observable<Category[]> { return of(this.categories); } getProductsByCategory(categoryId: number): Observable<Product[]> { return of(this.products.filter(p => p.categoryId === categoryId)); } } - Create components:
ng g c components/product-list ng g c components/product-card --change-detection=OnPush ng g c components/product-filter ng g c components/category-list - Build the component hierarchy:
- The ProductListComponent should be a smart component that fetches products
- The ProductCardComponent should be a presentational component that displays a product
- The ProductFilterComponent should emit filter events
- The CategoryListComponent should display categories and emit selections
Challenge
- Add a product detail component with route parameters
- Implement OnPush change detection for appropriate components
- Add content projection for a reusable card component
- Create a shopping cart with the ability to add/remove products
Key Takeaways
- Components are the building blocks of Angular applications
- Components consist of a TypeScript class, HTML template, and CSS styles
- Components have a lifecycle with hooks for various stages
- Components can interact through @Input, @Output, ViewChild, and content projection
- Separating components into smart and presentational improves reusability and testability
- OnPush change detection can improve performance for complex applications
- Style encapsulation keeps component styles isolated
- Angular CLI provides commands to generate and work with components