Chapter 1 — What We Are Going to Learn and Why
In Phase 2 you learned what a component is and how to create one. You know that a component has a TypeScript file, an HTML template, and a CSS file. You know how to generate components and wire them together.
But so far you have only scratched the surface. A component is capable of so much more than just displaying some static data.
Think about any real application. A product card on an e-commerce site receives product data from its parent and displays it. A like button sends an event back up to the parent when clicked. A modal dialog only runs certain code when it first appears and cleans up when it disappears. A form validates user input and reacts to every keystroke.
All of this is possible because of what we are covering in this phase — the deeper mechanics of how components work.
Here is what we will cover:
We will start with lifecycle hooks — Angular gives every component a predictable life cycle, from the moment it is created to the moment it is destroyed. You can hook into specific moments of this lifecycle and run your own code. This is how you load data when a page opens, react to changes, and clean up resources when a component is removed.
Then we will cover @Input() — the mechanism for passing data from a parent component down to a child component. This is how you build reusable components that can display different data each time they are used.
Then @Output() and EventEmitter — the mechanism for a child component to communicate back up to its parent. The child says "something happened" and the parent listens and reacts.
Then @ViewChild — a way for a parent to directly access and talk to a child component from TypeScript code.
Then content projection with ng-content — a powerful feature that lets you build wrapper components that can accept and display HTML passed in from outside.
Then ViewEncapsulation — how Angular keeps your component's CSS styles scoped and isolated.
Then change detection — how Angular knows when data has changed and when it needs to update the screen.
By the end of this phase you will understand components at a deep level. Let's start.
Chapter 2 — Lifecycle Hooks
2.1 — What is a Lifecycle?
Every Angular component has a lifecycle. A lifecycle is simply the sequence of events that happen from the moment a component is created to the moment it is destroyed.
Think of it like a human life:
You are born → you grow up → you go through changes → eventually you pass away.
A component has something similar:
Component is created → its template is rendered → its data changes → eventually it is removed from the screen.
Angular provides lifecycle hooks — special methods you can add to your component class that Angular automatically calls at specific moments in this lifecycle. They are called "hooks" because you are "hooking into" a specific moment to run your own code.
This is incredibly useful. For example:
- You want to load data from an API when a component first appears on screen. You hook into the "component just initialized" moment.
- You want to react every time a specific piece of data changes. You hook into the "inputs just changed" moment.
- You want to cancel an ongoing subscription when a component is removed. You hook into the "component is about to be destroyed" moment.
Without lifecycle hooks, you would have no reliable way to do any of this.
2.2 — The 8 Lifecycle Hooks
Angular has 8 lifecycle hooks in total. They fire in a specific order. Let's go through each one.
ngOnChanges
This hook fires when an @Input() property value changes. It is the first hook to fire, even before ngOnInit. It fires once when the component first loads (with the initial input values) and then again every time any input value changes after that.
It receives a SimpleChanges object that tells you what changed, what the previous value was, and what the current value is.
import { Component, OnChanges, SimpleChanges, Input } from '@angular/core';
@Component({
selector: 'app-product-card',
imports: [],
templateUrl: './product-card.html',
styleUrl: './product-card.css'
})
export class ProductCard implements OnChanges {
@Input() productName: string = '';
@Input() price: number = 0;
ngOnChanges(changes: SimpleChanges): void {
console.log('Something changed!', changes);
if (changes['productName']) {
console.log('Previous name:', changes['productName'].previousValue);
console.log('Current name:', changes['productName'].currentValue);
console.log('Is this the first change?', changes['productName'].firstChange);
}
}
}
Notice implements OnChanges after the class name. This is a TypeScript interface. It is not required to make the hook work, but it is a best practice because TypeScript will warn you if you spell the method name wrong.
When to use it: When you receive data through @Input() and you need to react to it changing. For example, re-running a calculation when a new value comes in.
ngOnInit
This is the most important and most commonly used lifecycle hook. It fires once, after Angular has finished setting up the component and initialized all its @Input() properties. It only ever fires once in the component's lifetime.
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-user-profile',
imports: [],
templateUrl: './user-profile.html',
styleUrl: './user-profile.css'
})
export class UserProfile implements OnInit {
userData: any = null;
ngOnInit(): void {
console.log('Component is ready!');
// This is where you load data from an API
// This is where you run any one-time setup code
this.loadUserData();
}
loadUserData(): void {
// we will use a real API call in Phase 8
// for now, simulating it:
this.userData = {
name: 'Rahul Sharma',
email: 'rahul@example.com',
role: 'Developer'
};
}
}
You might wonder — why not just put initialization code in the constructor? The constructor is Angular's very first step when creating a component, but at that point Angular has not yet set up the @Input() properties. ngOnInit fires after everything is ready, so it is always the right place for initialization logic.
When to use it: Almost everything you want to run once when a component loads. Loading data from an API, setting up initial values, starting a timer, subscribing to a data stream.
ngDoCheck
This hook fires on every single change detection cycle — which means it can fire very frequently. Angular runs change detection whenever something happens in the app: a click, a keypress, an HTTP response, a timer firing. Every single time, ngDoCheck fires.
import { Component, DoCheck } from '@angular/core';
@Component({
selector: 'app-monitor',
imports: [],
template: `<p>Monitoring changes</p>`
})
export class Monitor implements DoCheck {
ngDoCheck(): void {
// This fires extremely often — be careful
// Only put very lightweight code here
console.log('Change detection cycle ran');
}
}
When to use it: Only when you need to manually detect a change that Angular cannot detect on its own — for example, changes deep inside an object or array. This is an advanced use case. Most beginners will rarely need this.
ngAfterContentInit
Before explaining this hook, you need to understand content projection. Content projection is when you pass HTML content into a component from outside — using ng-content. We cover this in detail in Chapter 5 of this phase.
ngAfterContentInit fires once after Angular has projected any external content into the component's template.
import { Component, AfterContentInit, ContentChild } from '@angular/core';
@Component({
selector: 'app-card',
imports: [],
template: `
<div class="card">
<ng-content></ng-content>
</div>
`
})
export class Card implements AfterContentInit {
ngAfterContentInit(): void {
console.log('Projected content is ready');
// Now you can safely interact with projected content
}
}
When to use it: When you need to do something after projected content has been inserted into your component.
ngAfterContentChecked
This fires after Angular checks the projected content for changes. It fires once after ngAfterContentInit and then again after every subsequent change detection cycle.
import { Component, AfterContentChecked } from '@angular/core';
@Component({ selector: 'app-card', imports: [], template: `<ng-content></ng-content>` })
export class Card implements AfterContentChecked {
ngAfterContentChecked(): void {
console.log('Projected content was checked for changes');
}
}
When to use it: Rarely. Only when you specifically need to react after Angular has verified that projected content has not changed. Most apps will never need this.
ngAfterViewInit
This fires once after Angular has fully rendered the component's own template — meaning all child components in the template are fully initialized and accessible.
This is important because before this hook fires, you cannot safely access child components using @ViewChild. After this hook fires, everything is ready.
import { Component, AfterViewInit, ViewChild, ElementRef } from '@angular/core';
@Component({
selector: 'app-dashboard',
imports: [],
template: `
<canvas #myChart></canvas>
`
})
export class Dashboard implements AfterViewInit {
@ViewChild('myChart') chartCanvas!: ElementRef;
ngAfterViewInit(): void {
// The template is fully rendered here
// You can now safely access @ViewChild references
console.log('Canvas element:', this.chartCanvas.nativeElement);
// Safe to initialize a chart library here
}
}
When to use it: When you need to work with @ViewChild references, or initialize a third-party library (like a chart library or a map) that needs a DOM element to attach to.
ngAfterViewChecked
This fires after Angular has checked the component's view (its own template and all child components) for changes. It fires once after ngAfterViewInit and then again after every change detection cycle.
import { Component, AfterViewChecked } from '@angular/core';
@Component({ selector: 'app-panel', imports: [], template: `<p>Panel</p>` })
export class Panel implements AfterViewChecked {
ngAfterViewChecked(): void {
// Fires very frequently — use with caution
console.log('View was checked');
}
}
When to use it: Very rarely. Only when you need to react after Angular has verified the view has not changed. Be careful with this one — because it fires so often, putting heavy code here will slow down your app.
ngOnDestroy
This fires just before Angular removes the component from the screen and destroys it. This is your chance to clean up anything the component was doing.
What kind of cleanup? Unsubscribing from data streams, clearing timers, removing event listeners. If you do not clean up these things, they continue running in the background even after the component is gone — this is called a memory leak. ngOnDestroy is how you prevent it.
import { Component, OnInit, OnDestroy } from '@angular/core';
@Component({
selector: 'app-timer',
imports: [],
template: `<p>Time: {{ seconds }} seconds</p>`
})
export class Timer implements OnInit, OnDestroy {
seconds: number = 0;
private timerInterval: any;
ngOnInit(): void {
// Start a timer when the component appears
this.timerInterval = setInterval(() => {
this.seconds++;
}, 1000);
}
ngOnDestroy(): void {
// Clean up the timer when the component is removed
// Without this, the timer would keep running forever
clearInterval(this.timerInterval);
console.log('Timer cleaned up!');
}
}
When to use it: Always use this to clean up any ongoing work — timers, subscriptions, event listeners. If your ngOnInit starts something, your ngOnDestroy should stop it.
2.3 — The Lifecycle Order
Here is the exact order in which these hooks fire:
Component Created
↓
ngOnChanges ← fires first (only if @Input() exists)
↓
ngOnInit ← fires once after initialization
↓
ngDoCheck ← fires on every change detection cycle
↓
ngAfterContentInit ← fires once after content projection
↓
ngAfterContentChecked ← fires after projected content is checked
↓
ngAfterViewInit ← fires once after view is rendered
↓
ngAfterViewChecked ← fires after view is checked
↓
(repeating cycle of DoCheck, ContentChecked, ViewChecked
on every subsequent change detection run)
↓
ngOnDestroy ← fires just before component is removed
You do not need to memorize all 8. In your day-to-day Angular work, you will use three the most: ngOnInit, ngOnDestroy, and ngOnChanges. The others are there for specific advanced scenarios.
2.4 — A Practical Lifecycle Example
Let's build a component that demonstrates the three most used hooks working together. This component will show a countdown timer that starts when it appears, reacts when its starting value changes, and cleans up when it disappears.
src/app/countdown/countdown.ts:
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-countdown',
imports: [],
templateUrl: './countdown.html',
styleUrl: './countdown.css'
})
export class Countdown implements OnInit, OnChanges, OnDestroy {
@Input() startFrom: number = 10;
currentCount: number = 0;
message: string = '';
private interval: any;
ngOnChanges(changes: SimpleChanges): void {
// Fires when startFrom input value changes
if (changes['startFrom']) {
console.log('startFrom changed to:', this.startFrom);
this.currentCount = this.startFrom;
}
}
ngOnInit(): void {
// Fires once when component is ready
console.log('Countdown component initialized');
this.currentCount = this.startFrom;
this.startCountdown();
}
startCountdown(): void {
this.interval = setInterval(() => {
if (this.currentCount > 0) {
this.currentCount--;
} else {
this.message = 'Time is up!';
clearInterval(this.interval);
}
}, 1000);
}
ngOnDestroy(): void {
// Fires just before component is removed
// Cleans up the interval to prevent memory leaks
if (this.interval) {
clearInterval(this.interval);
}
console.log('Countdown component destroyed, interval cleared');
}
}
src/app/countdown/countdown.html:
<div class="countdown">
@if (message) {
<p class="message">{{ message }}</p>
} @else {
<p class="count">{{ currentCount }}</p>
}
</div>
src/app/countdown/countdown.css:
.countdown {
text-align: center;
padding: 24px;
}
.count {
font-size: 72px;
font-weight: 700;
color: #64ffda;
}
.message {
font-size: 24px;
color: #e94560;
}
Notice @if in the template — this is Angular's built-in conditional syntax. If message has a value, show the message. Otherwise, show the count. We cover @if deeply in Phase 4.
Chapter 3 — @Input() — Passing Data from Parent to Child
3.1 — Why @Input() Exists
Imagine you are building a product listing page. You have a ProductCard component that displays one product's name, price, and image. Your page has 20 products, so you need to show 20 product cards.
The naive way would be to create 20 different components, one for each product. That is obviously terrible. The right way is to create ONE ProductCard component and pass different product data to it each time you use it.
This is exactly what @Input() enables. It lets a parent component pass data down to a child component. The child component declares what data it can receive using @Input(), and the parent provides that data when it uses the child in its template.
Think of it like a function with parameters. When you call a function, you pass arguments. When you use a component, you pass @Input() values.
3.2 — Creating an @Input() Property
In the child component, you import Input from Angular and use it as a decorator on a property:
src/app/product-card/product-card.ts:
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-product-card',
imports: [],
templateUrl: './product-card.html',
styleUrl: './product-card.css'
})
export class ProductCard {
@Input() productName: string = '';
@Input() price: number = 0;
@Input() category: string = '';
@Input() inStock: boolean = true;
}
The @Input() decorator on a property tells Angular: "this property's value should come from outside — from the parent component that uses me."
The default values after = are used when no value is passed in. Always provide sensible defaults.
3.3 — Passing Data from the Parent
In the parent component, you use the child's selector as a tag and pass values using property binding — square brackets around the property name:
src/app/product-list/product-list.ts:
import { Component } from '@angular/core';
import { ProductCard } from '../product-card/product-card';
@Component({
selector: 'app-product-list',
imports: [ProductCard],
templateUrl: './product-list.html',
styleUrl: './product-list.css'
})
export class ProductList {
products = [
{ name: 'Laptop Pro', price: 75000, category: 'Electronics', inStock: true },
{ name: 'Wireless Mouse', price: 1500, category: 'Accessories', inStock: true },
{ name: 'Desk Chair', price: 12000, category: 'Furniture', inStock: false }
];
}
src/app/product-list/product-list.html:
<div class="product-list">
@for (product of products; track product.name) {
<app-product-card
[productName]="product.name"
[price]="product.price"
[category]="product.category"
[inStock]="product.inStock">
</app-product-card>
}
</div>
The square brackets [productName] tell Angular: "evaluate this as a TypeScript expression, not a plain string." So [productName]="product.name" passes the actual value of product.name into the child's productName input.
Without square brackets: productName="product.name" — this passes the literal string "product.name". Always use square brackets when passing dynamic values.
Now let's build out the product card template:
src/app/product-card/product-card.html:
<div class="card">
<div class="card-body">
<span class="category">{{ category }}</span>
<h3 class="name">{{ productName }}</h3>
<p class="price">₹{{ price.toLocaleString() }}</p>
@if (inStock) {
<span class="badge in-stock">In Stock</span>
} @else {
<span class="badge out-of-stock">Out of Stock</span>
}
<button [disabled]="!inStock">Add to Cart</button>
</div>
</div>
src/app/product-card/product-card.css:
.card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
overflow: hidden;
transition: transform 0.2s;
}
.card:hover {
transform: translateY(-4px);
}
.card-body {
padding: 20px;
}
.category {
font-size: 12px;
color: #888;
text-transform: uppercase;
letter-spacing: 1px;
}
.name {
font-size: 18px;
font-weight: 600;
color: #1a1a2e;
margin: 8px 0;
}
.price {
font-size: 22px;
font-weight: 700;
color: #0070f3;
margin-bottom: 12px;
}
.badge {
display: inline-block;
padding: 4px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
margin-bottom: 16px;
}
.in-stock {
background: #d4edda;
color: #155724;
}
.out-of-stock {
background: #f8d7da;
color: #721c24;
}
button {
width: 100%;
padding: 10px;
background: #0070f3;
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
Now when the parent renders three product cards, each one displays different data — all from the same single component. The ProductCard component has no idea where its data comes from. It just knows it receives productName, price, category, and inStock through @Input() and displays them.
3.4 — Required Inputs
If a property must always be provided and there is no sensible default, you can mark it as required. Angular will throw an error at compile time if you forget to pass it:
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-user-avatar',
imports: [],
template: `<img [src]="imageUrl" [alt]="userName">`
})
export class UserAvatar {
@Input({ required: true }) userName!: string;
@Input({ required: true }) imageUrl!: string;
}
The ! after the property name is a TypeScript non-null assertion. It tells TypeScript "I know this might look uninitialized but it will always have a value at runtime." When you mark an input as required: true, it is safe to use ! because Angular guarantees the value will be provided.
3.5 — Input with an Alias
Sometimes you want the property name used in TypeScript to be different from the name used in the template. You can do this with an alias:
@Input({ alias: 'color' }) buttonColor: string = 'blue';
Now in the parent template you use [color]="'red'", but inside the child component TypeScript code you access it as this.buttonColor. This is useful when the external API name and the internal implementation name should be different.
Chapter 4 — @Output() and EventEmitter — Child to Parent Communication
4.1 — Why @Output() Exists
@Input() lets data flow from parent to child. But what about the other direction?
Imagine your ProductCard component has an "Add to Cart" button. When the user clicks it, the parent component needs to know about it — it needs to update the cart count, show a notification, maybe send a request to the server. The child component (ProductCard) cannot do all of this itself. It needs to tell the parent "the button was clicked."
This is what @Output() is for. A child component can emit events that the parent listens to. It is like a child calling out to a parent — "something happened here, you should know about it."
4.2 — Creating an @Output()
In the child component, you import Output and EventEmitter from Angular:
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-product-card',
imports: [],
templateUrl: './product-card.html',
styleUrl: './product-card.css'
})
export class ProductCard {
@Input() productName: string = '';
@Input() price: number = 0;
@Input() inStock: boolean = true;
@Output() addedToCart = new EventEmitter<string>();
// The <string> means this event will carry a string value when it fires
onAddToCart(): void {
// Emit the event, passing the product name as the payload
this.addedToCart.emit(this.productName);
}
}
EventEmitter<string> is a generic — the type inside <> is the type of data this event will carry when it fires. You can pass any type: a string, a number, an object, or nothing at all with EventEmitter<void>.
Update the template to call onAddToCart() when the button is clicked:
product-card.html:
<div class="card">
<h3>{{ productName }}</h3>
<p>₹{{ price }}</p>
<button (click)="onAddToCart()" [disabled]="!inStock">
Add to Cart
</button>
</div>
(click)="onAddToCart()" is Angular's event binding syntax. The parentheses () around click mean "listen to this DOM event." When the button is clicked, call onAddToCart(). We cover event binding deeply in Phase 4.
4.3 — Listening to the Event in the Parent
In the parent component, you listen to the child's event using event binding — but instead of a DOM event like click, you use the @Output() property name:
product-list.ts:
import { Component } from '@angular/core';
import { ProductCard } from '../product-card/product-card';
@Component({
selector: 'app-product-list',
imports: [ProductCard],
templateUrl: './product-list.html',
styleUrl: './product-list.css'
})
export class ProductList {
products = [
{ name: 'Laptop Pro', price: 75000, inStock: true },
{ name: 'Wireless Mouse', price: 1500, inStock: true },
{ name: 'Desk Chair', price: 12000, inStock: false }
];
cartItems: string[] = [];
notification: string = '';
onProductAddedToCart(productName: string): void {
this.cartItems.push(productName);
this.notification = `${productName} added to cart! Cart has ${this.cartItems.length} item(s).`;
console.log('Cart:', this.cartItems);
}
}
product-list.html:
<div>
@if (notification) {
<div class="notification">{{ notification }}</div>
}
<div class="product-grid">
@for (product of products; track product.name) {
<app-product-card
[productName]="product.name"
[price]="product.price"
[inStock]="product.inStock"
(addedToCart)="onProductAddedToCart($event)">
</app-product-card>
}
</div>
</div>
(addedToCart)="onProductAddedToCart($event)" — the parentheses around addedToCart mean "listen to this event." $event is a special Angular variable that contains whatever data was emitted — in this case, the product name string that was passed to .emit().
When the user clicks "Add to Cart" on a product card:
- The button triggers
(click)in the child template onAddToCart()runs in the child TypeScriptthis.addedToCart.emit(this.productName)fires the event with the product name- Angular passes this to the parent's
(addedToCart)listener onProductAddedToCart($event)runs in the parent TypeScript- The cart updates and the notification appears
This is the complete parent-child communication pattern in Angular.
4.4 — Emitting Complex Objects
You are not limited to emitting simple strings. You can emit any data type — including objects:
interface CartItem {
productName: string;
price: number;
quantity: number;
}
@Output() addedToCart = new EventEmitter<CartItem>();
onAddToCart(): void {
this.addedToCart.emit({
productName: this.productName,
price: this.price,
quantity: 1
});
}
And in the parent:
onProductAddedToCart(item: CartItem): void {
console.log('Added to cart:', item.productName, 'at price', item.price);
}
Chapter 5 — @ViewChild — Accessing Child Components from the Parent
5.1 — What is @ViewChild?
@Input() and @Output() are how components communicate during the normal data flow — parent passes data down, child emits events up. But sometimes you need to go further. Sometimes the parent needs to directly call a method on a child component, or access a specific HTML element in the template.
@ViewChild gives you a direct reference to a child component or a DOM element from within the parent TypeScript code.
5.2 — Accessing a Child Component with @ViewChild
Let's say you have a VideoPlayer component with a play() and pause() method. The parent has a Play button and a Pause button that should control the video player.
src/app/video-player/video-player.ts:
import { Component } from '@angular/core';
@Component({
selector: 'app-video-player',
imports: [],
template: `
<div class="player">
<div class="screen">{{ status }}</div>
</div>
`,
styles: [`
.player { background: #000; padding: 40px; border-radius: 8px; }
.screen { color: #64ffda; text-align: center; font-size: 18px; }
`]
})
export class VideoPlayer {
status: string = 'Stopped';
isPlaying: boolean = false;
play(): void {
this.isPlaying = true;
this.status = '▶ Playing...';
console.log('Video started playing');
}
pause(): void {
this.isPlaying = false;
this.status = '⏸ Paused';
console.log('Video paused');
}
stop(): void {
this.isPlaying = false;
this.status = '⏹ Stopped';
}
}
Now the parent uses @ViewChild to get a direct reference to this component:
src/app/media-controls/media-controls.ts:
import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { VideoPlayer } from '../video-player/video-player';
@Component({
selector: 'app-media-controls',
imports: [VideoPlayer],
templateUrl: './media-controls.html',
styleUrl: './media-controls.css'
})
export class MediaControls implements AfterViewInit {
@ViewChild(VideoPlayer) videoPlayer!: VideoPlayer;
// This gives us a direct reference to the VideoPlayer component instance
ngAfterViewInit(): void {
// @ViewChild is only available from ngAfterViewInit onwards
// Before this hook, the view is not rendered yet, so the reference would be undefined
console.log('VideoPlayer component reference:', this.videoPlayer);
}
onPlayClick(): void {
this.videoPlayer.play(); // directly calling the child's method
}
onPauseClick(): void {
this.videoPlayer.pause();
}
onStopClick(): void {
this.videoPlayer.stop();
}
}
src/app/media-controls/media-controls.html:
<div class="media-controls">
<app-video-player></app-video-player>
<div class="controls">
<button (click)="onPlayClick()">▶ Play</button>
<button (click)="onPauseClick()">⏸ Pause</button>
<button (click)="onStopClick()">⏹ Stop</button>
</div>
</div>
@ViewChild(VideoPlayer) tells Angular: "find the first instance of VideoPlayer in my template and give me a reference to it." Now this.videoPlayer is the actual VideoPlayer instance, and you can call any of its public methods or access its properties directly.
The ! after videoPlayer!: VideoPlayer is the non-null assertion again — you are telling TypeScript this will never be null at the point you use it (because you only use it after ngAfterViewInit).
5.3 — Accessing a DOM Element with @ViewChild
You can also use @ViewChild to access a raw HTML element using a template reference variable. A template reference variable is a name you give to an HTML element using #:
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-auto-focus',
imports: [],
template: `
<input #searchInput type="text" placeholder="Search...">
<button (click)="focusInput()">Focus Input</button>
`
})
export class AutoFocus implements AfterViewInit {
@ViewChild('searchInput') searchInput!: ElementRef;
// 'searchInput' matches the #searchInput in the template
ngAfterViewInit(): void {
// Automatically focus the input when component loads
this.searchInput.nativeElement.focus();
}
focusInput(): void {
this.searchInput.nativeElement.focus();
}
}
#searchInput on the HTML element creates a template reference variable named searchInput. @ViewChild('searchInput') with the string name retrieves that element. nativeElement gives you the actual underlying DOM element, so you can call any native DOM methods on it like .focus(), .click(), .scrollIntoView() etc.
Chapter 6 — Content Projection with ng-content
6.1 — What is Content Projection?
Imagine you want to build a reusable Card component — a styled container with a shadow, rounded corners, and padding. You want to use this card in multiple places, but each time with completely different content inside it. One card might have a user profile. Another might have a product listing. Another might have a chart.
If you use @Input(), you would have to pass all that content as strings or data — very limiting and messy.
Content projection is the solution. It lets a parent component pass HTML content directly into a child component. The child component decides where that content appears using <ng-content>.
Think of <ng-content> as a slot — a hole in the component's template where whatever you put between the component's opening and closing tags will appear.
6.2 — Basic Content Projection
src/app/card/card.ts:
import { Component } from '@angular/core';
@Component({
selector: 'app-card',
imports: [],
templateUrl: './card.html',
styleUrl: './card.css'
})
export class Card { }
src/app/card/card.html:
<div class="card">
<ng-content></ng-content>
</div>
src/app/card/card.css:
.card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
padding: 24px;
margin: 16px;
}
Now in any parent component, you can put any content between <app-card> and </app-card>:
import { Component } from '@angular/core';
import { Card } from '../card/card';
@Component({
selector: 'app-home',
imports: [Card],
template: `
<app-card>
<h2>User Profile</h2>
<p>Name: Rahul Sharma</p>
<p>Role: Developer</p>
</app-card>
<app-card>
<h2>Latest Stats</h2>
<p>Projects: 20</p>
<p>Clients: 15</p>
</app-card>
<app-card>
<img src="/profile.jpg" alt="Photo">
<p>This card has an image!</p>
</app-card>
`
})
export class Home { }
Each <app-card> is the same component, but each one displays completely different content. The <ng-content> in the card's template acts as a placeholder for whatever you put between the tags.
6.3 — Multiple Content Slots with select
Sometimes you want more than one slot in your component. For example, a card with a separate header, body, and footer. You can use the select attribute on <ng-content> to create named slots.
src/app/panel/panel.ts:
import { Component } from '@angular/core';
@Component({
selector: 'app-panel',
imports: [],
templateUrl: './panel.html',
styleUrl: './panel.css'
})
export class Panel { }
src/app/panel/panel.html:
<div class="panel">
<div class="panel-header">
<ng-content select="[slot=header]"></ng-content>
</div>
<div class="panel-body">
<ng-content select="[slot=body]"></ng-content>
</div>
<div class="panel-footer">
<ng-content select="[slot=footer]"></ng-content>
</div>
</div>
src/app/panel/panel.css:
.panel {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
}
.panel-header {
background: #0a192f;
color: white;
padding: 16px 20px;
font-weight: 600;
}
.panel-body {
padding: 20px;
}
.panel-footer {
background: #f5f5f5;
padding: 12px 20px;
border-top: 1px solid #e0e0e0;
}
Now in the parent, you assign content to specific slots using the slot attribute:
<app-panel>
<div slot="header">User Settings</div>
<div slot="body">
<p>Email: rahul@example.com</p>
<p>Theme: Dark</p>
<p>Notifications: On</p>
</div>
<div slot="footer">
<button>Save Changes</button>
</div>
</app-panel>
select="[slot=header]" means: "project content that has the attribute slot="header"." The content goes into exactly the right slot.
Chapter 7 — ViewEncapsulation — How Component Styles Work
7.1 — The Problem with CSS
In regular HTML, CSS is global. If you write .title { color: red } in one stylesheet, every element with class title anywhere on the page turns red. This causes endless conflicts in large applications.
Angular solves this with ViewEncapsulation. By default, Angular scopes the CSS of each component so it only affects that component's own template. You can write .title { color: red } in navbar.css and .title { color: blue } in hero.css and they will not conflict at all.
7.2 — How Angular Does It
Angular achieves style scoping by automatically adding a unique attribute to every element in your component's template. For example, it might add _ngcontent-abc-c123 to every element in the Navbar component. Then it rewrites your CSS selectors to only match elements with that specific attribute.
So .title { color: red } in navbar.css becomes something like .title[_ngcontent-abc-c123] { color: red } internally. Other components have different attributes, so their elements are not affected.
You do not need to do anything for this to work — it is the default behavior.
7.3 — The Three ViewEncapsulation Modes
You can control this behavior using the encapsulation option in @Component:
import { Component, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-example',
imports: [],
template: `<p class="title">Hello</p>`,
styles: [`.title { color: red }`],
encapsulation: ViewEncapsulation.Emulated // this is the default
})
export class Example { }
ViewEncapsulation.Emulated — the default. Angular emulates shadow DOM by adding unique attributes. Styles are scoped to the component. This is what you will use 99% of the time.
ViewEncapsulation.None — no scoping at all. The component's CSS becomes global and affects the entire application. Use this very carefully, only when you specifically want global styles from a component.
ViewEncapsulation.ShadowDom — uses the browser's real Shadow DOM for isolation. The strongest isolation possible. Styles are completely invisible to the outside world and vice versa. Has some browser compatibility considerations.
7.4 — The :host Selector
Inside a component's CSS file, :host refers to the component's own root element — the element matched by the selector.
/* navbar.css */
:host {
/* Styles the <app-navbar> element itself */
display: block;
width: 100%;
}
:host(.active) {
/* Styles <app-navbar class="active"> */
border-bottom: 2px solid #64ffda;
}
This is useful when you want to style the wrapper element of your component without needing to add an extra wrapper div inside the template.
Chapter 8 — Change Detection — How Angular Knows What to Update
8.1 — What is Change Detection?
When data in your component changes — a property gets a new value, an array gets a new item, a user types something — Angular needs to update the DOM to reflect that change.
But how does Angular know something changed? It cannot watch every variable in your entire application at all times. That would be incredibly slow.
Angular uses a system called change detection to figure out what changed and what needs to be re-rendered.
8.2 — The Default Change Detection Strategy
By default, Angular uses ChangeDetectionStrategy.Default. This strategy works like this:
Whenever anything happens in your application — a button click, an HTTP response, a timer, any browser event — Angular runs a change detection cycle. During this cycle, it goes through the entire component tree from top to bottom and checks every component. For each component, it compares the current values of all template bindings with the previous values. If anything is different, it updates that part of the DOM.
This approach is simple and it always works correctly. But on very large applications with hundreds of components, checking everything on every event can become slow.
8.3 — OnPush Change Detection Strategy
Angular gives you a more performant option called ChangeDetectionStrategy.OnPush.
With OnPush, Angular only runs change detection on a component when one of these things happens:
An @Input() property receives a new reference (not just a mutation of an existing object). An event was triggered from within the component or its children. An Observable subscribed with the async pipe emits a new value. You manually trigger it.
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-product-card',
imports: [],
templateUrl: './product-card.html',
styleUrl: './product-card.css',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductCard {
@Input() productName: string = '';
@Input() price: number = 0;
}
This means Angular skips this component during most change detection cycles, only checking it when something directly relevant to it changes. In large lists with hundreds of items, this can make a significant performance difference.
The key thing to understand with OnPush: if you mutate an object or array that is passed as input — like pushing a new item into an array that already exists — Angular will NOT detect the change. You need to replace the reference entirely.
// This will NOT trigger change detection with OnPush:
this.products.push(newProduct); // mutating the existing array
// This WILL trigger change detection with OnPush:
this.products = [...this.products, newProduct]; // creating a new array
8.4 — Manually Triggering Change Detection
Sometimes with OnPush you update something and need to tell Angular to check your component right now. You do this using ChangeDetectorRef:
import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-notification',
imports: [],
template: `<p>{{ message }}</p>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class Notification {
message: string = '';
constructor(private cdr: ChangeDetectorRef) {}
updateMessage(newMessage: string): void {
this.message = newMessage;
this.cdr.markForCheck(); // Tell Angular: please check this component
}
}
this.cdr.markForCheck() schedules the component and its ancestors for checking in the next change detection cycle. Use this when you have OnPush and you update something that Angular cannot automatically detect.
Chapter 9 — Putting It All Together — A Real Component System
Let's build a complete, real-world example that uses everything from this phase together. We will build a Student Dashboard with a list of student cards where each card can be selected and the parent component reacts.
Step 1 — Create the project and components
ng new student-dashboard --style=css
cd student-dashboard
ng g c student-card --skip-tests
ng g c student-list --skip-tests
Step 2 — The StudentCard Component
This component receives student data via @Input() and emits an event via @Output() when clicked.
src/app/student-card/student-card.ts:
import { Component, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
@Component({
selector: 'app-student-card',
imports: [],
templateUrl: './student-card.html',
styleUrl: './student-card.css'
})
export class StudentCard implements OnInit, OnDestroy {
@Input({ required: true }) studentName!: string;
@Input({ required: true }) grade!: string;
@Input({ required: true }) score!: number;
@Input() isSelected: boolean = false;
@Output() selected = new EventEmitter<string>();
status: string = '';
ngOnInit(): void {
this.status = this.score >= 60 ? 'Passing' : 'Needs Improvement';
console.log(`${this.studentName} card initialized`);
}
ngOnDestroy(): void {
console.log(`${this.studentName} card destroyed`);
}
onCardClick(): void {
this.selected.emit(this.studentName);
}
}
src/app/student-card/student-card.html:
<div class="card" [class.selected]="isSelected" (click)="onCardClick()">
<div class="avatar">{{ studentName.charAt(0) }}</div>
<div class="info">
<h3>{{ studentName }}</h3>
<p class="grade">Grade: {{ grade }}</p>
<p class="score">Score: {{ score }}/100</p>
<span class="status" [class.passing]="score >= 60" [class.failing]="score < 60">
{{ status }}
</span>
</div>
</div>
src/app/student-card/student-card.css:
.card {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
background: white;
border-radius: 12px;
border: 2px solid transparent;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
cursor: pointer;
transition: all 0.2s;
}
.card:hover {
border-color: #0070f3;
transform: translateY(-2px);
}
.card.selected {
border-color: #0070f3;
background: #f0f7ff;
}
.avatar {
width: 52px;
height: 52px;
border-radius: 50%;
background: #0070f3;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
font-weight: 700;
flex-shrink: 0;
}
.info h3 {
font-size: 16px;
font-weight: 600;
color: #1a1a2e;
margin-bottom: 4px;
}
.grade, .score {
font-size: 13px;
color: #666;
margin: 2px 0;
}
.status {
display: inline-block;
margin-top: 6px;
padding: 3px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.passing {
background: #d4edda;
color: #155724;
}
.failing {
background: #f8d7da;
color: #721c24;
}
Step 3 — The StudentList Component
This component holds the list of students, renders student cards, and listens for selection events.
src/app/student-list/student-list.ts:
import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { StudentCard } from '../student-card/student-card';
interface Student {
name: string;
grade: string;
score: number;
}
@Component({
selector: 'app-student-list',
imports: [StudentCard],
templateUrl: './student-list.html',
styleUrl: './student-list.css'
})
export class StudentList implements AfterViewInit {
students: Student[] = [
{ name: 'Rahul Sharma', grade: 'A', score: 92 },
{ name: 'Priya Patel', grade: 'B', score: 78 },
{ name: 'Amit Kumar', grade: 'C', score: 55 },
{ name: 'Sneha Gupta', grade: 'A', score: 95 },
{ name: 'Vikram Singh', grade: 'B', score: 71 },
{ name: 'Neha Joshi', grade: 'D', score: 48 }
];
selectedStudentName: string = '';
@ViewChild(StudentCard) firstCard!: StudentCard;
ngAfterViewInit(): void {
console.log('First student card component:', this.firstCard);
console.log('First student name:', this.firstCard.studentName);
}
onStudentSelected(studentName: string): void {
this.selectedStudentName = studentName;
}
getSelectedStudent(): Student | undefined {
return this.students.find(s => s.name === this.selectedStudentName);
}
}
src/app/student-list/student-list.html:
<div class="dashboard">
<h1>Student Dashboard</h1>
@if (selectedStudentName) {
<div class="selection-banner">
✓ Selected: <strong>{{ selectedStudentName }}</strong>
— Score: {{ getSelectedStudent()?.score }}/100
</div>
}
<div class="student-grid">
@for (student of students; track student.name) {
<app-student-card
[studentName]="student.name"
[grade]="student.grade"
[score]="student.score"
[isSelected]="selectedStudentName === student.name"
(selected)="onStudentSelected($event)">
</app-student-card>
}
</div>
</div>
src/app/student-list/student-list.css:
.dashboard {
max-width: 900px;
margin: 0 auto;
padding: 40px 24px;
}
h1 {
font-size: 32px;
color: #1a1a2e;
margin-bottom: 24px;
}
.selection-banner {
background: #f0f7ff;
border: 1px solid #0070f3;
border-radius: 8px;
padding: 12px 20px;
margin-bottom: 24px;
color: #0070f3;
font-size: 15px;
}
.student-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
Step 4 — Wire it in app.ts
src/app/app.ts:
import { Component } from '@angular/core';
import { StudentList } from './student-list/student-list';
@Component({
selector: 'app-root',
imports: [StudentList],
template: `<app-student-list></app-student-list>`,
styles: [`
:host {
display: block;
background: #f5f7fa;
min-height: 100vh;
}
`]
})
export class App { }
src/styles.css:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
Run ng serve -o. You will see a complete Student Dashboard where:
- Each student card is initialized with
ngOnInitwhich calculates the status - Clicking a card emits an event via
@Output()with the student's name - The parent
StudentListreceives the event and updatesselectedStudentName - The selected card gets highlighted because
[isSelected]="selectedStudentName === student.name"becomestrue - The selection banner appears via
@ifshowing the selected student's details @ViewChildgives the parent direct access to the firstStudentCardcomponent
Every concept from this phase is at work in this example.
Phase 3 — Complete Summary
This phase went deep into how components really work. Here is everything you covered:
Lifecycle hooks — Angular calls special methods at specific moments of a component's life. ngOnInit runs once when the component is ready — use it to load data. ngOnDestroy runs just before the component is removed — use it to clean up timers and subscriptions. ngOnChanges runs whenever an @Input() value changes. ngAfterViewInit runs once after the template is fully rendered — use it when you need @ViewChild references.
@Input() — Passes data from a parent component down to a child component. You use @Input() to declare which properties the child accepts. The parent passes values using square bracket binding [propertyName]="value". You can mark inputs as required and give them aliases.
@Output() and EventEmitter — Sends events from a child component up to a parent. The child declares an EventEmitter with @Output() and calls .emit() to fire the event with optional data. The parent listens using event binding (eventName)="handler($event)".
@ViewChild — Gives a parent component direct access to a child component instance or a DOM element. You use it after ngAfterViewInit because the view must be fully rendered first. With a component type you get the component instance and can call its methods. With a template reference variable #name you get an ElementRef to access the raw DOM element.
Content projection with ng-content — Lets you build wrapper components that accept HTML content from outside. <ng-content> is the slot where projected content appears. Multiple named slots are created using select="[slot=name]" for finer control over where different pieces of content go.
ViewEncapsulation — Angular scopes component CSS by default so styles never leak out. Emulated is the default and works for almost everything. None makes styles global. ShadowDom uses real browser shadow DOM for the strongest isolation. :host selects the component's own root element in CSS.
Change detection — Angular checks the component tree for changes after every browser event. Default strategy checks everything on every cycle — simple and always correct. OnPush strategy only checks when inputs change or events fire from within — more performant for large lists and complex UIs. With OnPush you must replace object and array references rather than mutating them. ChangeDetectorRef.markForCheck() manually triggers a check when needed.
What's Next — Phase 4
In Phase 4 we cover Data Binding and Directives — the complete syntax for making your templates dynamic:
The four types of data binding in complete depth — interpolation, property binding, event binding, and two-way binding. You will understand exactly when to use each one.
Angular's built-in control flow — @if for conditionals, @for for loops, @switch for multiple conditions. This is how you make your templates respond to data.
Built-in directives — NgClass, NgStyle, and how to apply dynamic styles.
Building your own custom directives from scratch.
The ng-template and ng-container elements and what they are for.