Phase 4 — Data Binding & Directives

Chapter 1 — What is Data Binding?


1.1 — The Problem Data Binding Solves

When you build a web application, you have two worlds that need to talk to each other constantly.

The first world is your TypeScript class — this is where all your data and logic lives. User names, product lists, whether a button is loading, what the current count is. All of that is in TypeScript.

The second world is your HTML template — this is what the user actually sees and interacts with on screen. Buttons, inputs, lists, text.

The problem is: how do these two worlds stay in sync?

When the user types their name into an input field, that value needs to get into your TypeScript so you can process it. When your TypeScript fetches a list of products from an API, those products need to show up on screen. When a button is clicked, TypeScript needs to know about it. When TypeScript marks something as "loading", the button on screen should show a spinner.

In plain JavaScript, you would do all of this manually. You would grab DOM elements with document.getElementById, read their values, update their text, add and remove classes. This gets messy and hard to manage very quickly.

Angular solves this with data binding — a clean, declarative way to connect your TypeScript class and your HTML template. Instead of manually writing code to push data back and forth, you just declare the connection in your template and Angular handles the rest automatically.


1.2 — The Four Types of Data Binding

Angular has four types of data binding. Each one handles a different direction of data flow:

TypeScript Class                    HTML Template
─────────────                       ─────────────

      ──── Interpolation {{ }} ────►
      ──── Property Binding [  ] ──►
      ◄─── Event Binding    (  ) ───
      ◄──► Two-Way Binding [(  )] ──►

Interpolation — TypeScript → Template. Displays a value from TypeScript in the HTML.

Property Binding — TypeScript → Template. Sets an HTML element's property to a value from TypeScript.

Event Binding — Template → TypeScript. Listens for an event in the HTML and calls a TypeScript method when it fires.

Two-Way Binding — Both directions at once. The HTML input and the TypeScript property stay perfectly in sync with each other.

Let's cover each one in complete depth.


Chapter 2 — Interpolation


2.1 — What is Interpolation?

Interpolation is the simplest form of data binding. You use double curly braces {{ }} to display a value from your TypeScript class directly in the HTML template.

Angular reads whatever is inside the curly braces, evaluates it, converts it to a string, and inserts it into the HTML at that exact spot.

import { Component } from '@angular/core';

@Component({
  selector: 'app-profile',
  imports: [],
  templateUrl: './profile.html',
  styleUrl: './profile.css'
})
export class Profile {
  userName: string = 'Rahul Sharma';
  age: number = 25;
  city: string = 'Mumbai';
  isVerified: boolean = true;
}
<div class="profile">
  <h1>{{ userName }}</h1>
  <p>Age: {{ age }}</p>
  <p>City: {{ city }}</p>
  <p>Verified: {{ isVerified }}</p>
</div>

Angular reads userName from the TypeScript class and puts "Rahul Sharma" in the <h1>. Reads age and puts "25" in the paragraph. And so on.


2.2 — Expressions Inside Interpolation

The {{ }} does not just display variables. It evaluates any valid TypeScript expression:

<!-- Math -->
<p>Next year I will be {{ age + 1 }} years old</p>

<!-- String methods -->
<p>{{ userName.toUpperCase() }}</p>
<p>{{ city.toLowerCase() }}</p>

<!-- Ternary operator -->
<p>Status: {{ isVerified ? 'Verified ✓' : 'Not Verified' }}</p>

<!-- Array length -->
<p>You have {{ notifications.length }} notifications</p>

<!-- Calling a method -->
<p>{{ getFullGreeting() }}</p>
export class Profile {
  userName: string = 'Rahul Sharma';
  age: number = 25;
  city: string = 'Mumbai';
  isVerified: boolean = true;
  notifications: string[] = ['Message 1', 'Message 2', 'Message 3'];

  getFullGreeting(): string {
    return `Welcome back, ${this.userName}! You are in ${this.city}.`;
  }
}

2.3 — What You Cannot Do in Interpolation

Interpolation is for displaying values only. It is not for executing complex logic or statements:

<!-- These will NOT work: -->

{{ let x = 5 }}            ← variable declarations not allowed
{{ if (age > 18) { } }}    ← if statements not allowed
{{ userName = 'New Name' }} ← assignments not allowed

If you need complex logic, put it in a method in your TypeScript class and call that method from the template.


Chapter 3 — Property Binding


3.1 — What is Property Binding?

Property binding lets you set an HTML element's property to a dynamic value from your TypeScript class. You use square brackets [ ] around the property name.

The key thing to understand is the difference between an HTML attribute and a DOM property:

An attribute is what you write in the HTML source code. Attributes are static text that the browser reads once when it parses the HTML. Example: <input type="text" value="hello">type and value are attributes.

A property is what exists on the actual DOM element object in JavaScript after the browser has parsed the HTML. Properties are dynamic and can change. When you do element.value = 'new value' in JavaScript, you are setting a property, not an attribute.

Property binding binds to DOM properties, not HTML attributes. In most cases they have the same name and this distinction does not matter. But in a few cases it matters a lot — we will see examples of this.


3.2 — Basic Property Binding

import { Component } from '@angular/core';

@Component({
  selector: 'app-button-demo',
  imports: [],
  templateUrl: './button-demo.html',
  styleUrl: './button-demo.css'
})
export class ButtonDemo {
  isDisabled: boolean = true;
  isLoading: boolean = false;
  imageUrl: string = '/images/profile.jpg';
  imageAlt: string = 'Profile photo';
  inputPlaceholder: string = 'Enter your name...';
  linkUrl: string = 'https://angular.dev';
}
<!-- Binding to the disabled property -->
<button [disabled]="isDisabled">Submit</button>

<!-- Binding to src and alt properties of an image -->
<img [src]="imageUrl" [alt]="imageAlt">

<!-- Binding to placeholder property of an input -->
<input [placeholder]="inputPlaceholder" type="text">

<!-- Binding to href property of a link -->
<a [href]="linkUrl">Visit Angular Docs</a>

With [disabled]="isDisabled", Angular reads the isDisabled value from TypeScript (which is true) and sets the button's disabled property to true. The button becomes disabled. If you later change isDisabled to false, Angular automatically updates the button and it becomes enabled.


3.3 — Property Binding vs String Interpolation vs Plain Attribute

This is a common point of confusion. There are three ways that look similar but behave differently:

<!-- 1. Plain HTML attribute — static string, never changes -->
<button disabled="true">Button 1</button>

