Phase 5 — Services & Dependency Injection

Chapter 1 — The Problem Services Solve


1.1 — When Components Are Not Enough

So far everything you have built has lived inside components. The data was inside the component. The logic was inside the component. The methods were inside the component.

This works fine for small, isolated pieces of UI. But as soon as your application grows, you run into a very real problem.

Imagine you are building an e-commerce app. You have a Navbar component that shows the cart item count. You have a ProductList component where users add items to the cart. You have a CartPage component that shows everything in the cart. You have a CheckoutPage component that processes the order.

All four of these components need access to the same cart data. What do you do?

You could put the cart data in the root App component and pass it down to every child using @Input(). But that means App becomes a massive class managing data for every single feature of your application. And every component in between the root and the one that actually needs the data would have to receive it and pass it along — even if it has no use for it. This is called prop drilling and it is a nightmare to maintain.

You could duplicate the cart data in each component. But then they all go out of sync the moment one of them updates. Three components think the cart has 2 items and one thinks it has 3. Bugs everywhere.

Neither of these approaches works. What you actually need is a separate place — outside of any component — where you can store and manage shared data and logic. A place that any component can access directly whenever it needs to.

That is exactly what a Service is.


1.2 — What is a Service?

A service is a plain TypeScript class that holds data and logic that is not tied to any specific component. It sits completely outside the component tree.

Think about it this way. In your body, your heart pumps blood to every part — brain, lungs, hands, feet. The heart does not belong to the brain or to the hands. It is a shared service that every part of your body uses.

In Angular, a service is the same idea. It is a shared resource that any component in your application can use. No matter where a component is in the tree, it can reach into a service and get what it needs.

Services are used for:

Sharing data between components — the cart data, the currently logged-in user, the application settings. Any data that multiple components need lives in a service.

HTTP calls and API communication — fetching data from a server, sending form data, handling API responses. All of this lives in services, not components.

Business logic — calculations, data transformations, validation rules that are complex enough to live outside a component. If the same logic is needed in more than one place, it belongs in a service.

Utility functions — date formatting, currency conversion, string manipulation helpers that multiple parts of your app use.

The key principle is this: if you need the same thing in more than one component, put it in a service. Components stay lean and focused on their UI. Services handle everything else.


1.3 — What is Dependency Injection?

Dependency Injection, or DI, is the mechanism Angular uses to give components the services they need.

Here is the problem DI solves. Imagine a ProductList component needs the CartService. Without DI, you would do something like this:

export class ProductList {
  private cartService = new CartService();  // creating it yourself
}

This works but it is problematic. The ProductList is now responsible for creating the CartService — it controls the service's lifetime. If three components all do new CartService(), each one gets a completely separate instance with completely separate data. They are not sharing anything. And testing becomes a nightmare because you cannot easily replace the CartService with a mock version.

Dependency Injection flips this responsibility around. Instead of the component creating the service, Angular creates the service and gives it to the component. The component just says "I need a CartService" and Angular provides one.

export class ProductList {
  constructor(private cartService: CartService) {}
  // Angular creates CartService and provides it here
}

This is the "injection" part. Angular injects the service into the component. The component does not create it — it just receives it.

And because Angular manages the service's creation, it can ensure that every component that asks for CartService gets the same single instance. One CartService. Shared by every component that needs it. This is called a singleton — a class that only exists as one single instance in the application.


Chapter 2 — Creating Your First Service


2.1 — Generating a Service

Generate a service using the Angular CLI:

ng generate service cart

Shorthand:

ng g s cart

This creates two files:

src/app/cart/
├── cart.ts          ← your service class
└── cart.spec.ts     ← tests for the service

Or with --skip-tests:

ng g s cart --skip-tests

Which creates just src/app/cart/cart.ts.


2.2 — What Gets Generated

Open src/app/cart/cart.ts:

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

@Injectable({
  providedIn: 'root'
})
export class Cart {

}

Simple and clean. Let's understand every part.

@Injectable is a decorator — just like @Component turns a class into a component, @Injectable turns a class into a service that Angular knows how to inject.

providedIn: 'root' is the most important part. This tells Angular where this service should be available. 'root' means the service is available everywhere in the entire application — every single component, every other service, everything. Angular creates exactly one instance of this service and shares it across the whole app.

