Module Overview
Estimated Time : 4-5 hours | Difficulty : Beginner-Intermediate | Prerequisites : Module 1
Components are the fundamental building blocks of Angular applications. Every Angular app has at least one component—the root component—that connects a component tree to the page DOM.
What You’ll Learn:
Component anatomy and architecture
Data binding (interpolation, property, event, two-way)
Component communication with @Input and @Output
Content projection with ng-content
ViewChild and ContentChild queries
Lifecycle hooks
Component Anatomy
Every Angular component consists of three parts:
┌─────────────────────────────────────────────────────────────────┐
│ Angular Component │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ │
│ │ TypeScript │ ← Logic, properties, methods │
│ │ (.component.ts) │ │
│ └──────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Template │ ← HTML structure with Angular syntax │
│ │ (.component.html)│ │
│ └──────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Styles │ ← Scoped CSS/SCSS │
│ │ (.component.scss)│ │
│ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Component Decorator
import { Component } from '@angular/core' ;
@ Component ({
// CSS selector for the component
selector: 'app-user-card' ,
// Standalone component (Angular 14+)
standalone: true ,
// Components, directives, pipes this component uses
imports: [ CommonModule , FormsModule ],
// Template options (use ONE)
templateUrl: './user-card.component.html' ,
// OR inline:
// template: `<h1>{{ title }}</h1>`,
// Style options (use ONE)
styleUrl: './user-card.component.scss' ,
// OR inline:
// styles: [`.card { padding: 1rem; }`],
// Change detection strategy (advanced)
changeDetection: ChangeDetectionStrategy . OnPush ,
// View encapsulation
encapsulation: ViewEncapsulation . Emulated // default
})
export class UserCardComponent {
// Component logic here
}
Data Binding
Angular provides four types of data binding:
┌─────────────────────────────────────────────────────────────────┐
│ Data Binding Types │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. INTERPOLATION {{ expression }} │
│ Component ──────────────────────────► Template │
│ Outputs data to the view │
│ │
│ 2. PROPERTY BINDING [property]="expression" │
│ Component ──────────────────────────► Template │
│ Binds to DOM properties │
│ │
│ 3. EVENT BINDING (event)="handler()" │
│ Component ◄────────────────────────── Template │
│ Responds to user actions │
│ │
│ 4. TWO-WAY BINDING [(ngModel)]="property" │
│ Component ◄─────────────────────────► Template │
│ Syncs data both ways │
│ │
└─────────────────────────────────────────────────────────────────┘
1. Interpolation
Display component data in the template:
// user.component.ts
export class UserComponent {
name = 'John Doe' ;
age = 30 ;
getFullName () : string {
return ` ${ this . name } ( ${ this . age } )` ;
}
}
<!-- user.component.html -->
< h1 > Welcome, {{ name }}! </ h1 >
< p > Age: {{ age }} </ p >
< p > {{ getFullName() }} </ p >
<!-- Expressions are allowed -->
< p > Next year: {{ age + 1 }} </ p >
< p > Uppercase: {{ name.toUpperCase() }} </ p >
<!-- Conditional expression -->
< p > {{ age >= 18 ? 'Adult' : 'Minor' }} </ p >
Avoid in interpolation:
Assignments ({{ name = 'test' }})
Multiple statements ({{ a; b }})
Increment/decrement ({{ i++ }})
new operator
Chained expressions
2. Property Binding
Bind to element properties (not HTML attributes):
<!-- Binding to native properties -->
< img [src] = "imageUrl" [alt] = "imageDescription" >
< button [disabled] = "isLoading" > Submit </ button >
< input [value] = "username" >
< div [hidden] = "!isVisible" > Content </ div >
<!-- Binding to component inputs -->
< app-user-card [user] = "currentUser" ></ app-user-card >
<!-- Binding to directive properties -->
< div [ngClass] = "{'active': isActive, 'disabled': isDisabled}" >
< div [ngStyle] = "{'color': textColor, 'font-size': fontSize}" >
Property vs Attribute : Properties are DOM node properties (JavaScript), while attributes are HTML markup attributes. Angular binds to properties!<!-- Attribute binding (special syntax for attributes) -->
< td [attr.colspan] = "colSpan" >
< button [attr.aria-label] = "label" >
3. Event Binding
Respond to user interactions:
<!-- Basic event binding -->
< button (click) = "onSave()" > Save </ button >
< input (input) = "onInput($event)" >
< form (submit) = "onSubmit($event)" >
<!-- Keyboard events -->
< input (keyup) = "onKeyUp($event)" >
< input (keydown.enter) = "onEnter()" >
< input (keydown.escape) = "onEscape()" >
<!-- Mouse events -->
< div (mouseenter) = "onHover()" >
< div (mouseleave) = "onLeave()" >
< div (dblclick) = "onDoubleClick()" >
export class FormComponent {
onSave () {
console . log ( 'Saving...' );
}
onInput ( event : Event ) {
const input = event . target as HTMLInputElement ;
console . log ( 'Input value:' , input . value );
}
onSubmit ( event : Event ) {
event . preventDefault ();
// Handle form submission
}
onKeyUp ( event : KeyboardEvent ) {
console . log ( 'Key pressed:' , event . key );
}
}
4. Two-Way Binding
Combine property and event binding:
import { Component } from '@angular/core' ;
import { FormsModule } from '@angular/forms' ;
@ Component ({
selector: 'app-search' ,
standalone: true ,
imports: [ FormsModule ], // Required for ngModel
template: `
<input [(ngModel)]="searchTerm" placeholder="Search...">
<p>Searching for: {{ searchTerm }}</p>
`
})
export class SearchComponent {
searchTerm = '' ;
}
How it works : [(ngModel)] is syntactic sugar for:< input [ngModel] = "searchTerm" (ngModelChange) = "searchTerm = $event" >
Component Communication
Pass data from parent to child component:
// child: user-card.component.ts
import { Component , Input } from '@angular/core' ;
interface User {
id : number ;
name : string ;
email : string ;
}
@ Component ({
selector: 'app-user-card' ,
standalone: true ,
template: `
<div class="card">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
`
})
export class UserCardComponent {
@ Input () user !: User ; // Required input
@ Input () showEmail = true ; // Optional with default
// Transform input
@ Input ({ transform: booleanAttribute }) disabled = false ;
// Alias
@ Input ({ alias: 'userData' }) user !: User ;
}
// parent: users.component.ts
@ Component ({
selector: 'app-users' ,
standalone: true ,
imports: [ UserCardComponent ],
template: `
@for (user of users; track user.id) {
<app-user-card
[user]="user"
[showEmail]="true">
</app-user-card>
}
`
})
export class UsersComponent {
users : User [] = [
{ id: 1 , name: 'John' , email: '[email protected] ' },
{ id: 2 , name: 'Jane' , email: '[email protected] ' }
];
}
@Output - Child to Parent
Emit events from child to parent:
// child: user-card.component.ts
import { Component , Input , Output , EventEmitter } from '@angular/core' ;
@ Component ({
selector: 'app-user-card' ,
standalone: true ,
template: `
<div class="card">
<h3>{{ user.name }}</h3>
<button (click)="onSelect()">Select</button>
<button (click)="onDelete()">Delete</button>
</div>
`
})
export class UserCardComponent {
@ Input () user !: User ;
@ Output () selected = new EventEmitter < User >();
@ Output () deleted = new EventEmitter < number >();
onSelect () {
this . selected . emit ( this . user );
}
onDelete () {
this . deleted . emit ( this . user . id );
}
}
// parent: users.component.ts
@ Component ({
selector: 'app-users' ,
standalone: true ,
imports: [ UserCardComponent ],
template: `
@for (user of users; track user.id) {
<app-user-card
[user]="user"
(selected)="onUserSelected($event)"
(deleted)="onUserDeleted($event)">
</app-user-card>
}
@if (selectedUser) {
<p>Selected: {{ selectedUser.name }}</p>
}
`
})
export class UsersComponent {
users : User [] = [];
selectedUser ?: User ;
onUserSelected ( user : User ) {
this . selectedUser = user ;
}
onUserDeleted ( userId : number ) {
this . users = this . users . filter ( u => u . id !== userId );
}
}
Modern Angular supports signal-based inputs and outputs:
import { Component , input , output } from '@angular/core' ;
@ Component ({
selector: 'app-user-card' ,
standalone: true ,
template: `
<div class="card">
<h3>{{ user().name }}</h3> <!-- Note: calling as function -->
<button (click)="select()">Select</button>
</div>
`
})
export class UserCardComponent {
// Required signal input
user = input . required < User >();
// Optional signal input with default
showEmail = input ( true );
// Signal output
selected = output < User >();
select () {
this . selected . emit ( this . user ());
}
}
Content Projection
Project content from parent into child component slots:
Basic Projection
// card.component.ts
@ Component ({
selector: 'app-card' ,
standalone: true ,
template: `
<div class="card">
<ng-content></ng-content> <!-- Content goes here -->
</div>
` ,
styles: [ `.card { border: 1px solid #ddd; padding: 1rem; }` ]
})
export class CardComponent {}
<!-- Usage -->
< app-card >
< h2 > Card Title </ h2 >
< p > This content is projected into the card. </ p >
</ app-card >
Multi-Slot Projection
// card.component.ts
@ Component ({
selector: 'app-card' ,
standalone: true ,
template: `
<div class="card">
<div class="card-header">
<ng-content select="[card-header]"></ng-content>
</div>
<div class="card-body">
<ng-content></ng-content> <!-- Default slot -->
</div>
<div class="card-footer">
<ng-content select="[card-footer]"></ng-content>
</div>
</div>
`
})
export class CardComponent {}
<!-- Usage -->
< app-card >
< div card-header >
< h2 > Card Title </ h2 >
</ div >
< p > This goes in the body (default slot). </ p >
< p > More body content. </ p >
< div card-footer >
< button > Save </ button >
</ div >
</ app-card >
ViewChild & ContentChild
Query for elements or components in the view:
ViewChild
Access elements in the component’s own template:
import {
Component ,
ViewChild ,
ElementRef ,
AfterViewInit
} from '@angular/core' ;
@ Component ({
selector: 'app-search' ,
standalone: true ,
template: `
<input #searchInput type="text">
<button (click)="focusInput()">Focus</button>
<app-results #resultsComponent></app-results>
`
})
export class SearchComponent implements AfterViewInit {
// Query for native element
@ ViewChild ( 'searchInput' )
searchInput !: ElementRef < HTMLInputElement >;
// Query for component
@ ViewChild ( ResultsComponent )
resultsComponent !: ResultsComponent ;
ngAfterViewInit () {
// ViewChild is available here
console . log ( this . searchInput . nativeElement );
}
focusInput () {
this . searchInput . nativeElement . focus ();
}
}
ContentChild
Access projected content:
@ Component ({
selector: 'app-collapsible' ,
standalone: true ,
template: `
<button (click)="toggle()">Toggle</button>
<div [hidden]="collapsed">
<ng-content></ng-content>
</div>
`
})
export class CollapsibleComponent implements AfterContentInit {
collapsed = false ;
@ ContentChild ( 'headerContent' )
headerContent ?: ElementRef ;
ngAfterContentInit () {
// ContentChild is available here
if ( this . headerContent ) {
console . log ( 'Header content found' );
}
}
toggle () {
this . collapsed = ! this . collapsed ;
}
}
Lifecycle Hooks
Complete Lifecycle
import {
Component ,
OnInit ,
OnChanges ,
DoCheck ,
AfterContentInit ,
AfterContentChecked ,
AfterViewInit ,
AfterViewChecked ,
OnDestroy ,
Input ,
SimpleChanges
} from '@angular/core' ;
@ Component ({
selector: 'app-lifecycle' ,
standalone: true ,
template: `<p>Lifecycle Demo</p>`
})
export class LifecycleComponent implements
OnInit ,
OnChanges ,
DoCheck ,
AfterContentInit ,
AfterContentChecked ,
AfterViewInit ,
AfterViewChecked ,
OnDestroy {
@ Input () data = '' ;
constructor () {
console . log ( '1. Constructor - DI happens here' );
}
ngOnChanges ( changes : SimpleChanges ) {
console . log ( '2. ngOnChanges - Input changed' , changes );
// Access previous and current values
if ( changes [ 'data' ]) {
console . log ( 'Previous:' , changes [ 'data' ]. previousValue );
console . log ( 'Current:' , changes [ 'data' ]. currentValue );
console . log ( 'First change:' , changes [ 'data' ]. firstChange );
}
}
ngOnInit () {
console . log ( '3. ngOnInit - Initialize data, fetch from API' );
// @Input values are available here
// Good place to set up subscriptions
}
ngDoCheck () {
console . log ( '4. ngDoCheck - Custom change detection' );
// Called on every change detection run
// Use sparingly - performance intensive
}
ngAfterContentInit () {
console . log ( '5. ngAfterContentInit - Projected content initialized' );
// @ContentChild available here
}
ngAfterContentChecked () {
console . log ( '6. ngAfterContentChecked - Projected content checked' );
}
ngAfterViewInit () {
console . log ( '7. ngAfterViewInit - View initialized' );
// @ViewChild available here
// Good place to interact with DOM
}
ngAfterViewChecked () {
console . log ( '8. ngAfterViewChecked - View checked' );
}
ngOnDestroy () {
console . log ( '9. ngOnDestroy - Cleanup!' );
// Unsubscribe from observables
// Clear timers/intervals
// Disconnect from WebSocket
}
}
Common Patterns
@ Component ({
selector: 'app-data-fetcher' ,
standalone: true ,
imports: [ AsyncPipe ],
template: `
@if (loading) {
<p>Loading...</p>
}
@if (data$ | async; as data) {
<pre>{{ data | json }}</pre>
}
`
})
export class DataFetcherComponent implements OnInit , OnDestroy {
private destroy$ = new Subject < void >();
data$ !: Observable < any >;
loading = true ;
private dataService = inject ( DataService );
ngOnInit () {
// Fetch data on init
this . data$ = this . dataService . getData (). pipe (
tap (() => this . loading = false ),
takeUntil ( this . destroy$ ) // Cleanup on destroy
);
}
ngOnDestroy () {
this . destroy$ . next ();
this . destroy$ . complete ();
}
}
Best Practices
Smart vs Dumb Components
Smart : Handle logic, fetch data, contain business logic
Dumb : Receive data via @Input, emit events via @Output, purely presentational
Single Responsibility Each component should do one thing well. If it’s getting complex, split it!
Use OnPush Use ChangeDetectionStrategy.OnPush for better performance with immutable data
Cleanup Subscriptions Always unsubscribe in ngOnDestroy or use takeUntil pattern
Practice Exercise
Exercise: Build a Todo List Component Create a todo list with:
Parent component managing the list
Child component for individual todo items
Add new todo functionality
Mark as complete
Delete todo
Use @Input and @Output for communication.
// todo-item.component.ts
@ Component ({
selector: 'app-todo-item' ,
standalone: true ,
template: `
<div class="todo-item" [class.completed]="todo.completed">
<input
type="checkbox"
[checked]="todo.completed"
(change)="onToggle()">
<span>{{ todo.text }}</span>
<button (click)="onDelete()">×</button>
</div>
`
})
export class TodoItemComponent {
@ Input () todo !: { id : number ; text : string ; completed : boolean };
@ 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-list.component.ts
@ Component ({
selector: 'app-todo-list' ,
standalone: true ,
imports: [ TodoItemComponent , FormsModule ],
template: `
<div class="todo-list">
<div class="add-todo">
<input [(ngModel)]="newTodoText" placeholder="Add todo...">
<button (click)="addTodo()">Add</button>
</div>
@for (todo of todos; track todo.id) {
<app-todo-item
[todo]="todo"
(toggle)="toggleTodo($event)"
(delete)="deleteTodo($event)">
</app-todo-item>
}
</div>
`
})
export class TodoListComponent {
newTodoText = '' ;
todos = [
{ id: 1 , text: 'Learn Angular' , completed: false },
{ id: 2 , text: 'Build an app' , completed: false }
];
addTodo () {
if ( this . newTodoText . trim ()) {
this . todos . push ({
id: Date . now (),
text: this . newTodoText ,
completed: false
});
this . newTodoText = '' ;
}
}
toggleTodo ( id : number ) {
const todo = this . todos . find ( t => t . id === id );
if ( todo ) todo . completed = ! todo . completed ;
}
deleteTodo ( id : number ) {
this . todos = this . todos . filter ( t => t . id !== id );
}
}
Summary
In this module, you learned:
Component Structure
Components consist of TypeScript class, HTML template, and CSS styles
Data Binding
Four types: interpolation, property, event, and two-way binding
Component Communication
@Input for parent-to-child, @Output for child-to-parent
Content Projection
Using ng-content for flexible component composition
Lifecycle Hooks
Managing component initialization, changes, and cleanup
Next Steps
Next: Directives & Pipes Learn about structural directives, attribute directives, and pipes for data transformation