<!-- 2. Interpolation — works for simple cases but is limited -->
<img src="{{ imageUrl }}">

<!-- 3. Property binding — the Angular way, evaluates TypeScript -->
<img [src]="imageUrl">

For setting properties dynamically, always use property binding with [ ]. Interpolation in attributes technically works for simple cases but property binding is the correct approach and gives you access to non-string values.

There is one important case where this distinction really matters — the disabled attribute:

<!-- This will ALWAYS disable the button — even when the value is false! -->
<!-- Because the attribute "disabled" exists, the button IS disabled -->
<button disabled="{{ isDisabled }}">Wrong way</button>

<!-- This correctly enables/disables based on the actual boolean value -->
<button [disabled]="isDisabled">Correct way</button>

HTML attributes are strings. When you write disabled="false", the attribute exists with the value "false" — but because the attribute exists, the button is disabled. The string "false" is truthy. This is a classic bug in Angular beginners' code. Property binding avoids this by working with actual JavaScript values.


3.4 — Binding to Class and Style

You can dynamically apply CSS classes and inline styles using property binding:

export class StyleDemo {
  isActive: boolean = true;
  hasError: boolean = false;
  textColor: string = '#0070f3';
  fontSize: number = 18;
}
<!-- Bind a single class conditionally -->
<div [class.active]="isActive">This div gets class 'active' when isActive is true</div>
<div [class.error]="hasError">This div gets class 'error' when hasError is true</div>

<!-- Bind inline styles dynamically -->
<p [style.color]="textColor">This text color comes from TypeScript</p>
<p [style.font-size.px]="fontSize">Font size in pixels from TypeScript</p>
<p [style.font-weight]="isActive ? 'bold' : 'normal'">Bold when active</p>

The [class.active]="expression" syntax adds the class active when the expression is true and removes it when false. This is clean and direct.

The [style.color]="value" syntax sets that specific style property. When the value has a unit, you can include the unit in the binding name itself: [style.font-size.px]="18" sets font-size: 18px.


3.5 — Binding to Non-Standard HTML Attributes with attr.

Some HTML attributes do not have a corresponding DOM property. For these, you need to use the attr. prefix:

<!-- ARIA attributes for accessibility -->
<button [attr.aria-label]="buttonLabel">Click me</button>

<!-- colspan on a table cell -->
<td [attr.colspan]="columnSpan">Cell content</td>

<!-- Custom data attributes -->
<div [attr.data-product-id]="product.id">Product</div>

When you try to use regular property binding with an attribute that has no DOM property equivalent, Angular will throw an error. The attr. prefix tells Angular: "bind to the HTML attribute directly, not a DOM property."


Chapter 4 — Event Binding


4.1 — What is Event Binding?

Event binding lets your HTML template tell TypeScript when something happens — a button is clicked, a key is pressed, a form is submitted, the mouse moves over an element. You use parentheses ( ) around the event name.

When that event fires, Angular calls the method or expression you specified in the template.

import { Component } from '@angular/core';

@Component({
  selector: 'app-counter',
  imports: [],
  templateUrl: './counter.html',
  styleUrl: './counter.css'
})
export class Counter {
  count: number = 0;

  increment(): void {
    this.count++;
  }

  decrement(): void {
    this.count--;
  }

  reset(): void {
    this.count = 0;
  }
}
<div class="counter">
  <button (click)="decrement()">-</button>
  <span>{{ count }}</span>
  <button (click)="increment()">+</button>
  <button (click)="reset()">Reset</button>
</div>

Every time the + button is clicked, Angular calls increment() in the TypeScript class. The count property increases. Angular detects the change and updates {{ count }} on screen. All of this happens automatically.


4.2 — The $event Object

When an event fires, the browser creates an event object containing information about what happened — where the mouse was, which key was pressed, what value was in the input. In Angular, you access this using the special $event variable:

import { Component } from '@angular/core';

@Component({
  selector: 'app-event-demo',
  imports: [],
  templateUrl: './event-demo.html',
  styleUrl: './event-demo.css'
})
export class EventDemo {

  mousePosition = { x: 0, y: 0 };
  typedText: string = '';
  lastKey: string = '';

  onMouseMove(event: MouseEvent): void {
    this.mousePosition.x = event.clientX;
    this.mousePosition.y = event.clientY;
  }

  onInputChange(event: Event): void {
    const inputElement = event.target as HTMLInputElement;
    this.typedText = inputElement.value;
  }

  onKeyPress(event: KeyboardEvent): void {
    this.lastKey = event.key;
  }
}
<div class="demo" (mousemove)="onMouseMove($event)">
  <p>Mouse position: {{ mousePosition.x }}, {{ mousePosition.y }}</p>
</div>

<input
  type="text"
  (input)="onInputChange($event)"
  placeholder="Type something...">
<p>You typed: {{ typedText }}</p>

<input
  type="text"
  (keydown)="onKeyPress($event)"
  placeholder="Press any key...">
<p>Last key pressed: {{ lastKey }}</p>

$event is the native browser event object. The type depends on the event — MouseEvent for mouse events, KeyboardEvent for keyboard events, Event for general events. TypeScript knows what properties are available on each event type, so you get autocomplete.


4.3 — Common Events You Will Use

Here are all the events you will use regularly:

<!-- Mouse events -->
<button (click)="onClick()">Click me</button>
<div (dblclick)="onDoubleClick()">Double click me</div>
<div (mouseenter)="onHoverStart()">Hover start</div>
<div (mouseleave)="onHoverEnd()">Hover end</div>

<!-- Keyboard events -->
<input (keydown)="onKeyDown($event)">    ← fires when key is pressed down
<input (keyup)="onKeyUp($event)">        ← fires when key is released
<input (keyup.enter)="onEnterPressed()"> ← only fires when Enter is released

<!-- Form / input events -->
<input (input)="onTyping($event)">       ← fires on every character typed
<input (change)="onChange($event)">      ← fires when input loses focus with a changed value
<input (focus)="onFocused()">            ← fires when input gains focus
<input (blur)="onBlurred()">             ← fires when input loses focus
<form (submit)="onSubmit($event)">       ← fires when form is submitted

The (keyup.enter) syntax is an Angular-specific shortcut. Instead of checking if (event.key === 'Enter') inside your method, you can specify the key directly in the template.


4.4 — Calling Methods with Arguments

You can pass arguments directly to methods from the template:

export class ItemList {
  items = ['Angular', 'TypeScript', 'RxJS', 'Node.js'];