The class name in Angular is just the feature name. So a cart service class is named Cart, a user service is named User, an auth service is named Auth. Clean and simple.


2.3 — Building a Real Cart Service

Let's build a complete cart service with real functionality:

src/app/cart/cart.ts:

import { Injectable, signal, computed } from '@angular/core';

export interface CartItem {
  id: number;
  name: string;
  price: number;
  quantity: number;
  imageUrl: string;
}

@Injectable({
  providedIn: 'root'
})
export class Cart {

  // The cart items stored as a signal
  private items = signal<CartItem[]>([]);

  // Computed values that automatically update when items change
  readonly cartItems = this.items.asReadonly();

  readonly totalItems = computed(() =>
    this.items().reduce((sum, item) => sum + item.quantity, 0)
  );

  readonly totalPrice = computed(() =>
    this.items().reduce((sum, item) => sum + (item.price * item.quantity), 0)
  );

  readonly isEmpty = computed(() => this.items().length === 0);

  addItem(product: Omit<CartItem, 'quantity'>): void {
    const currentItems = this.items();
    const existingItem = currentItems.find(item => item.id === product.id);

    if (existingItem) {
      // If item already in cart, increase quantity
      this.items.update(items =>
        items.map(item =>
          item.id === product.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        )
      );
    } else {
      // If item not in cart, add it with quantity 1
      this.items.update(items => [...items, { ...product, quantity: 1 }]);
    }
  }

  removeItem(productId: number): void {
    this.items.update(items => items.filter(item => item.id !== productId));
  }

  increaseQuantity(productId: number): void {
    this.items.update(items =>
      items.map(item =>
        item.id === productId
          ? { ...item, quantity: item.quantity + 1 }
          : item
      )
    );
  }

  decreaseQuantity(productId: number): void {
    const item = this.items().find(i => i.id === productId);

    if (item && item.quantity === 1) {
      // If quantity would go to 0, remove the item
      this.removeItem(productId);
    } else {
      this.items.update(items =>
        items.map(i =>
          i.id === productId
            ? { ...i, quantity: i.quantity - 1 }
            : i
        )
      );
    }
  }

  clearCart(): void {
    this.items.set([]);
  }

  isInCart(productId: number): boolean {
    return this.items().some(item => item.id === productId);
  }
}

Let's look at a few things here.

The items signal is marked as private — components cannot directly change it. They must go through the service methods like addItem() and removeItem(). This is important. The service controls how its data changes. Components only ask the service to do things — they do not reach in and change data themselves.

computed() creates a value that is automatically recalculated whenever its dependencies change. totalItems automatically recalculates whenever items changes. You never have to manually update totalItems — Angular handles it. computed values are read-only.

asReadonly() exposes the signal as read-only to components. Components can read cartItems but cannot call .set() or .update() on it. Only the service's own methods can modify the data.


Chapter 3 — Injecting Services into Components


3.1 — Two Ways to Inject a Service

Angular gives you two ways to inject a service into a component. Both work — we will cover both and explain when to use each.


Method 1 — Constructor Injection (the classic way)

import { Component } from '@angular/core';
import { Cart } from '../cart/cart';

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

  constructor(private cartService: Cart) {}
  // Angular sees 'Cart' type, finds the Cart service, and provides it here
}

You declare the service as a constructor parameter with private (or public if you need the template to access it directly). Angular reads the TypeScript type Cart, finds the corresponding service, and injects it automatically.

The service is now available as this.cartService throughout the component class.


Method 2 — inject() function (the modern way)

Angular introduced a function called inject() that is increasingly the preferred approach in modern Angular:

import { Component, inject } from '@angular/core';
import { Cart } from '../cart/cart';

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

  private cartService = inject(Cart);
  // Same result as constructor injection, cleaner syntax
}

You call inject(Cart) as a class-level property initializer. Angular resolves and provides the Cart service automatically.

This approach is cleaner because it keeps your constructor empty (or removes the need for a constructor entirely). It also works in more places than constructor injection — for example, inside functions that run during component initialization.

Both approaches produce exactly the same result. Throughout this course we will primarily use inject() because it is the direction Angular is moving.


3.2 — Using the Service in the Component

Once injected, you use the service's properties and methods just like any other class property:

src/app/navbar/navbar.ts:

import { Component, inject } from '@angular/core';
import { Cart } from '../cart/cart';

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

  private cartService = inject(Cart);

  // Expose the computed signal to the template
  totalItems = this.cartService.totalItems;
}