  deleteItem(itemName: string): void {
    this.items = this.items.filter(item => item !== itemName);
  }

  selectItem(item: string, index: number): void {
    console.log(`Selected ${item} at index ${index}`);
  }
}
@for (item of items; track item; let i = $index) {
  <div class="item">
    <span>{{ item }}</span>
    <button (click)="deleteItem(item)">Delete</button>
    <button (click)="selectItem(item, i)">Select</button>
  </div>
}

You can pass any expression as an argument — the item itself, the index, a computed value, or even a combination.


4.5 — Inline Event Expressions

For very simple cases, you can write a small expression directly in the template instead of creating a separate method:

<!-- Simple assignment directly in template -->
<button (click)="count = count + 1">Increment</button>
<button (click)="isMenuOpen = !isMenuOpen">Toggle Menu</button>
<button (click)="selectedTab = 'profile'">Go to Profile</button>

This is fine for truly simple, one-liner changes. But for anything more complex — validation, multiple operations, async work — always use a method in the TypeScript class. Keep your templates readable.


Chapter 5 — Two-Way Binding


5.1 — What is Two-Way Binding?

Interpolation and property binding are one-way — from TypeScript to the template. Event binding is one-way — from the template to TypeScript.

Two-way binding is both directions at once. The most common use case is form inputs — you want the input to display the current value from TypeScript, AND you want TypeScript to update whenever the user types something.

You use the syntax [(ngModel)] for two-way binding on form inputs. This syntax is sometimes jokingly called "banana in a box" because [()] looks like a banana inside a box.


5.2 — Setting Up FormsModule

To use ngModel, you need to import FormsModule in your component:

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-form-demo',
  imports: [FormsModule],    // ← must import this
  templateUrl: './form-demo.html',
  styleUrl: './form-demo.css'
})
export class FormDemo {
  userName: string = '';
  email: string = '';
  selectedColor: string = 'blue';
  agreeToTerms: boolean = false;
  favoriteFramework: string = 'angular';
}

5.3 — Two-Way Binding in Action

<div class="form-demo">

  <div class="field">
    <label>Name</label>
    <input type="text" [(ngModel)]="userName" placeholder="Enter your name">
    <p>Hello, {{ userName }}!</p>
  </div>

  <div class="field">
    <label>Email</label>
    <input type="email" [(ngModel)]="email" placeholder="Enter email">
    <p>Your email: {{ email }}</p>
  </div>

  <div class="field">
    <label>Favorite Color</label>
    <select [(ngModel)]="selectedColor">
      <option value="blue">Blue</option>
      <option value="red">Red</option>
      <option value="green">Green</option>
    </select>
    <p>You chose: {{ selectedColor }}</p>
  </div>

  <div class="field">
    <label>
      <input type="checkbox" [(ngModel)]="agreeToTerms">
      I agree to the terms
    </label>
    <p>Agreed: {{ agreeToTerms }}</p>
  </div>

  <div class="field">
    <label>Framework</label>
    <label><input type="radio" [(ngModel)]="favoriteFramework" value="angular"> Angular</label>
    <label><input type="radio" [(ngModel)]="favoriteFramework" value="react"> React</label>
    <label><input type="radio" [(ngModel)]="favoriteFramework" value="vue"> Vue</label>
    <p>You chose: {{ favoriteFramework }}</p>
  </div>

</div>

As soon as you type in the Name input, userName in TypeScript updates instantly. The paragraph below the input updates at the same time because {{ userName }} reads from that same property. Both the input and the paragraph are always in sync — that is two-way binding.


5.4 — How Two-Way Binding Works Under the Hood

[(ngModel)]="userName" is actually just a shorthand for doing both property binding AND event binding at the same time:

<!-- Two-way binding shorthand -->
<input [(ngModel)]="userName">

<!-- Exactly equivalent to this: -->
<input [ngModel]="userName" (ngModelChange)="userName = $event">

[ngModel]="userName" — property binding sets the input's value to whatever userName holds. (ngModelChange)="userName = $event" — event binding updates userName whenever the input value changes.

Angular combines these two into the [()] syntax as a convenience. Understanding this is useful for debugging and for building your own two-way bindable components.


Chapter 6 — Angular's Built-in Control Flow


6.1 — Why Control Flow Exists in Templates

Your templates need to be dynamic. Sometimes you want to show something only when a condition is true. Sometimes you want to repeat an element for every item in a list. Sometimes you want to show different things based on a value.

Angular provides built-in template syntax for all of this. In modern Angular, this is done with @if, @for, and @switch — clean, readable syntax that lives directly in your HTML template.


6.2 — @if — Conditional Rendering

@if shows or hides a block of HTML based on a condition. If the condition is true, the HTML is added to the DOM. If it is false, the HTML is completely removed from the DOM (not just hidden — actually removed).

import { Component } from '@angular/core';

@Component({
  selector: 'app-auth-demo',
  imports: [],
  templateUrl: './auth-demo.html',
  styleUrl: './auth-demo.css'
})
export class AuthDemo {
  isLoggedIn: boolean = false;
  userName: string = 'Rahul';
  userRole: string = 'admin';
  cartCount: number = 3;

  login(): void {
    this.isLoggedIn = true;
  }

  logout(): void {
    this.isLoggedIn = false;
  }
}
@if (isLoggedIn) {
  <div class="welcome">
    <h2>Welcome back, {{ userName }}!</h2>
    <p>You have {{ cartCount }} items in your cart.</p>
    <button (click)="logout()">Logout</button>
  </div>
} @else {
  <div class="login-prompt">
    <h2>Please log in</h2>
    <button (click)="login()">Login</button>
  </div>
}

The @else block is optional. When the condition in @if is false, Angular renders the @else block instead.


6.3 — @if with @else if

For multiple conditions, you chain @else if:

@if (userRole === 'admin') {
  <div class="admin-panel">
    <h2>Admin Dashboard</h2>
    <p>You have full access to all features.</p>
  </div>
} @else if (userRole === 'moderator') {
  <div class="mod-panel">
    <h2>Moderator Panel</h2>
    <p>You can manage content and users.</p>
  </div>
} @else if (userRole === 'user') {
  <div class="user-panel">
    <h2>User Dashboard</h2>
    <p>Welcome to your personal space.</p>
  </div>
} @else {
  <div class="guest-view">
    <h2>Guest Access</h2>
    <p>Please sign up for a full experience.</p>
  </div>
}