src/app/navbar/navbar.html:

<nav>
  <div class="logo">ShopApp</div>

  <div class="nav-right">
    <a href="#">Products</a>

    <div class="cart-icon">
      🛒
      @if (totalItems() > 0) {
        <span class="cart-badge">{{ totalItems() }}</span>
      }
    </div>
  </div>
</nav>

totalItems is a computed signal from the service. When items are added to the cart anywhere in the app, totalItems automatically recalculates, and because the Navbar is reading it, Angular automatically updates the badge number. No manual communication between components needed.


3.3 — Multiple Components Using the Same Service

This is the magic of services. Let's create a ProductList component that also uses the Cart service:

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

import { Component, inject } from '@angular/core';
import { Cart } from '../cart/cart';
import { NgClass } from '@angular/common';

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

  private cartService = inject(Cart);

  // Expose service data to template
  isInCart = (id: number) => this.cartService.isInCart(id);

  products = [
    { id: 1, name: 'Mechanical Keyboard', price: 8500, imageUrl: '/keyboard.jpg' },
    { id: 2, name: 'Wireless Mouse', price: 2200, imageUrl: '/mouse.jpg' },
    { id: 3, name: 'Monitor Stand', price: 3400, imageUrl: '/stand.jpg' },
    { id: 4, name: 'USB-C Hub', price: 1800, imageUrl: '/hub.jpg' },
    { id: 5, name: 'Desk Lamp', price: 1200, imageUrl: '/lamp.jpg' },
    { id: 6, name: 'Webcam', price: 4500, imageUrl: '/webcam.jpg' }
  ];

  addToCart(product: any): void {
    this.cartService.addItem(product);
  }
}

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

<div class="product-list">
  <h2>Products</h2>
  <div class="product-grid">
    @for (product of products; track product.id) {
      <div class="product-card">
        <div class="product-image">📦</div>
        <h3>{{ product.name }}</h3>
        <p class="price">₹{{ product.price.toLocaleString() }}</p>

        <button
          (click)="addToCart(product)"
          [ngClass]="{ 'in-cart': isInCart(product.id) }">
          {{ isInCart(product.id) ? '✓ Added' : 'Add to Cart' }}
        </button>
      </div>
    }
  </div>
</div>

And a CartPage component that shows the cart contents:

src/app/cart-page/cart-page.ts:

import { Component, inject } from '@angular/core';
import { Cart } from '../cart/cart';

@Component({
  selector: 'app-cart-page',
  imports: [],
  templateUrl: './cart-page.html',
  styleUrl: './cart-page.css'
})
export class CartPage {

  cartService = inject(Cart);

  // These are direct references to the service's computed signals
  cartItems = this.cartService.cartItems;
  totalItems = this.cartService.totalItems;
  totalPrice = this.cartService.totalPrice;
  isEmpty = this.cartService.isEmpty;

  increaseQuantity(id: number): void {
    this.cartService.increaseQuantity(id);
  }

  decreaseQuantity(id: number): void {
    this.cartService.decreaseQuantity(id);
  }

  removeItem(id: number): void {
    this.cartService.removeItem(id);
  }

  clearCart(): void {
    this.cartService.clearCart();
  }
}

src/app/cart-page/cart-page.html:

<div class="cart-page">
  <h2>Your Cart ({{ totalItems() }} items)</h2>

  @if (isEmpty()) {
    <div class="empty-cart">
      <p>🛒 Your cart is empty</p>
      <p>Add some products to get started!</p>
    </div>
  } @else {
    <div class="cart-items">
      @for (item of cartItems(); track item.id) {
        <div class="cart-item">
          <div class="item-info">
            <h3>{{ item.name }}</h3>
            <p class="unit-price">₹{{ item.price.toLocaleString() }} each</p>
          </div>

          <div class="quantity-controls">
            <button (click)="decreaseQuantity(item.id)">−</button>
            <span>{{ item.quantity }}</span>
            <button (click)="increaseQuantity(item.id)">+</button>
          </div>

          <div class="item-total">
            <p>₹{{ (item.price * item.quantity).toLocaleString() }}</p>
            <button class="remove-btn" (click)="removeItem(item.id)">Remove</button>
          </div>
        </div>
      }
    </div>

    <div class="cart-summary">
      <div class="summary-row">
        <span>Total Items:</span>
        <span>{{ totalItems() }}</span>
      </div>
      <div class="summary-row total">
        <span>Total Amount:</span>
        <span>₹{{ totalPrice().toLocaleString() }}</span>
      </div>

      <div class="cart-actions">
        <button class="clear-btn" (click)="clearCart()">Clear Cart</button>
        <button class="checkout-btn">Proceed to Checkout</button>
      </div>
    </div>
  }