Angular evaluates each condition in order and renders the first block whose condition is true.


6.4 — @for — Rendering Lists

@for loops through an array and renders a block of HTML for each item:

import { Component } from '@angular/core';

@Component({
  selector: 'app-list-demo',
  imports: [],
  templateUrl: './list-demo.html',
  styleUrl: './list-demo.css'
})
export class ListDemo {
  fruits: string[] = ['Apple', 'Mango', 'Banana', 'Orange', 'Grapes'];

  students = [
    { id: 1, name: 'Rahul Sharma', score: 92, passing: true },
    { id: 2, name: 'Priya Patel', score: 78, passing: true },
    { id: 3, name: 'Amit Kumar', score: 45, passing: false },
    { id: 4, name: 'Sneha Gupta', score: 88, passing: true }
  ];
}
<!-- Simple array of strings -->
<ul>
  @for (fruit of fruits; track fruit) {
    <li>{{ fruit }}</li>
  }
</ul>

<!-- Array of objects -->
<div class="student-list">
  @for (student of students; track student.id) {
    <div class="student-item">
      <strong>{{ student.name }}</strong>
      <span>Score: {{ student.score }}</span>
      @if (student.passing) {
        <span class="pass">✓ Passing</span>
      } @else {
        <span class="fail">✗ Failing</span>
      }
    </div>
  }
</div>

The track part is required in Angular's @for. It tells Angular how to uniquely identify each item in the list. This is important for performance — when the list changes, Angular uses track to figure out which items are new, which moved, and which were removed, so it can update only what changed instead of re-rendering the entire list.

For arrays of objects, always track by a unique ID: track student.id. For arrays of simple values like strings, you can track by the value itself: track fruit. Never track $index unless there is absolutely no other option — it negates the performance benefit.


6.5 — @for with Built-in Variables

Inside a @for block, Angular provides several useful variables you can use:

@for (student of students; track student.id; let i = $index; let isFirst = $first; let isLast = $last; let isEven = $even) {
  <div class="student-item"
       [class.highlighted]="isFirst"
       [class.even-row]="isEven">

    <span class="position">{{ i + 1 }}.</span>
    <span>{{ student.name }}</span>

    @if (isFirst) {
      <span class="badge">🏆 Top Student</span>
    }

    @if (isLast) {
      <span class="badge">📌 Last Entry</span>
    }
  </div>
}

Here are all the built-in variables available in @for:

$index — the current iteration index, starting from 0. Assign it with let i = $index.

$first — a boolean, true only for the very first item.

$last — a boolean, true only for the very last item.

$even — a boolean, true for items at even indexes (0, 2, 4...).

$odd — a boolean, true for items at odd indexes (1, 3, 5...).

$count — the total number of items in the array.


6.6 — @empty — Handling Empty Lists

Angular's @for has a built-in @empty block that renders when the array is empty:

export class TaskList {
  tasks: string[] = [];    // empty array

  addTask(): void {
    this.tasks.push('New Task ' + (this.tasks.length + 1));
  }
}
<button (click)="addTask()">Add Task</button>

<div class="task-list">
  @for (task of tasks; track task) {
    <div class="task">{{ task }}</div>
  } @empty {
    <div class="empty-state">
      <p>No tasks yet. Click "Add Task" to get started!</p>
    </div>
  }
</div>

When tasks is empty, the @empty block renders. When tasks are added, the @empty block disappears and the task items appear. This eliminates the need for a separate @if (tasks.length === 0) check.


6.7 — @switch — Multiple Conditions Based on One Value

When you want to show different things based on a single value and you have many possible cases, @switch is cleaner than a long chain of @else if:

export class OrderTracking {
  orderStatus: string = 'processing';
}
<div class="order-status">
  @switch (orderStatus) {
    @case ('pending') {
      <div class="status pending">
        <span>⏳</span>
        <p>Your order is pending confirmation</p>
      </div>
    }
    @case ('processing') {
      <div class="status processing">
        <span>⚙️</span>
        <p>Your order is being processed</p>
      </div>
    }
    @case ('shipped') {
      <div class="status shipped">
        <span>🚚</span>
        <p>Your order is on the way</p>
      </div>
    }
    @case ('delivered') {
      <div class="status delivered">
        <span>✅</span>
        <p>Your order has been delivered</p>
      </div>
    }
    @case ('cancelled') {
      <div class="status cancelled">
        <span>❌</span>
        <p>Your order was cancelled</p>
      </div>
    }
    @default {
      <div class="status unknown">
        <p>Unknown status</p>
      </div>
    }
  }
</div>

Angular evaluates the expression in @switch, then finds the matching @case and renders that block. If no case matches, @default renders. There is no fall-through behavior like in JavaScript's switch — only the matching case renders.


Chapter 7 — NgClass — Dynamic CSS Classes


7.1 — What is NgClass?

You already saw [class.className]="condition" for adding a single class conditionally. NgClass is more powerful — it lets you add and remove multiple classes at once based on different conditions.

To use NgClass, import it in your component:

import { Component } from '@angular/core';
import { NgClass } from '@angular/common';

@Component({
  selector: 'app-ngclass-demo',
  imports: [NgClass],
  templateUrl: './ngclass-demo.html',
  styleUrl: './ngclass-demo.css'
})
export class NgClassDemo {
  isActive: boolean = true;
  hasError: boolean = false;
  isLarge: boolean = true;
  buttonType: string = 'primary';
}

7.2 — NgClass with an Object

Pass an object where the keys are CSS class names and the values are boolean conditions:

<div [ngClass]="{
  'active': isActive,
  'error': hasError,
  'large': isLarge
}">
  This div gets active and large classes (not error because hasError is false)
</div>

Angular adds the class when the condition is true and removes it when false. You can have as many class/condition pairs as you need.


7.3 — NgClass with a Method

For complex logic, compute the class object in TypeScript and return it from a method:

export class StatusBadge {
  status: string = 'success';  // could be: success, warning, error, info

  getStatusClasses(): object {
    return {
      'badge': true,                           // always applied
      'badge-success': this.status === 'success',
      'badge-warning': this.status === 'warning',
      'badge-error': this.status === 'error',
      'badge-info': this.status === 'info'
    };
  }
}
<span [ngClass]="getStatusClasses()">{{ status }}</span>

7.4 — NgClass with an Array

You can also pass an array of class names:

<div [ngClass]="['card', 'shadow', 'rounded']">
  This div gets all three classes
</div>

<!-- Or with dynamic values -->
<div [ngClass]="['btn', 'btn-' + buttonType]">
  Button
</div>

When buttonType is 'primary', this renders with classes btn and btn-primary.


Chapter 8 — NgStyle — Dynamic Inline Styles


8.1 — What is NgStyle?

NgStyle lets you apply multiple inline styles dynamically to an element. Import it from @angular/common.

import { Component } from '@angular/core';
import { NgStyle } from '@angular/common';

@Component({
  selector: 'app-ngstyle-demo',
  imports: [NgStyle],
  templateUrl: './ngstyle-demo.html',
  styleUrl: './ngstyle-demo.css'
})
export class NgStyleDemo {
  primaryColor: string = '#0070f3';
  fontSize: number = 16;
  isHighlighted: boolean = true;
  opacity: number = 1;
}
<p [ngStyle]="{
  'color': primaryColor,
  'font-size': fontSize + 'px',
  'opacity': opacity,
  'background-color': isHighlighted ? '#fffde7' : 'transparent',
  'font-weight': 'bold',
  'padding': '12px'
}">
  This paragraph has dynamic styles
</p>

8.2 — When to Use NgStyle vs Class Binding

Use [style.property] for a single dynamic style:

<p [style.color]="textColor">Simple single style</p>

Use NgStyle when you need to apply several styles at once based on complex conditions. But as a general rule, prefer using CSS classes over inline styles. Inline styles are harder to maintain and override. It is usually better to define CSS classes in your stylesheet and use NgClass to apply them conditionally.


Chapter 9 — ng-template, ng-container, ng-content


9.1 — ng-template

ng-template defines a block of HTML that Angular does NOT render by default. It sits in your template as a definition that Angular can use later — either you tell Angular to render it conditionally, or Angular uses it as a template reference.

The most common use is with @if @else:

@if (isLoading) {
  <app-spinner></app-spinner>
} @else {
  <div class="content">
    <h2>Data loaded!</h2>
  </div>
}

You can also use a template reference with the older *ngIf style (still valid):

<div *ngIf="isLoggedIn; else loginTemplate">
  Welcome, {{ userName }}!
</div>

<ng-template #loginTemplate>
  <p>Please log in to continue.</p>
</ng-template>

#loginTemplate is a template reference variable. When isLoggedIn is false, Angular renders the content of the ng-template marked with #loginTemplate. The ng-template itself never appears in the DOM — only its content is ever rendered.


9.2 — ng-container

ng-container is an invisible wrapper element. It lets you group elements and apply structural directives without adding any actual HTML element to the DOM.

The problem it solves — sometimes you need to apply a condition to a group of elements but you do not want a wrapper div:

<!-- This adds an unwanted div to the DOM -->
<div @if (isAdmin)>
  <button>Edit</button>
  <button>Delete</button>
  <button>Manage Users</button>
</div>

<!-- This renders nothing extra in the DOM — ng-container disappears -->
@if (isAdmin) {
  <ng-container>
    <button>Edit</button>
    <button>Delete</button>
    <button>Manage Users</button>
  </ng-container>
}

Actually with the modern @if syntax, you do not even need ng-container for this because @if blocks do not add wrapper elements. ng-container is more useful with the older *ngIf and *ngFor directive syntax where you need to apply two directives to the same element but HTML only allows one:

<!-- Can't put both *ngFor and *ngIf on the same element -->
<!-- This is wrong: -->
<div *ngFor="let item of items" *ngIf="item.isVisible">{{ item.name }}</div>

<!-- Use ng-container to separate them: -->
<ng-container *ngFor="let item of items">
  <div *ngIf="item.isVisible">{{ item.name }}</div>
</ng-container>

<!-- Or better yet with modern syntax: -->
@for (item of items; track item.id) {
  @if (item.isVisible) {
    <div>{{ item.name }}</div>
  }
}

9.3 — ng-content

We covered this deeply in Phase 3, but let's see it again here in context with data binding.

ng-content is a slot in a component's template where projected content (HTML passed in from outside) will appear:

import { Component } from '@angular/core';

@Component({
  selector: 'app-alert',
  imports: [],
  template: `
    <div class="alert" [class]="'alert-' + type">
      <strong>{{ title }}</strong>
      <ng-content></ng-content>
    </div>
  `,
  styles: [`
    .alert { padding: 16px; border-radius: 8px; margin: 8px 0; }
    .alert-success { background: #d4edda; color: #155724; }
    .alert-error { background: #f8d7da; color: #721c24; }
    .alert-info { background: #d1ecf1; color: #0c5460; }
  `]
})
export class Alert {
  @Input() type: string = 'info';
  @Input() title: string = '';
}
<!-- In parent template -->
<app-alert type="success" title="Success!">
  Your profile has been updated successfully.
</app-alert>

<app-alert type="error" title="Error!">
  Something went wrong. Please try again later.
</app-alert>

The Alert component receives type and title via @Input() for the parts it controls. The message body is completely flexible — whatever the parent puts between the tags goes into <ng-content>. This makes the component reusable for any kind of message.


Chapter 10 — Custom Directives


10.1 — What is a Directive?

A directive is a class that adds behavior to an existing DOM element. Components are actually a special type of directive that have their own template. Regular directives do not have templates — they just modify existing elements.

There are two types of directives you will build:

Attribute directives — change the appearance or behavior of an element. You add them as attributes on existing HTML elements.

Structural directives — change the DOM structure by adding or removing elements. @if and @for are structural directives built into Angular.

Let's build some real custom attribute directives.


10.2 — Building a Highlight Directive

Generate a directive:

ng generate directive highlight --skip-tests

This creates src/app/highlight/highlight.ts:

import { Directive, ElementRef, HostListener, Input, OnInit } from '@angular/core';

@Directive({
  selector: '[appHighlight]'   // used as an attribute: <p appHighlight>
})
export class Highlight implements OnInit {

  @Input() appHighlight: string = '#ffff00';  // default yellow
  @Input() defaultColor: string = 'transparent';

  constructor(private el: ElementRef) {
    // el.nativeElement is the actual DOM element this directive is on
  }

  ngOnInit(): void {
    this.el.nativeElement.style.backgroundColor = this.defaultColor;
  }

  @HostListener('mouseenter') onMouseEnter(): void {
    this.el.nativeElement.style.backgroundColor = this.appHighlight;
  }