</div>

Now here is the beautiful thing. Navbar, ProductList, and CartPage all inject Cart. They all get the exact same instance of the service. When ProductList calls cartService.addItem(), the items signal in the service updates. Because totalItems is a computed signal based on items, it recalculates automatically. Because Navbar is reading totalItems, its badge updates. Because CartPage is reading cartItems, its list updates. All three components stay in sync perfectly — and they never even talk to each other directly. The service is the single source of truth.


Chapter 4 — How Angular's Dependency Injection Works


4.1 — The Injector

Angular has a system called the Injector. Think of the Injector as a registry — a lookup table that maps service types to service instances.

When you write inject(Cart) in a component, Angular does this:

  1. Looks up Cart in the Injector registry
  2. If an instance already exists, returns it
  3. If no instance exists yet, creates one, stores it in the registry, and returns it

Because Angular stores the instance and returns the same one every time, you get a singleton — one instance shared everywhere.

This is completely automatic. You never have to manually create services or manage their lifecycle. Angular handles it all.


4.2 — providedIn: 'root' — The Default

When you write providedIn: 'root' in your service's @Injectable, you are telling Angular to register this service with the root injector — the top-level injector that covers the entire application.

@Injectable({
  providedIn: 'root'  // ← registered at the application level
})
export class Cart { }

This means:

  • One single instance exists for the entire app
  • Every component, every other service, everything can use it
  • The service is created the first time something requests it (lazy initialization)
  • The service lives for the entire lifetime of the application

This is the correct setting for most services. Use providedIn: 'root' by default.


4.3 — Component-Level Providers — A Service Per Component Instance

Sometimes you do NOT want a shared singleton. Sometimes you want each component instance to have its own fresh copy of a service. You can do this by providing the service directly on the component:

import { Component } from '@angular/core';
import { FormState } from '../form-state/form-state';

@Component({
  selector: 'app-registration-form',
  imports: [],
  templateUrl: './registration-form.html',
  styleUrl: './registration-form.css',
  providers: [FormState]    // ← this component gets its OWN instance of FormState
})
export class RegistrationForm {
  private formState = inject(FormState);
}

When you put a service in providers on a component, Angular creates a new, separate instance of that service just for that component and all its children. When the component is destroyed, that service instance is destroyed too.

This is useful when:

  • You have a multi-step form where each form instance needs its own isolated state
  • You are building a widget that can be used multiple times on a page and each instance needs independent state
  • You want a service that resets itself when the user navigates away from a page

4.4 — Injecting Services into Other Services

Services can use other services too. You inject them exactly the same way as in components:

import { Injectable, inject } from '@angular/core';
import { Cart } from '../cart/cart';
import { Auth } from '../auth/auth';

@Injectable({
  providedIn: 'root'
})
export class Checkout {

  private cartService = inject(Cart);
  private authService = inject(Auth);

  processOrder(): void {
    if (!this.authService.isLoggedIn()) {
      console.log('User must be logged in to checkout');
      return;
    }

    const items = this.cartService.cartItems();
    const total = this.cartService.totalPrice();

    console.log(`Processing order for ${items.length} items totaling ₹${total}`);
    this.cartService.clearCart();
  }
}

Checkout uses both Cart and Auth. Angular injects both. The Checkout service coordinates between the two. This is how complex business logic stays organized — each service has a single responsibility and they collaborate through injection.


Chapter 5 — Services as State Management


5.1 — What is State?

State is data that your application needs to remember. The currently logged-in user is state. Whether the sidebar is open or closed is state. The items in the shopping cart are state. The results of a search are state.

Managing state well is one of the most important skills in building Angular apps. Services with signals give you a clean, simple way to manage state without needing any third-party library.


5.2 — Building an Auth Service

Let's build a complete authentication service that manages login state across the whole application:

src/app/auth/auth.ts:

import { Injectable, signal, computed } from '@angular/core';

export interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'guest';
  avatar: string;
}

@Injectable({
  providedIn: 'root'
})
export class Auth {

  private currentUser = signal<User | null>(null);
  private authError = signal<string>('');
  private isAuthenticating = signal<boolean>(false);

  // Public read-only access to state
  readonly user = this.currentUser.asReadonly();
  readonly error = this.authError.asReadonly();
  readonly loading = this.isAuthenticating.asReadonly();

  // Computed values derived from state
  readonly isLoggedIn = computed(() => this.currentUser() !== null);
  readonly isAdmin = computed(() => this.currentUser()?.role === 'admin');
  readonly userName = computed(() => this.currentUser()?.name ?? 'Guest');

  // Simulated user database
  private mockUsers: (User & { password: string })[] = [
    {
      id: 1,
      name: 'Rahul Sharma',
      email: 'rahul@example.com',
      password: 'password123',
      role: 'admin',
      avatar: 'R'
    },
    {
      id: 2,
      name: 'Priya Patel',
      email: 'priya@example.com',
      password: 'password123',
      role: 'user',
      avatar: 'P'
    }
  ];

  login(email: string, password: string): boolean {
    this.isAuthenticating.set(true);
    this.authError.set('');

    // Simulating authentication check
    const foundUser = this.mockUsers.find(
      u => u.email === email && u.password === password
    );

    if (foundUser) {
      const { password: _, ...userWithoutPassword } = foundUser;
      this.currentUser.set(userWithoutPassword);
      this.isAuthenticating.set(false);
      return true;
    } else {
      this.authError.set('Invalid email or password. Please try again.');
      this.isAuthenticating.set(false);
      return false;
    }
  }

  logout(): void {
    this.currentUser.set(null);
    this.authError.set('');
  }

  clearError(): void {
    this.authError.set('');
  }
}

This service manages everything related to authentication:

The currentUser signal holds either a User object or null. When it is null, the user is not logged in. When it holds a user object, they are logged in.

isLoggedIn and isAdmin are computed values — they automatically derive from currentUser. You never have to manually keep a separate isLoggedIn boolean in sync.

The login method validates credentials and updates the signal. Everything that reads isLoggedIn, userName, or user across the entire application automatically updates.


5.3 — Building a Theme Service

Here is another common use case — a service that manages app-wide theme settings:

src/app/theme/theme.ts:

import { Injectable, signal, computed, effect } from '@angular/core';

export type Theme = 'light' | 'dark' | 'system';

@Injectable({
  providedIn: 'root'
})
export class Theme {

  private activeTheme = signal<Theme>('light');

  readonly currentTheme = this.activeTheme.asReadonly();

  readonly isDark = computed(() => this.activeTheme() === 'dark');

  readonly themeLabel = computed(() => {
    const theme = this.activeTheme();
    return theme.charAt(0).toUpperCase() + theme.slice(1) + ' Mode';
  });

  constructor() {
    // Apply theme to document whenever it changes
    effect(() => {
      const theme = this.activeTheme();
      document.body.setAttribute('data-theme', theme);
    });
  }

  setTheme(theme: Theme): void {
    this.activeTheme.set(theme);
  }

  toggleTheme(): void {
    this.activeTheme.update(current => current === 'light' ? 'dark' : 'light');
  }
}

effect() is a new concept here. An effect is a side effect that runs whenever the signals it reads change. Here, whenever activeTheme changes, the effect runs and updates the data-theme attribute on the document body. You then use CSS variables in styles.css to change colors based on this attribute:

/* styles.css */
:root {
  --bg: #ffffff;
  --text: #1a1a2e;
  --surface: #f5f5f5;
}

[data-theme="dark"] {
  --bg: #0a192f;
  --text: #ccd6f6;
  --surface: #112240;
}

body {
  background: var(--bg);
  color: var(--text);
}

Any component can now inject Theme and toggle the theme — and the entire application's colors update instantly.


5.4 — Building a Notification Service

One more practical service — managing app-wide toast notifications:

src/app/notifications/notifications.ts:

import { Injectable, signal } from '@angular/core';

export interface Notification {
  id: number;
  message: string;
  type: 'success' | 'error' | 'info' | 'warning';
  duration: number;
}

@Injectable({
  providedIn: 'root'
})
export class Notifications {

  private notificationList = signal<Notification[]>([]);
  readonly notifications = this.notificationList.asReadonly();

  private nextId = 1;

  show(message: string, type: Notification['type'] = 'info', duration: number = 3000): void {
    const notification: Notification = {
      id: this.nextId++,
      message,
      type,
      duration
    };

    this.notificationList.update(list => [...list, notification]);

    // Auto-remove after duration
    setTimeout(() => {
      this.remove(notification.id);
    }, duration);
  }

  success(message: string): void {
    this.show(message, 'success');
  }

  error(message: string): void {
    this.show(message, 'error', 5000);  // errors stay longer
  }

  info(message: string): void {
    this.show(message, 'info');
  }

  warning(message: string): void {
    this.show(message, 'warning', 4000);
  }

  remove(id: number): void {
    this.notificationList.update(list => list.filter(n => n.id !== id));
  }

  clearAll(): void {
    this.notificationList.set([]);
  }
}

Now build a ToastContainer component that displays these notifications:

src/app/toast-container/toast-container.ts:

import { Component, inject } from '@angular/core';
import { Notifications } from '../notifications/notifications';
import { NgClass } from '@angular/common';

@Component({
  selector: 'app-toast-container',
  imports: [NgClass],
  templateUrl: './toast-container.html',
  styleUrl: './toast-container.css'
})
export class ToastContainer {

  notificationService = inject(Notifications);
  notifications = this.notificationService.notifications;

  dismiss(id: number): void {
    this.notificationService.remove(id);
  }
}

src/app/toast-container/toast-container.html:

<div class="toast-container">
  @for (notification of notifications(); track notification.id) {
    <div class="toast" [ngClass]="'toast-' + notification.type">
      <span class="toast-message">{{ notification.message }}</span>
      <button class="toast-close" (click)="dismiss(notification.id)">×</button>
    </div>
  }
</div>

src/app/toast-container/toast-container.css:

.toast-container {
  position: fixed;
  top: 20px;
  right: 20px;
  z-index: 9999;
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-width: 360px;
}

.toast {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 18px;
  border-radius: 8px;
  font-size: 14px;
  font-weight: 500;
  box-shadow: 0 4px 16px rgba(0,0,0,0.12);
  animation: slideIn 0.3s ease;
}

@keyframes slideIn {
  from { transform: translateX(100%); opacity: 0; }
  to { transform: translateX(0); opacity: 1; }
}