  @HostListener('mouseleave') onMouseLeave(): void {
    this.el.nativeElement.style.backgroundColor = this.defaultColor;
  }
}

Let's understand each part:

@Directive({ selector: '[appHighlight]' }) — the selector is in square brackets because it is used as an HTML attribute, not an element tag. When Angular sees appHighlight as an attribute on any element, it activates this directive on that element.

ElementRef — Angular injects this and it gives you direct access to the DOM element the directive is attached to. this.el.nativeElement is the raw DOM element.

@HostListener('mouseenter') — this decorator listens for events on the host element (the element the directive is attached to). When the mouseenter event fires on that element, the decorated method is called automatically.

@Input() appHighlight — the input name matches the directive selector. This is a convention — it lets you pass the highlight color directly on the same attribute: <p appHighlight="#ff0000">.

Now to use this directive, import it in any component:

import { Component } from '@angular/core';
import { Highlight } from './highlight/highlight';

@Component({
  selector: 'app-home',
  imports: [Highlight],
  template: `
    <p appHighlight>Hover me — highlights yellow by default</p>
    <p appHighlight="#64ffda">Hover me — highlights teal</p>
    <p appHighlight="#ff6b6b" defaultColor="#ffe0e0">Hover me — pink highlight, light default</p>
  `
})
export class Home { }

10.3 — Building a Click Outside Directive

Here is a more practical directive — detecting when the user clicks outside of an element. This is commonly used for closing dropdowns or modals:

ng generate directive click-outside --skip-tests

src/app/click-outside/click-outside.ts:

import { Directive, ElementRef, EventEmitter, HostListener, Output } from '@angular/core';

@Directive({
  selector: '[appClickOutside]'
})
export class ClickOutside {

  @Output() clickOutside = new EventEmitter<void>();

  constructor(private el: ElementRef) {}

  @HostListener('document:click', ['$event'])
  onDocumentClick(event: MouseEvent): void {
    const clickedInside = this.el.nativeElement.contains(event.target);

    if (!clickedInside) {
      this.clickOutside.emit();
    }
  }
}

@HostListener('document:click', ['$event']) — the document: prefix means we are listening on the entire document, not just the host element. This lets us detect clicks anywhere on the page.

this.el.nativeElement.contains(event.target) — this checks whether the element that was clicked (event.target) is inside our host element. If it is not inside, we emit the clickOutside event.

Using it:

import { Component } from '@angular/core';
import { ClickOutside } from './click-outside/click-outside';

@Component({
  selector: 'app-dropdown',
  imports: [ClickOutside],
  template: `
    <div class="dropdown" appClickOutside (clickOutside)="closeDropdown()">
      <button (click)="toggleDropdown()">Menu ▾</button>

      @if (isOpen) {
        <div class="dropdown-menu">
          <a href="#">Profile</a>
          <a href="#">Settings</a>
          <a href="#">Logout</a>
        </div>
      }
    </div>
  `,
  styles: [`
    .dropdown { position: relative; display: inline-block; }
    .dropdown-menu {
      position: absolute;
      top: 100%;
      left: 0;
      background: white;
      border: 1px solid #e0e0e0;
      border-radius: 8px;
      min-width: 160px;
      box-shadow: 0 4px 12px rgba(0,0,0,0.1);
    }
    .dropdown-menu a {
      display: block;
      padding: 10px 16px;
      color: #333;
      text-decoration: none;
    }
    .dropdown-menu a:hover { background: #f5f5f5; }
  `]
})
export class Dropdown {
  isOpen: boolean = false;

  toggleDropdown(): void {
    this.isOpen = !this.isOpen;
  }

  closeDropdown(): void {
    this.isOpen = false;
  }
}

10.4 — Building an AutoFocus Directive

A directive that automatically focuses an input when it appears:

ng generate directive auto-focus --skip-tests

src/app/auto-focus/auto-focus.ts:

import { AfterViewInit, Directive, ElementRef } from '@angular/core';

@Directive({
  selector: '[appAutoFocus]'
})
export class AutoFocus implements AfterViewInit {

  constructor(private el: ElementRef) {}

  ngAfterViewInit(): void {
    this.el.nativeElement.focus();
  }
}

Using it:

<input type="text" appAutoFocus placeholder="This input is focused automatically">

As soon as this input appears in the DOM, the directive focuses it. No JavaScript needed in the component. The directive handles it completely.


Chapter 11 — A Complete Real-World Example


Let's build a Task Manager application that uses every concept from this phase together. It will have a task list with filtering, status toggling, priority badges, and a form to add new tasks.


Step 1 — Create the project

ng new task-manager --style=css
cd task-manager
ng g c task-list --skip-tests
ng g c task-card --skip-tests
ng g c task-form --skip-tests
ng g d priority-color --skip-tests

Step 2 — Task Card Component

src/app/task-card/task-card.ts:

import { Component, Input, Output, EventEmitter } from '@angular/core';
import { NgClass } from '@angular/common';
import { PriorityColor } from '../priority-color/priority-color';

interface Task {
  id: number;
  title: string;
  description: string;
  priority: 'low' | 'medium' | 'high';
  completed: boolean;
}

@Component({
  selector: 'app-task-card',
  imports: [NgClass, PriorityColor],
  templateUrl: './task-card.html',
  styleUrl: './task-card.css'
})
export class TaskCard {

  @Input({ required: true }) task!: Task;

  @Output() toggled = new EventEmitter<number>();
  @Output() deleted = new EventEmitter<number>();

  onToggle(): void {
    this.toggled.emit(this.task.id);
  }

  onDelete(): void {
    this.deleted.emit(this.task.id);
  }
}

src/app/task-card/task-card.html:

<div class="task-card" [ngClass]="{ 'completed': task.completed }">
  <div class="task-header">
    <input
      type="checkbox"
      [checked]="task.completed"
      (change)="onToggle()">

    <h3 [class.strikethrough]="task.completed">{{ task.title }}</h3>

    <span class="priority-badge" [appPriorityColor]="task.priority">
      {{ task.priority }}
    </span>
  </div>

  <p class="description">{{ task.description }}</p>

  <div class="task-footer">
    <span class="status">
      {{ task.completed ? '✓ Completed' : '○ Pending' }}
    </span>
    <button class="delete-btn" (click)="onDelete()">Delete</button>
  </div>
</div>

src/app/task-card/task-card.css:

.task-card {
  background: white;
  border-radius: 12px;
  padding: 20px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.08);
  border-left: 4px solid #0070f3;
  transition: all 0.2s;
}