.toast-success { background: #d4edda; color: #155724; border-left: 4px solid #28a745; }
.toast-error   { background: #f8d7da; color: #721c24; border-left: 4px solid #dc3545; }
.toast-info    { background: #d1ecf1; color: #0c5460; border-left: 4px solid #17a2b8; }
.toast-warning { background: #fff3cd; color: #856404; border-left: 4px solid #ffc107; }

.toast-message { flex: 1; }

.toast-close {
  background: none;
  border: none;
  font-size: 18px;
  cursor: pointer;
  color: inherit;
  opacity: 0.7;
  margin-left: 12px;
  line-height: 1;
}

.toast-close:hover { opacity: 1; }

Put <app-toast-container> once in app.html and it will be available on every page. Now any component anywhere in the app can do:

private notifications = inject(Notifications);

someMethod(): void {
  this.notifications.success('Item added to cart!');
  this.notifications.error('Failed to save changes.');
  this.notifications.info('New updates available.');
}

And the toast appears in the top right corner. One service. One container component. Used everywhere.


Chapter 6 — Putting It All Together — A Complete App


Let's build a complete Shop App that wires the Cart, Auth, and Notifications services together with components:


Setup

ng new shop-app --style=css
cd shop-app

ng g s cart --skip-tests
ng g s auth --skip-tests
ng g s notifications --skip-tests
ng g c navbar --skip-tests
ng g c product-list --skip-tests
ng g c cart-page --skip-tests
ng g c toast-container --skip-tests
ng g c login --skip-tests

Use the service and component code from the chapters above. Then wire it in app.ts:

src/app/app.ts:

import { Component, inject } from '@angular/core';
import { Auth } from './auth/auth';
import { Navbar } from './navbar/navbar';
import { ProductList } from './product-list/product-list';
import { CartPage } from './cart-page/cart-page';
import { ToastContainer } from './toast-container/toast-container';
import { Login } from './login/login';

@Component({
  selector: 'app-root',
  imports: [Navbar, ProductList, CartPage, ToastContainer, Login],
  templateUrl: './app.html',
  styleUrl: './app.css'
})
export class App {

  authService = inject(Auth);

  isLoggedIn = this.authService.isLoggedIn;
  showCart: boolean = false;

  toggleCart(): void {
    this.showCart = !this.showCart;
  }
}

src/app/app.html:

<app-toast-container></app-toast-container>

@if (isLoggedIn()) {
  <app-navbar></app-navbar>

  @if (showCart()) {
    <app-cart-page></app-cart-page>
  } @else {
    <app-product-list></app-product-list>
  }
} @else {
  <app-login></app-login>
}

Now build the Login component:

src/app/login/login.ts:

import { Component, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Auth } from '../auth/auth';
import { Notifications } from '../notifications/notifications';

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

  private authService = inject(Auth);
  private notifications = inject(Notifications);

  error = this.authService.error;
  loading = this.authService.loading;

  email: string = '';
  password: string = '';

  onLogin(): void {
    const success = this.authService.login(this.email, this.password);

    if (success) {
      this.notifications.success(`Welcome back, ${this.authService.userName()}!`);
    } else {
      this.notifications.error('Login failed. Check your credentials.');
    }
  }
}

src/app/login/login.html:

<div class="login-page">
  <div class="login-card">
    <h1>Welcome Back</h1>
    <p class="subtitle">Sign in to your account</p>

    @if (error()) {
      <div class="error-banner">{{ error() }}</div>
    }

    <div class="field">
      <label>Email</label>
      <input
        type="email"
        [(ngModel)]="email"
        placeholder="rahul@example.com">
    </div>

    <div class="field">
      <label>Password</label>
      <input
        type="password"
        [(ngModel)]="password"
        (keyup.enter)="onLogin()">
    </div>

    <button
      class="login-btn"
      (click)="onLogin()"
      [disabled]="loading()">
      {{ loading() ? 'Signing in...' : 'Sign In' }}
    </button>

    <p class="hint">Use: rahul@example.com / password123</p>
  </div>
</div>

src/app/login/login.css:

.login-page {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #f0f2f5;
}

.login-card {
  background: white;
  padding: 40px;
  border-radius: 16px;
  box-shadow: 0 4px 24px rgba(0,0,0,0.1);
  width: 100%;
  max-width: 400px;
}

h1 {
  font-size: 28px;
  color: #1a1a2e;
  margin-bottom: 8px;
}

.subtitle {
  color: #888;
  margin-bottom: 28px;
  font-size: 15px;
}

.error-banner {
  background: #f8d7da;
  color: #721c24;
  padding: 12px 16px;
  border-radius: 8px;
  margin-bottom: 20px;
  font-size: 14px;
}

.field {
  margin-bottom: 18px;
}

label {
  display: block;
  font-size: 13px;
  font-weight: 600;
  color: #555;
  margin-bottom: 6px;
}

input {
  width: 100%;
  padding: 11px 14px;
  border: 1px solid #ddd;
  border-radius: 8px;
  font-size: 15px;
  transition: border-color 0.2s;
  box-sizing: border-box;
}

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

.login-btn {
  width: 100%;
  padding: 13px;
  background: #0070f3;
  color: white;
  border: none;
  border-radius: 8px;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  margin-top: 8px;
  transition: background 0.2s;
}

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

.hint {
  text-align: center;
  color: #999;
  font-size: 13px;
  margin-top: 16px;
}

Now update the Navbar to show the user and a logout button:

src/app/navbar/navbar.ts:

import { Component, inject, Output, EventEmitter } from '@angular/core';
import { Auth } from '../auth/auth';
import { Cart } from '../cart/cart';
import { Notifications } from '../notifications/notifications';

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

  private authService = inject(Auth);
  private cartService = inject(Cart);
  private notifications = inject(Notifications);

  userName = this.authService.userName;
  isAdmin = this.authService.isAdmin;
  totalItems = this.cartService.totalItems;

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

  logout(): void {
    const name = this.userName();
    this.authService.logout();
    this.notifications.info(`Goodbye, ${name}!`);
  }

  onCartClick(): void {
    this.cartToggled.emit();
  }
}

src/app/navbar/navbar.html:

<nav>
  <div class="logo">ShopApp</div>

  <div class="nav-right">
    @if (isAdmin()) {
      <span class="admin-badge">Admin</span>
    }

    <span class="user-name">{{ userName() }}</span>

    <button class="cart-btn" (click)="onCartClick()">
      🛒
      @if (totalItems() > 0) {
        <span class="badge">{{ totalItems() }}</span>
      }
    </button>

    <button class="logout-btn" (click)="logout()">Logout</button>
  </div>
</nav>

src/app/navbar/navbar.css:

nav {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px 40px;
  background: #0f0f23;
  position: sticky;
  top: 0;
  z-index: 100;
}

.logo {
  font-size: 20px;
  font-weight: 700;
  color: #64ffda;
}

.nav-right {
  display: flex;
  align-items: center;
  gap: 16px;
}

.admin-badge {
  background: #e94560;
  color: white;
  padding: 3px 10px;
  border-radius: 20px;
  font-size: 12px;
  font-weight: 600;
}

.user-name {
  color: #ccd6f6;
  font-size: 14px;
}

.cart-btn {
  position: relative;
  background: none;
  border: none;
  font-size: 22px;
  cursor: pointer;
  padding: 4px;
}

.badge {
  position: absolute;
  top: -6px;
  right: -6px;
  background: #e94560;
  color: white;
  border-radius: 50%;
  width: 18px;
  height: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  font-weight: 700;
}

.logout-btn {
  background: none;
  border: 1px solid #8892b0;
  color: #8892b0;
  padding: 6px 14px;
  border-radius: 6px;
  cursor: pointer;
  font-size: 13px;
  transition: all 0.2s;
}

.logout-btn:hover {
  border-color: #64ffda;
  color: #64ffda;
}

And update app.html to handle the cart toggle from the navbar:

<app-toast-container></app-toast-container>

@if (isLoggedIn()) {
  <app-navbar (cartToggled)="toggleCart()"></app-navbar>

  @if (showCart) {
    <app-cart-page></app-cart-page>
  } @else {
    <app-product-list></app-product-list>
  }
} @else {
  <app-login></app-login>
}

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 working shop application where:

The Auth service manages login state. When you log in, the entire app switches from showing Login to showing Navbar + ProductList. The Cart service tracks items. Adding a product updates the cart badge in Navbar and the cart page — without the two components ever talking to each other. The Notifications service shows toasts from any component — login success, cart actions, logout. All three services share data across multiple components using signals. One service instance. One source of truth. Everything in sync automatically.


Phase 5 — Complete Summary

Here is everything you learned in this phase:

Why services exist — components should focus on UI. Shared data, business logic, and API calls belong in services. If more than one component needs something, it belongs in a service.

Creating a serviceng g s name generates a service. The @Injectable decorator makes the class injectable. providedIn: 'root' registers it at the application level so one shared instance is available everywhere.

inject() function — the modern way to inject a service. private cartService = inject(Cart) gives you a reference to the service. Cleaner than constructor injection and works in more contexts.

Constructor injection — the classic way. constructor(private cartService: Cart) {} — both approaches produce identical results.

Singleton pattern — with providedIn: 'root', Angular creates one instance and shares it everywhere. Every component that injects the same service gets the exact same instance with the same data.

Component-level providers — adding a service to a component's providers array gives that component its own isolated instance. Useful for components that need independent state.

Services injecting services — services can inject other services using inject(). Complex business logic is composed from multiple focused services.

Signals in services — private signals with public readonly access is the correct pattern. Components can read service signals but only the service's own methods can modify them. computed() creates derived values that update automatically. effect() runs side effects when signals change.

Services as state management — a service with signals is a simple, powerful state management solution. Multiple components reading the same service signals stay in sync automatically without any direct communication between them.


What's Next — Phase 6

In Phase 6 we cover Routing and Navigation — how Angular handles multiple pages in a Single Page Application:

How to define routes and map URLs to components. The <router-outlet> where page components appear. Navigation with RouterLink and the Router service. Route parameters — passing IDs and data through the URL. Nested routes and child routing. Route guards — protecting pages from unauthorized access. Lazy loading — only loading a component's code when the user actually navigates to it.


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 5 — Services & Dependency Injection

Chapter 1 — The Problem Services Solve 1.1 — When Components Are Not Enough So far everything you have built has lived inside components. Th...