.task-card.completed {
  opacity: 0.6;
  border-left-color: #28a745;
}

.task-header {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 8px;
}

.task-header h3 {
  flex: 1;
  font-size: 16px;
  color: #1a1a2e;
}

.strikethrough {
  text-decoration: line-through;
  color: #999;
}

.priority-badge {
  padding: 3px 10px;
  border-radius: 20px;
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
}

.description {
  color: #666;
  font-size: 14px;
  line-height: 1.5;
  margin-bottom: 12px;
}

.task-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.status {
  font-size: 13px;
  color: #888;
}

.delete-btn {
  background: none;
  border: 1px solid #dc3545;
  color: #dc3545;
  padding: 4px 12px;
  border-radius: 6px;
  cursor: pointer;
  font-size: 13px;
  transition: all 0.2s;
}

.delete-btn:hover {
  background: #dc3545;
  color: white;
}

Step 3 — Priority Color Directive

src/app/priority-color/priority-color.ts:

import { Directive, Input, ElementRef, OnInit } from '@angular/core';

@Directive({
  selector: '[appPriorityColor]'
})
export class PriorityColor implements OnInit {

  @Input() appPriorityColor: string = 'low';

  constructor(private el: ElementRef) {}

  ngOnInit(): void {
    const colors: Record<string, { bg: string; text: string }> = {
      low: { bg: '#d4edda', text: '#155724' },
      medium: { bg: '#fff3cd', text: '#856404' },
      high: { bg: '#f8d7da', text: '#721c24' }
    };

    const colorSet = colors[this.appPriorityColor] || colors['low'];
    this.el.nativeElement.style.backgroundColor = colorSet.bg;
    this.el.nativeElement.style.color = colorSet.text;
  }
}

Step 4 — Task Form Component

src/app/task-form/task-form.ts:

import { Component, Output, EventEmitter } from '@angular/core';
import { FormsModule } from '@angular/forms';

interface NewTask {
  title: string;
  description: string;
  priority: 'low' | 'medium' | 'high';
}

@Component({
  selector: 'app-task-form',
  imports: [FormsModule],
  templateUrl: './task-form.html',
  styleUrl: './task-form.css'
})
export class TaskForm {

  @Output() taskAdded = new EventEmitter<NewTask>();

  newTask: NewTask = {
    title: '',
    description: '',
    priority: 'medium'
  };

  onSubmit(): void {
    if (this.newTask.title.trim() === '') {
      return;
    }

    this.taskAdded.emit({ ...this.newTask });

    this.newTask = {
      title: '',
      description: '',
      priority: 'medium'
    };
  }
}

src/app/task-form/task-form.html:

<div class="form-container">
  <h2>Add New Task</h2>

  <div class="field">
    <label>Title</label>
    <input
      type="text"
      [(ngModel)]="newTask.title"
      placeholder="What needs to be done?">
  </div>

  <div class="field">
    <label>Description</label>
    <textarea
      [(ngModel)]="newTask.description"
      placeholder="Add more details..."
      rows="3">
    </textarea>
  </div>

  <div class="field">
    <label>Priority</label>
    <select [(ngModel)]="newTask.priority">
      <option value="low">Low</option>
      <option value="medium">Medium</option>
      <option value="high">High</option>
    </select>
  </div>

  <button
    class="submit-btn"
    (click)="onSubmit()"
    [disabled]="newTask.title.trim() === ''">
    Add Task
  </button>
</div>

src/app/task-form/task-form.css:

.form-container {
  background: white;
  border-radius: 12px;
  padding: 24px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.08);
  margin-bottom: 32px;
}

h2 {
  font-size: 20px;
  color: #1a1a2e;
  margin-bottom: 20px;
}

.field {
  margin-bottom: 16px;
}

label {
  display: block;
  font-size: 13px;
  font-weight: 600;
  color: #555;
  margin-bottom: 6px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

input[type="text"],
textarea,
select {
  width: 100%;
  padding: 10px 14px;
  border: 1px solid #ddd;
  border-radius: 8px;
  font-size: 15px;
  color: #333;
  transition: border-color 0.2s;
  font-family: inherit;
}

input:focus, textarea:focus, select:focus {
  outline: none;
  border-color: #0070f3;
}

textarea { resize: vertical; }

.submit-btn {
  width: 100%;
  padding: 12px;
  background: #0070f3;
  color: white;
  border: none;
  border-radius: 8px;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.2s;
}

.submit-btn:hover { background: #005ac1; }
.submit-btn:disabled { background: #ccc; cursor: not-allowed; }

Step 5 — Task List Component

src/app/task-list/task-list.ts:

import { Component } from '@angular/core';
import { NgClass } from '@angular/common';
import { TaskCard } from '../task-card/task-card';
import { TaskForm } from '../task-form/task-form';

interface Task {
  id: number;
  title: string;
  description: string;
  priority: 'low' | 'medium' | 'high';
  completed: boolean;
}

@Component({
  selector: 'app-task-list',
  imports: [TaskCard, TaskForm, NgClass],
  templateUrl: './task-list.html',
  styleUrl: './task-list.css'
})
export class TaskList {

  activeFilter: string = 'all';

  tasks: Task[] = [
    { id: 1, title: 'Learn Angular Data Binding', description: 'Study interpolation, property binding, event binding, and two-way binding.', priority: 'high', completed: true },
    { id: 2, title: 'Build a Task Manager App', description: 'Create a complete task manager using components, directives, and data binding.', priority: 'high', completed: false },
    { id: 3, title: 'Read RxJS Documentation', description: 'Understand Observables, Subjects, and common operators.', priority: 'medium', completed: false },
    { id: 4, title: 'Practice TypeScript Generics', description: 'Review generic functions, interfaces, and classes.', priority: 'low', completed: false }
  ];

  get filteredTasks(): Task[] {
    switch (this.activeFilter) {
      case 'active':
        return this.tasks.filter(t => !t.completed);
      case 'completed':
        return this.tasks.filter(t => t.completed);
      default:
        return this.tasks;
    }
  }

  get completedCount(): number {
    return this.tasks.filter(t => t.completed).length;
  }

  get pendingCount(): number {
    return this.tasks.filter(t => !t.completed).length;
  }

  setFilter(filter: string): void {
    this.activeFilter = filter;
  }

  onTaskAdded(newTask: { title: string; description: string; priority: 'low' | 'medium' | 'high' }): void {
    const task: Task = {
      id: Date.now(),
      ...newTask,
      completed: false
    };
    this.tasks = [...this.tasks, task];
  }

  onTaskToggled(taskId: number): void {
    this.tasks = this.tasks.map(task =>
      task.id === taskId ? { ...task, completed: !task.completed } : task
    );
  }

  onTaskDeleted(taskId: number): void {
    this.tasks = this.tasks.filter(task => task.id !== taskId);
  }
}

src/app/task-list/task-list.html:

<div class="task-manager">
  <div class="header">
    <h1>Task Manager</h1>
    <div class="stats">
      <span class="stat">{{ pendingCount }} Pending</span>
      <span class="divider">|</span>
      <span class="stat done">{{ completedCount }} Completed</span>
    </div>
  </div>

  <app-task-form (taskAdded)="onTaskAdded($event)"></app-task-form>

  <div class="filters">
    @for (filter of ['all', 'active', 'completed']; track filter) {
      <button
        [ngClass]="{ 'active': activeFilter === filter }"
        (click)="setFilter(filter)">
        {{ filter | titlecase }}
      </button>
    }
  </div>

  <div class="tasks">
    @for (task of filteredTasks; track task.id) {
      <app-task-card
        [task]="task"
        (toggled)="onTaskToggled($event)"
        (deleted)="onTaskDeleted($event)">
      </app-task-card>
    } @empty {
      <div class="empty-state">
        <p>No tasks found. Add one above!</p>
      </div>
    }
  </div>
</div>

src/app/task-list/task-list.css:

.task-manager {
  max-width: 720px;
  margin: 0 auto;
  padding: 40px 24px;
}

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 28px;
}

h1 {
  font-size: 32px;
  font-weight: 700;
  color: #1a1a2e;
}

.stats {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  color: #666;
}

.stat.done { color: #28a745; }
.divider { color: #ddd; }

.filters {
  display: flex;
  gap: 8px;
  margin-bottom: 24px;
}

.filters button {
  padding: 8px 20px;
  border: 1px solid #ddd;
  background: white;
  border-radius: 20px;
  font-size: 14px;
  cursor: pointer;
  transition: all 0.2s;
  color: #555;
}

.filters button.active {
  background: #0070f3;
  color: white;
  border-color: #0070f3;
}

.tasks {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.empty-state {
  text-align: center;
  padding: 48px;
  color: #999;
  background: white;
  border-radius: 12px;
}

Step 6 — Wire it in app.ts

src/app/app.ts:

import { Component } from '@angular/core';
import { TaskList } from './task-list/task-list';

@Component({
  selector: 'app-root',
  imports: [TaskList],
  template: `<app-task-list></app-task-list>`
})
export class App { }

src/styles.css:

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}

body {
  background: #f0f2f5;
  min-height: 100vh;
}

Run ng serve -o. You now have a fully working Task Manager that demonstrates:

Interpolation displaying task titles, descriptions, and counts. Property binding with [checked], [disabled], [class.strikethrough], and [ngClass]. Event binding with (click), (change) on checkboxes. Two-way binding with [(ngModel)] on the form inputs. @for rendering the task list and filter buttons. @if for showing the completed state. @empty for the empty state. @switch equivalent logic in the filter getter. A custom directive PriorityColor that dynamically styles priority badges. Parent-child communication using @Input() and @Output() throughout.


Phase 4 — Complete Summary

Here is everything you learned in this phase:

Interpolation {{ }} — Displays TypeScript values in the template. Can evaluate expressions, call methods, and use operators. One-way from TypeScript to template.

Property Binding [ ] — Sets DOM element properties to TypeScript values. Use square brackets when the value is dynamic. Works with any DOM property — [disabled], [src], [href], [class.name], [style.property]. Use [attr.name] for HTML attributes that have no DOM property equivalent.

Event Binding ( ) — Listens for browser events and calls TypeScript methods. Use $event to access the native event object. Works with any DOM event — (click), (keyup), (input), (submit), (mouseenter). Angular-specific key filters like (keyup.enter) are a useful shortcut.

Two-Way Binding [( )] — Combines property and event binding. [(ngModel)] keeps TypeScript properties and HTML form inputs perfectly in sync. Requires FormsModule in the component's imports.

@if — Conditionally renders HTML blocks. Supports @else if and @else blocks. Removes elements from the DOM completely when the condition is false.

@for — Renders a block for each item in an array. The track expression is required and should use a unique identifier. Built-in variables: $index, $first, $last, $even, $odd, $count.

@empty — Renders when the @for array is empty. Eliminates the need for a separate empty state check.

@switch — Renders based on one value matching multiple cases. Cleaner than a long chain of @else if when checking a single value.

NgClass — Applies multiple CSS classes conditionally using an object, array, or string.

NgStyle — Applies multiple inline styles dynamically using an object.

ng-template — Defines reusable template blocks that are not rendered by default. Used with template reference variables.

ng-container — An invisible grouping element that adds nothing to the DOM. Useful for applying directives without adding wrapper elements.

ng-content — Projects content from outside into a component's template. The foundation of reusable wrapper components.

Custom Directives — Add behavior to existing DOM elements. ElementRef gives access to the native element. @HostListener listens to events on the host element. @Input() passes configuration to the directive.


What's Next — Phase 5

In Phase 5 we go into Services and Dependency Injection — one of Angular's most powerful features:

What a service is and why you need it. How to create services and inject them into components. How Angular's Dependency Injection system works under the hood. How to share data between components using a service. How to scope services — app-wide, component-level. Using services as a simple state management solution with signals.

Phase 3 — Components Deep Dive

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:

  1. The button triggers (click) in the child template
  2. onAddToCart() runs in the child TypeScript
  3. this.addedToCart.emit(this.productName) fires the event with the product name
  4. Angular passes this to the parent's (addedToCart) listener
  5. onProductAddedToCart($event) runs in the parent TypeScript
  6. 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 ngOnInit which calculates the status
  • Clicking a card emits an event via @Output() with the student's name
  • The parent StudentList receives the event and updates selectedStudentName
  • The selected card gets highlighted because [isSelected]="selectedStudentName === student.name" becomes true
  • The selection banner appears via @if showing the selected student's details
  • @ViewChild gives the parent direct access to the first StudentCard component

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.

Phase 4 — Data Binding & Directives

Chapter 1 — What is Data Binding? 1.1 — The Problem Data Binding Solves When you build a web application, you have two worlds that need to t...