Phase 6 — Routing & Navigation

Chapter 1 — What is Routing and Why Does It Exist?


1.1 — The Problem with a Single Page

Remember from Phase 2 — Angular is a Single Page Application. There is only one HTML file. Everything renders inside <app-root>.

This is great for performance. But it creates a problem.

Real applications have multiple pages. A shop has a homepage, a product listing page, a product detail page, a cart page, a checkout page, a login page. A blog has a home, individual post pages, an about page, an author page.

If everything is one page and Angular just swaps components in and out, how does the user:

  • Share a link to a specific product?
  • Bookmark a page they want to come back to?
  • Press the browser back button and go to the previous page?
  • Refresh the page and land on the same page they were on?

Without routing, none of this works. The user cannot share a URL to a specific page because there is only one URL — your app's root URL. Refreshing always takes them back to the homepage. The back button does not work.

Routing solves all of this. Angular's router watches the URL in the browser's address bar. When the URL changes, the router figures out which component should be shown and renders it. When Angular navigates, it updates the URL — so the user can share it, bookmark it, refresh, and use the back button — all exactly like a traditional website, but without any full page reloads.


1.2 — How Angular Routing Works — The Mental Model

Think of your app's URL as an address. Each address maps to a specific component.

URL                        Component Shown
────────────────────       ──────────────────
/                    →     Home
/products            →     ProductList
/products/42         →     ProductDetail  (for product with id 42)
/cart                →     Cart
/login               →     Login
/dashboard           →     Dashboard
/dashboard/settings  →     Settings (nested inside Dashboard)

The Angular router reads the current URL, finds the matching route definition, and renders the corresponding component inside a <router-outlet> tag in your template.

<router-outlet> is just a placeholder — a slot in your template that says "render the matched component here." Your Navbar and Footer stay on screen all the time, but the content between them changes as the user navigates.

┌───────────────────────────────────┐
│            NavbarComponent        │  ← always visible
├───────────────────────────────────┤
│                                   │
│         <router-outlet>           │  ← changes based on URL
│   (Home / Products / Cart / etc.) │
│                                   │
├───────────────────────────────────┤
│            FooterComponent        │  ← always visible
└───────────────────────────────────┘

Chapter 2 — Setting Up Routes


2.1 — Where Routes Are Defined

In Angular, all routes are defined in src/app/app.routes.ts. You saw this file in Phase 2 — it was empty then. Now we fill it.

import { Routes } from '@angular/router';

export const routes: Routes = [];

Routes is just a TypeScript type — it is an array of route objects. Each route object maps a URL path to a component.


2.2 — Your First Routes

Let's set up a basic app with four pages. First generate the page components:

ng g c pages/home --skip-tests
ng g c pages/products --skip-tests
ng g c pages/about --skip-tests
ng g c pages/not-found --skip-tests

It is a common convention to put page-level components in a pages/ folder to distinguish them from smaller reusable components.

Now define the routes in app.routes.ts:

import { Routes } from '@angular/router';
import { Home } from './pages/home/home';
import { Products } from './pages/products/products';
import { About } from './pages/about/about';
import { NotFound } from './pages/not-found/not-found';

export const routes: Routes = [
  {
    path: '',
    component: Home
  },
  {
    path: 'products',
    component: Products
  },
  {
    path: 'about',
    component: About
  },
  {
    path: '**',
    component: NotFound
  }
];

Each route object has two required properties:

path — the URL segment that triggers this route. An empty string '' means the root URL — when the user is at yourapp.com/. The string 'products' means yourapp.com/products.

component — the component to render when this path is matched.

The '**' path is called a wildcard route. It matches anything that did not match any previous route. Put this last always — Angular matches routes from top to bottom and stops at the first match. If you put ** first, everything would match it.


2.3 — The Router Outlet — Where Pages Render

In your app.html, you need to have <router-outlet> — this is where the matched component gets rendered:

src/app/app.html:

<app-navbar></app-navbar>

<main>
  <router-outlet></router-outlet>
</main>

<app-footer></app-footer>

When the URL is /products, Angular finds the matching route, takes the Products component, and renders it right where <router-outlet> is. The Navbar and Footer stay untouched.

In app.ts, make sure RouterOutlet is imported:

import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { Navbar } from './navbar/navbar';
import { Footer } from './footer/footer';

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

2.4 — Redirects

Sometimes you want a URL to automatically redirect to another URL. For example, redirecting /home to /:

export const routes: Routes = [
  {
    path: '',
    component: Home
  },
  {
    path: 'home',
    redirectTo: '',        // redirect /home to /
    pathMatch: 'full'     // the full path must match 'home' exactly
  },
  {
    path: 'products',
    component: Products
  },
  {
    path: '**',
    redirectTo: ''         // redirect unknown URLs to home
  }
];

pathMatch: 'full' tells Angular to only redirect if the entire URL is exactly 'home', not if 'home' just appears somewhere in the URL. For empty path redirects (path: ''), always use pathMatch: 'full' to prevent unexpected matches.


Chapter 3 — Navigation in Templates


3.1 — RouterLink — Never Use href for Internal Navigation

In a regular HTML website, you navigate between pages using <a href="/about">. In Angular, you must NOT use href for internal navigation. If you use href, the browser performs a full page reload — it sends a request to the server, Angular boots up from scratch, all your state is lost. It defeats the entire purpose of a SPA.

Instead, Angular gives you the RouterLink directive. It looks like a regular link but it tells Angular's router to navigate without reloading the page:

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

@Component({
  selector: 'app-navbar',
  imports: [RouterLink],    // ← must import RouterLink
  templateUrl: './navbar.html',
  styleUrl: './navbar.css'
})
export class Navbar { }
<nav>
  <div class="logo">MyApp</div>
  <ul>
    <li><a routerLink="/">Home</a></li>
    <li><a routerLink="/products">Products</a></li>
    <li><a routerLink="/about">About</a></li>
  </ul>
</nav>

routerLink="/" — navigates to the home page. routerLink="/products" — navigates to the products page.

When the user clicks these links, Angular intercepts the click, updates the URL in the address bar, and renders the matching component. No page reload. Instant.


3.2 — RouterLinkActive — Highlighting the Active Link

You almost always want to highlight the navigation link for the currently active page. RouterLinkActive adds a CSS class to a link when its route is currently active:

import { Component } from '@angular/core';
import { RouterLink, RouterLinkActive } from '@angular/router';

@Component({
  selector: 'app-navbar',
  imports: [RouterLink, RouterLinkActive],
  templateUrl: './navbar.html',
  styleUrl: './navbar.css'
})
export class Navbar { }
<nav>
  <ul>
    <li>
      <a routerLink="/"
         routerLinkActive="active"
         [routerLinkActiveOptions]="{ exact: true }">
        Home
      </a>
    </li>
    <li>
      <a routerLink="/products"
         routerLinkActive="active">
        Products
      </a>
    </li>
    <li>
      <a routerLink="/about"
         routerLinkActive="active">
        About
      </a>
    </li>
  </ul>
</nav>

routerLinkActive="active" — Angular adds the class active to this element whenever the current URL matches the routerLink of this element. You define what active looks like in your CSS.

[routerLinkActiveOptions]="{ exact: true }" — this is important for the home route /. Without exact: true, the / link would be highlighted as active on every page because every URL starts with /. With exact: true, it only activates when the URL is exactly /.

/* navbar.css */
a {
  color: #ccd6f6;
  text-decoration: none;
  padding: 6px 12px;
  border-radius: 6px;
  transition: all 0.2s;
}

a.active {
  color: #64ffda;
  background: rgba(100, 255, 218, 0.1);
}

3.3 — Dynamic RouterLink with Property Binding

When you need to build the link dynamically — for example, linking to a specific product — use property binding:

<!-- Link to a specific product page -->
<a [routerLink]="['/products', product.id]">{{ product.name }}</a>
<!-- Generates: /products/42 -->

<!-- Link with query params -->
<a [routerLink]="['/products']" [queryParams]="{ category: 'electronics', sort: 'price' }">
  Electronics
</a>
<!-- Generates: /products?category=electronics&sort=price -->

The [routerLink] accepts an array where each element is a URL segment. ['/products', product.id] generates /products/42 when product.id is 42.


Chapter 4 — Programmatic Navigation


4.1 — Navigating from TypeScript Code

Sometimes you need to navigate in your TypeScript code — not from a link click. For example, after a form is submitted successfully, you want to redirect the user to a different page. After a successful login, redirect to the dashboard.

For this you use the Router service:

import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
import { FormsModule } from '@angular/forms';

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

  private router = inject(Router);

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

  onLogin(): void {
    // Pretend we validated the login here
    const loginSuccessful = true;

    if (loginSuccessful) {
      // Navigate to dashboard after successful login
      this.router.navigate(['/dashboard']);
    }
  }

  goToRegister(): void {
    this.router.navigate(['/register']);
  }

  goToProductDetail(productId: number): void {
    this.router.navigate(['/products', productId]);
  }
}

this.router.navigate(['/dashboard']) — navigates to /dashboard. The navigate method takes an array of URL segments, just like [routerLink].


4.2 — Navigate with Query Params

// Navigate to /products?category=electronics&sort=price
this.router.navigate(['/products'], {
  queryParams: { category: 'electronics', sort: 'price' }
});

4.3 — navigateByUrl

navigateByUrl is an alternative that takes a complete URL string instead of an array:

// Navigate using a full URL string
this.router.navigateByUrl('/products?category=electronics');

// Navigate to home
this.router.navigateByUrl('/');

The difference between navigate and navigateByUrl is that navigate builds the URL from segments (array-based) and navigateByUrl takes the URL directly as a string. Both work fine. Use navigate when you are building URLs from dynamic values. Use navigateByUrl when you already have the complete URL string.


Chapter 5 — Route Parameters


5.1 — What Are Route Parameters?

A route parameter is a dynamic part of the URL that changes based on what the user is viewing. Instead of creating a separate route for every product like /products/keyboard, /products/mouse, /products/monitor — you create one route with a parameter: /products/:id.

The :id is a placeholder. When the URL is /products/42, Angular puts 42 into the id parameter. When the URL is /products/99, Angular puts 99 into the id parameter. Same route, same component, different data.


5.2 — Defining a Route with Parameters

// app.routes.ts
import { Routes } from '@angular/router';
import { Home } from './pages/home/home';
import { Products } from './pages/products/products';
import { ProductDetail } from './pages/product-detail/product-detail';

export const routes: Routes = [
  { path: '', component: Home },
  { path: 'products', component: Products },
  { path: 'products/:id', component: ProductDetail },  // ← :id is a parameter
  { path: '**', redirectTo: '' }
];

Now /products/1 renders ProductDetail. /products/99 also renders ProductDetail. The component reads the id from the URL to know which product to display.


5.3 — Reading Route Parameters in the Component

To read the current URL's parameters, inject ActivatedRoute:

src/app/pages/product-detail/product-detail.ts:

import { Component, inject, OnInit, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { RouterLink } from '@angular/router';

interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
  category: string;
  rating: number;
}

@Component({
  selector: 'app-product-detail',
  imports: [RouterLink],
  templateUrl: './product-detail.html',
  styleUrl: './product-detail.css'
})
export class ProductDetail implements OnInit {

  private route = inject(ActivatedRoute);

  product = signal<Product | null>(null);
  isLoading = signal<boolean>(true);
  error = signal<string>('');

  // Simulated product database
  private allProducts: Product[] = [
    { id: 1, name: 'Mechanical Keyboard', price: 8500, description: 'Premium tactile feedback keyboard with RGB lighting.', category: 'Electronics', rating: 4.8 },
    { id: 2, name: 'Wireless Mouse', price: 2200, description: 'Ergonomic wireless mouse with 3 months battery life.', category: 'Electronics', rating: 4.5 },
    { id: 3, name: 'Monitor Stand', price: 3400, description: 'Adjustable aluminum monitor stand with cable management.', category: 'Accessories', rating: 4.7 },
    { id: 4, name: 'USB-C Hub', price: 1800, description: '7-in-1 USB-C hub with 4K HDMI, USB 3.0, and PD charging.', category: 'Electronics', rating: 4.3 }
  ];

  ngOnInit(): void {
    // Read the :id parameter from the current URL
    const idParam = this.route.snapshot.params['id'];
    const productId = Number(idParam);

    // Find the product with this ID
    const found = this.allProducts.find(p => p.id === productId);

    if (found) {
      this.product.set(found);
    } else {
      this.error.set(`Product with ID ${productId} was not found.`);
    }

    this.isLoading.set(false);
  }
}

ActivatedRoute is a service that gives you information about the currently active route. this.route.snapshot.params['id'] reads the id parameter from the URL at the moment the component loads.

snapshot means "the current state of the route right now." It is a one-time read. This is perfect when the component is loaded fresh every time the user navigates to it.

this.route.snapshot.params['id'] always returns a string — even if the URL has a number in it. URL parameters are always strings. That is why we convert it with Number(idParam).

src/app/pages/product-detail/product-detail.html:

<div class="product-detail">

  @if (isLoading()) {
    <div class="loading">Loading product...</div>
  }

  @if (error()) {
    <div class="error">
      <p>{{ error() }}</p>
      <a routerLink="/products">← Back to Products</a>
    </div>
  }

  @if (product()) {
    <div class="detail-card">
      <a routerLink="/products" class="back-link">← Back to Products</a>

      <div class="product-header">
        <span class="category">{{ product()!.category }}</span>
        <h1>{{ product()!.name }}</h1>
        <div class="rating">★ {{ product()!.rating }} / 5</div>
      </div>

      <p class="description">{{ product()!.description }}</p>

      <div class="price-section">
        <span class="price">₹{{ product()!.price.toLocaleString() }}</span>
        <button class="add-to-cart">Add to Cart</button>
      </div>
    </div>
  }

</div>

5.4 — Subscribing to Route Parameters for Same-Component Navigation

snapshot works perfectly when the user navigates to the component fresh. But there is a specific situation where it fails — when the user is already on a product detail page and navigates to a different product.

For example, if you are on /products/1 and click "Next Product" which takes you to /products/2 — Angular might reuse the same component instance instead of destroying and recreating it. In that case, ngOnInit does not run again, so snapshot is still reading the old ID.

To handle this, subscribe to route.params as an Observable:

import { Component, inject, OnInit, OnDestroy, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';

@Component({
  selector: 'app-product-detail',
  imports: [],
  templateUrl: './product-detail.html',
  styleUrl: './product-detail.css'
})
export class ProductDetail implements OnInit, OnDestroy {

  private route = inject(ActivatedRoute);
  private paramSubscription?: Subscription;

  productId = signal<number>(0);

  ngOnInit(): void {
    // Subscribe to params — this fires every time the param changes
    this.paramSubscription = this.route.params.subscribe(params => {
      const id = Number(params['id']);
      this.productId.set(id);
      this.loadProduct(id);
    });
  }

  loadProduct(id: number): void {
    console.log('Loading product with ID:', id);
    // Load the product data here
  }

  ngOnDestroy(): void {
    // Always unsubscribe to prevent memory leaks
    this.paramSubscription?.unsubscribe();
  }
}

this.route.params is an Observable that emits a new value every time the route parameters change. By subscribing to it, your component reacts every time the ID in the URL changes — even if the component instance is reused.

We cover Observables deeply in Phase 9. For now just know: snapshot for simple cases, subscribe to route.params when the same component can be navigated to with different parameters while it is already active.


5.5 — Query Parameters

Query parameters are the ?key=value parts of a URL. Unlike route parameters which are part of the URL path, query parameters are optional extras that provide additional context.

/products?category=electronics&sort=price&page=2

Reading query parameters:

import { Component, inject, OnInit, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-products',
  imports: [],
  templateUrl: './products.html',
  styleUrl: './products.css'
})
export class Products implements OnInit {

  private route = inject(ActivatedRoute);

  category = signal<string>('all');
  sortBy = signal<string>('name');
  currentPage = signal<number>(1);

  ngOnInit(): void {
    // Read query params from the URL
    const params = this.route.snapshot.queryParams;

    this.category.set(params['category'] || 'all');
    this.sortBy.set(params['sort'] || 'name');
    this.currentPage.set(Number(params['page']) || 1);

    console.log(`Showing category: ${this.category()}, sorted by: ${this.sortBy()}`);
  }
}

Setting query parameters when navigating:

// In TypeScript
this.router.navigate(['/products'], {
  queryParams: {
    category: 'electronics',
    sort: 'price',
    page: 1
  }
});
<!-- In template -->
<a [routerLink]="['/products']"
   [queryParams]="{ category: 'electronics', sort: 'price' }">
  Electronics
</a>

Chapter 6 — Nested Routes


6.1 — What Are Nested Routes?

Nested routes are routes that render components inside other components — not just in the root <router-outlet>, but inside a <router-outlet> that lives inside another routed component.

This is useful for dashboard-style layouts where the outer shell (sidebar, header) stays the same but the inner content changes:

/dashboard              → Dashboard shell
/dashboard/overview     → Dashboard overview (inside dashboard)
/dashboard/stats        → Dashboard stats (inside dashboard)
/dashboard/settings     → Dashboard settings (inside dashboard)

The Dashboard component renders once. Inside it, there is a second <router-outlet>. As the user navigates between /dashboard/overview, /dashboard/stats, and /dashboard/settings, only the inner content changes.


6.2 — Setting Up Nested Routes

// app.routes.ts
import { Routes } from '@angular/router';
import { Home } from './pages/home/home';
import { Dashboard } from './pages/dashboard/dashboard';
import { Overview } from './pages/dashboard/overview/overview';
import { Stats } from './pages/dashboard/stats/stats';
import { Settings } from './pages/dashboard/settings/settings';

export const routes: Routes = [
  { path: '', component: Home },
  {
    path: 'dashboard',
    component: Dashboard,
    children: [                              // ← children array for nested routes
      { path: '', redirectTo: 'overview', pathMatch: 'full' },
      { path: 'overview', component: Overview },
      { path: 'stats', component: Stats },
      { path: 'settings', component: Settings }
    ]
  }
];

The children array defines routes that render inside the Dashboard component. When the URL is /dashboard/overview, Angular renders Dashboard in the root outlet AND renders Overview inside Dashboard's own outlet.


6.3 — The Parent Component with Its Own Router Outlet

The Dashboard component needs its own <router-outlet> where child components will render:

src/app/pages/dashboard/dashboard.ts:

import { Component, inject } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { Auth } from '../../auth/auth';

@Component({
  selector: 'app-dashboard',
  imports: [RouterLink, RouterLinkActive, RouterOutlet],
  templateUrl: './dashboard.html',
  styleUrl: './dashboard.css'
})
export class Dashboard {

  authService = inject(Auth);
  userName = this.authService.userName;
}

src/app/pages/dashboard/dashboard.html:

<div class="dashboard-layout">

  <aside class="sidebar">
    <div class="sidebar-header">
      <h2>Dashboard</h2>
      <p>{{ userName() }}</p>
    </div>

    <nav class="sidebar-nav">
      <a routerLink="overview" routerLinkActive="active">
        📊 Overview
      </a>
      <a routerLink="stats" routerLinkActive="active">
        📈 Statistics
      </a>
      <a routerLink="settings" routerLinkActive="active">
        ⚙️ Settings
      </a>
    </nav>
  </aside>

  <main class="dashboard-content">
    <router-outlet></router-outlet>
    <!-- Child routes render here -->
  </main>

</div>

Notice the routerLink values inside the dashboard are relative — routerLink="overview" not routerLink="/dashboard/overview". When you are inside a routed component and use a routerLink without a leading /, Angular treats it as relative to the current route. So overview becomes /dashboard/overview automatically.

src/app/pages/dashboard/dashboard.css:

.dashboard-layout {
  display: flex;
  min-height: calc(100vh - 60px);
}

.sidebar {
  width: 240px;
  background: #1a1a2e;
  padding: 24px 0;
  flex-shrink: 0;
}

.sidebar-header {
  padding: 0 24px 24px;
  border-bottom: 1px solid #233554;
  margin-bottom: 16px;
}

.sidebar-header h2 {
  color: #64ffda;
  font-size: 18px;
  margin-bottom: 4px;
}

.sidebar-header p {
  color: #8892b0;
  font-size: 13px;
}

.sidebar-nav {
  display: flex;
  flex-direction: column;
  padding: 0 12px;
  gap: 4px;
}

.sidebar-nav a {
  display: block;
  padding: 10px 12px;
  color: #8892b0;
  text-decoration: none;
  border-radius: 8px;
  font-size: 14px;
  transition: all 0.2s;
}

.sidebar-nav a:hover {
  color: #ccd6f6;
  background: rgba(255,255,255,0.05);
}

.sidebar-nav a.active {
  color: #64ffda;
  background: rgba(100, 255, 218, 0.1);
}

.dashboard-content {
  flex: 1;
  padding: 32px;
  background: #f0f2f5;
  overflow-y: auto;
}

Now build the three child page components:

src/app/pages/dashboard/overview/overview.ts:

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

@Component({
  selector: 'app-overview',
  imports: [],
  template: `
    <div class="page">
      <h1>Overview</h1>

      <div class="stats-grid">
        <div class="stat-card">
          <span class="stat-value">1,284</span>
          <span class="stat-label">Total Users</span>
        </div>
        <div class="stat-card">
          <span class="stat-value">₹2,40,000</span>
          <span class="stat-label">Revenue</span>
        </div>
        <div class="stat-card">
          <span class="stat-value">342</span>
          <span class="stat-label">Orders</span>
        </div>
        <div class="stat-card">
          <span class="stat-value">98%</span>
          <span class="stat-label">Uptime</span>
        </div>
      </div>
    </div>
  `,
  styles: [`
    .page h1 { font-size: 28px; color: #1a1a2e; margin-bottom: 28px; }
    .stats-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
      gap: 20px;
    }
    .stat-card {
      background: white;
      padding: 24px;
      border-radius: 12px;
      box-shadow: 0 2px 8px rgba(0,0,0,0.06);
      display: flex;
      flex-direction: column;
      gap: 8px;
    }
    .stat-value { font-size: 32px; font-weight: 700; color: #0070f3; }
    .stat-label { font-size: 13px; color: #888; text-transform: uppercase; letter-spacing: 0.5px; }
  `]
})
export class Overview { }

src/app/pages/dashboard/stats/stats.ts:

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

@Component({
  selector: 'app-stats',
  imports: [],
  template: `
    <div class="page">
      <h1>Statistics</h1>
      <p style="color:#666; margin-top:12px">Detailed statistics and charts would appear here.</p>
    </div>
  `,
  styles: [`.page h1 { font-size: 28px; color: #1a1a2e; }`]
})
export class Stats { }

src/app/pages/dashboard/settings/settings.ts:

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

@Component({
  selector: 'app-settings',
  imports: [],
  template: `
    <div class="page">
      <h1>Settings</h1>
      <p style="color:#666; margin-top:12px">User and application settings would appear here.</p>
    </div>
  `,
  styles: [`.page h1 { font-size: 28px; color: #1a1a2e; }`]
})
export class Settings { }

Chapter 7 — Route Guards


7.1 — What is a Route Guard?

A route guard is a function that runs before a route is activated. It decides whether the navigation should proceed or be blocked.

The most common use case is protecting routes from unauthorized access. You do not want unauthenticated users reaching the /dashboard. You do not want regular users reaching /admin. Guards check the condition and either let the navigation proceed or redirect the user somewhere else.

Angular has several types of guards:

canActivate — can the user enter this route? canActivateChild — can the user enter any child routes? canDeactivate — can the user leave this route? (useful for unsaved changes warnings) canMatch — should this route even be considered for matching? resolve — fetch data before the component loads

We will cover the two most common: canActivate and canDeactivate.


7.2 — Creating a canActivate Guard

Generate a guard:

ng generate guard guards/auth --skip-tests

The CLI asks what interfaces you want to implement. Select CanActivate.

This creates src/app/guards/auth.ts:

import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { Auth } from '../auth/auth';

export const authGuard: CanActivateFn = (route, state) => {
  const authService = inject(Auth);
  const router = inject(Router);

  if (authService.isLoggedIn()) {
    return true;   // allow navigation to proceed
  }

  // User is not logged in — redirect to login page
  router.navigate(['/login'], {
    queryParams: { returnUrl: state.url }   // remember where they were trying to go
  });

  return false;    // block navigation
};

In modern Angular, guards are just functions — CanActivateFn. The function receives the route being activated (route) and the current router state (state). It returns either true (allow navigation) or false (block navigation), or it can redirect by calling router.navigate() and returning false.

The returnUrl query param is a nice UX touch — after the user logs in, you can redirect them back to the page they were trying to reach.

state.url is the URL the user was trying to navigate to. Storing it as a query param means your login component can read it and redirect after successful login:

// In the login component, after successful login:
const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/dashboard';
this.router.navigateByUrl(returnUrl);

7.3 — Applying a Guard to Routes

Add the guard to any route using canActivate:

// app.routes.ts
import { authGuard } from './guards/auth';

export const routes: Routes = [
  { path: '', component: Home },
  { path: 'login', component: Login },
  { path: 'products', component: Products },
  {
    path: 'dashboard',
    component: Dashboard,
    canActivate: [authGuard],       // ← protected by auth guard
    children: [
      { path: '', redirectTo: 'overview', pathMatch: 'full' },
      { path: 'overview', component: Overview },
      { path: 'stats', component: Stats },
      { path: 'settings', component: Settings }
    ]
  },
  { path: '**', redirectTo: '' }
];

Now if an unauthenticated user tries to go to /dashboard, the guard runs, finds they are not logged in, redirects them to /login?returnUrl=/dashboard, and blocks the navigation.


7.4 — canActivateChild — Protecting All Child Routes

Instead of adding canActivate to every child route individually, you can use canActivateChild on the parent to protect all children at once:

import { adminGuard } from './guards/admin';

export const routes: Routes = [
  {
    path: 'admin',
    component: AdminLayout,
    canActivateChild: [adminGuard],   // ← all children are protected
    children: [
      { path: 'users', component: UserManagement },
      { path: 'products', component: ProductManagement },
      { path: 'orders', component: OrderManagement }
    ]
  }
];

And the admin guard:

src/app/guards/admin.ts:

import { inject } from '@angular/core';
import { CanActivateChildFn, Router } from '@angular/router';
import { Auth } from '../auth/auth';

export const adminGuard: CanActivateChildFn = (route, state) => {
  const authService = inject(Auth);
  const router = inject(Router);

  if (authService.isLoggedIn() && authService.isAdmin()) {
    return true;
  }

  if (!authService.isLoggedIn()) {
    router.navigate(['/login']);
    return false;
  }

  // Logged in but not admin
  router.navigate(['/dashboard']);
  return false;
};

7.5 — canDeactivate — Preventing Accidental Navigation Away

This guard protects users from accidentally leaving a page with unsaved changes. You know that dialog that says "You have unsaved changes. Are you sure you want to leave?" — that is a canDeactivate guard.

First create the guard:

src/app/guards/unsaved-changes.ts:

import { CanDeactivateFn } from '@angular/router';

// Define an interface that components must implement to use this guard
export interface HasUnsavedChanges {
  hasUnsavedChanges(): boolean;
}

export const unsavedChangesGuard: CanDeactivateFn<HasUnsavedChanges> = (component) => {
  if (component.hasUnsavedChanges()) {
    return confirm('You have unsaved changes. Are you sure you want to leave?');
  }
  return true;
};

Then implement the interface in your component:

src/app/pages/edit-profile/edit-profile.ts:

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HasUnsavedChanges } from '../../guards/unsaved-changes';

@Component({
  selector: 'app-edit-profile',
  imports: [FormsModule],
  templateUrl: './edit-profile.html',
  styleUrl: './edit-profile.css'
})
export class EditProfile implements HasUnsavedChanges {

  originalName: string = 'Rahul Sharma';
  currentName: string = 'Rahul Sharma';
  isSaved: boolean = false;

  hasUnsavedChanges(): boolean {
    return this.currentName !== this.originalName && !this.isSaved;
  }

  saveChanges(): void {
    this.originalName = this.currentName;
    this.isSaved = true;
    console.log('Changes saved!');
  }
}

Apply it to the route:

import { unsavedChangesGuard } from './guards/unsaved-changes';

{ path: 'edit-profile', component: EditProfile, canDeactivate: [unsavedChangesGuard] }

Now if a user has typed something in the form and tries to navigate away, they get a confirmation dialog. If they click OK, navigation proceeds. If they click Cancel, they stay on the page.


Chapter 8 — Lazy Loading


8.1 — What is Lazy Loading and Why Does It Matter?

When you build an Angular app and run ng build, all your components get compiled into JavaScript files. By default, ALL of this JavaScript loads upfront when the user first opens your app — even parts they might never visit.

If your app has 50 components and the user only visits 10 of them in a session, they downloaded JavaScript for 40 components they never needed. This makes the initial load slower.

Lazy loading solves this. With lazy loading, each route's code is only downloaded when the user actually navigates to that route. The initial bundle is tiny. When the user navigates to /dashboard, Angular downloads the dashboard's code at that moment. Fast initial load. Code loads on demand.


8.2 — Setting Up Lazy Loading

Instead of importing the component at the top of app.routes.ts and passing it directly to component:, you use loadComponent with a dynamic import:

// app.routes.ts — WITH lazy loading
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: '',
    loadComponent: () => import('./pages/home/home').then(m => m.Home)
  },
  {
    path: 'products',
    loadComponent: () => import('./pages/products/products').then(m => m.Products)
  },
  {
    path: 'products/:id',
    loadComponent: () => import('./pages/product-detail/product-detail').then(m => m.ProductDetail)
  },
  {
    path: 'dashboard',
    canActivate: [authGuard],
    loadComponent: () => import('./pages/dashboard/dashboard').then(m => m.Dashboard),
    children: [
      { path: '', redirectTo: 'overview', pathMatch: 'full' },
      {
        path: 'overview',
        loadComponent: () => import('./pages/dashboard/overview/overview').then(m => m.Overview)
      },
      {
        path: 'stats',
        loadComponent: () => import('./pages/dashboard/stats/stats').then(m => m.Stats)
      },
      {
        path: 'settings',
        loadComponent: () => import('./pages/dashboard/settings/settings').then(m => m.Settings)
      }
    ]
  },
  {
    path: '**',
    loadComponent: () => import('./pages/not-found/not-found').then(m => m.NotFound)
  }
];

loadComponent takes a function that returns a dynamic import. import('./pages/home/home') is JavaScript's dynamic import — it only downloads that file when this function is called. The .then(m => m.Home) extracts the Home class from the module.

The first time a user navigates to /products, Angular downloads that component's code. The second time they go to /products, the code is already cached — no download needed.


8.3 — Loading State During Lazy Load

When a route is loading for the first time, there might be a brief delay while the JavaScript downloads. Angular lets you configure what to show during this loading:

In app.config.ts, add withRouterConfig to configure the router:

import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter, withRouterConfig } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(
      routes,
      withRouterConfig({ onSameUrlNavigation: 'reload' })
    )
  ]
};

For showing a loading indicator during route transitions, you listen to router events in your App component:

src/app/app.ts:

import { Component, inject, signal } from '@angular/core';
import { RouterOutlet, Router, NavigationStart, NavigationEnd, NavigationCancel, NavigationError, Event } from '@angular/router';
import { Navbar } from './navbar/navbar';

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

  private router = inject(Router);

  isNavigating = signal(false);

  constructor() {
    this.router.events.subscribe((event: Event) => {
      if (event instanceof NavigationStart) {
        this.isNavigating.set(true);
      }

      if (
        event instanceof NavigationEnd ||
        event instanceof NavigationCancel ||
        event instanceof NavigationError
      ) {
        this.isNavigating.set(false);
      }
    });
  }
}

src/app/app.html:

<app-navbar></app-navbar>

@if (isNavigating()) {
  <div class="page-loader">
    <div class="loader-bar"></div>
  </div>
}

<main>
  <router-outlet></router-outlet>
</main>

src/app/app.css:

.page-loader {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  z-index: 9999;
}

.loader-bar {
  height: 100%;
  background: #64ffda;
  animation: loading 1s ease-in-out infinite;
}

@keyframes loading {
  0% { width: 0%; }
  50% { width: 70%; }
  100% { width: 100%; }
}

Chapter 9 — Route Data and Extras


9.1 — Static Data on Routes

Sometimes you want to attach static data to a route — like a page title or breadcrumb label. You can do this with the data property:

export const routes: Routes = [
  {
    path: '',
    component: Home,
    data: { title: 'Home', breadcrumb: 'Home' }
  },
  {
    path: 'products',
    component: Products,
    data: { title: 'All Products', breadcrumb: 'Products' }
  },
  {
    path: 'dashboard',
    component: Dashboard,
    data: { title: 'Dashboard', requiresAuth: true }
  }
];

Reading it in the component:

import { Component, inject, OnInit, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-products',
  imports: [],
  templateUrl: './products.html',
  styleUrl: './products.css'
})
export class Products implements OnInit {

  private route = inject(ActivatedRoute);
  pageTitle = signal('');

  ngOnInit(): void {
    this.pageTitle.set(this.route.snapshot.data['title']);
    document.title = this.route.snapshot.data['title'] + ' | MyApp';
  }
}

Chapter 10 — Putting It All Together — A Complete Routed App


Let's build a Blog App that demonstrates everything from this phase. It will have a home page, a blog post listing page, individual post pages, a dashboard, and authentication with a guard.


Project Setup

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

ng g s services/auth --skip-tests
ng g g guards/auth --skip-tests
ng g c components/navbar --skip-tests
ng g c pages/home --skip-tests
ng g c pages/blog --skip-tests
ng g c pages/post-detail --skip-tests
ng g c pages/login --skip-tests
ng g c pages/not-found --skip-tests
ng g c pages/dashboard/dashboard --skip-tests
ng g c pages/dashboard/my-posts --skip-tests
ng g c pages/dashboard/write-post --skip-tests

Auth Service

src/app/services/auth.ts:

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

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

  private loggedIn = signal<boolean>(false);
  private username = signal<string>('');

  readonly isLoggedIn = this.loggedIn.asReadonly();
  readonly userName = this.username.asReadonly();
  readonly userInitial = computed(() => this.username() ? this.username()[0].toUpperCase() : '');

  login(name: string, password: string): boolean {
    if (password === 'password') {
      this.loggedIn.set(true);
      this.username.set(name);
      return true;
    }
    return false;
  }

  logout(): void {
    this.loggedIn.set(false);
    this.username.set('');
  }
}

Auth Guard

src/app/guards/auth.ts:

import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { Auth } from '../services/auth';

export const authGuard: CanActivateFn = (route, state) => {
  const auth = inject(Auth);
  const router = inject(Router);

  if (auth.isLoggedIn()) {
    return true;
  }

  router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
  return false;
};

Blog Data Service

src/app/services/blog.ts:

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

export interface Post {
  id: number;
  title: string;
  excerpt: string;
  content: string;
  author: string;
  date: string;
  category: string;
  readTime: number;
}

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

  private posts = signal<Post[]>([
    {
      id: 1,
      title: 'Getting Started with Angular 21',
      excerpt: 'A complete beginner guide to building your first Angular app from scratch.',
      content: 'Angular is a powerful framework for building web applications. In this guide we will cover everything from setup to deployment. We start by installing Node.js and the Angular CLI. Then we create our first project with ng new. The folder structure is clean and well-organized. Components are the building blocks of every Angular app...',
      author: 'Rahul Sharma',
      date: '2025-03-01',
      category: 'Angular',
      readTime: 8
    },
    {
      id: 2,
      title: 'Understanding TypeScript Generics',
      excerpt: 'Deep dive into TypeScript generics and how they make your code more reusable and type-safe.',
      content: 'TypeScript generics allow you to write functions, classes, and interfaces that work with a variety of types while maintaining type safety. The most basic generic is a function that returns the first item from an array...',
      author: 'Priya Patel',
      date: '2025-03-05',
      category: 'TypeScript',
      readTime: 6
    },
    {
      id: 3,
      title: 'RxJS Operators You Need to Know',
      excerpt: 'The most important RxJS operators explained clearly with practical examples.',
      content: 'RxJS is the reactive programming library that powers Angular. While it has dozens of operators, you only need to know a handful to be productive. The map operator transforms each emitted value. The filter operator only lets through values that match a condition...',
      author: 'Amit Kumar',
      date: '2025-03-10',
      category: 'RxJS',
      readTime: 10
    },
    {
      id: 4,
      title: 'Angular Services and Dependency Injection',
      excerpt: 'Learn how Angular DI works under the hood and how to use services effectively.',
      content: 'Dependency Injection is one of Angular\'s core features. It allows you to write loosely coupled code that is easy to test and maintain. A service is a class decorated with @Injectable that can be injected into any component...',
      author: 'Sneha Gupta',
      date: '2025-03-15',
      category: 'Angular',
      readTime: 7
    }
  ]);

  readonly allPosts = this.posts.asReadonly();

  getPostById(id: number): Post | undefined {
    return this.posts().find(p => p.id === id);
  }

  getPostsByCategory(category: string): Post[] {
    return this.posts().filter(p => p.category === category);
  }
}

Routes Configuration

src/app/app.routes.ts:

import { Routes } from '@angular/router';
import { authGuard } from './guards/auth';

export const routes: Routes = [
  {
    path: '',
    loadComponent: () => import('./pages/home/home').then(m => m.Home),
    data: { title: 'Home' }
  },
  {
    path: 'blog',
    loadComponent: () => import('./pages/blog/blog').then(m => m.Blog),
    data: { title: 'Blog' }
  },
  {
    path: 'blog/:id',
    loadComponent: () => import('./pages/post-detail/post-detail').then(m => m.PostDetail)
  },
  {
    path: 'login',
    loadComponent: () => import('./pages/login/login').then(m => m.Login)
  },
  {
    path: 'dashboard',
    canActivate: [authGuard],
    loadComponent: () => import('./pages/dashboard/dashboard').then(m => m.Dashboard),
    children: [
      { path: '', redirectTo: 'my-posts', pathMatch: 'full' },
      {
        path: 'my-posts',
        loadComponent: () => import('./pages/dashboard/my-posts/my-posts').then(m => m.MyPosts)
      },
      {
        path: 'write',
        loadComponent: () => import('./pages/dashboard/write-post/write-post').then(m => m.WritePost)
      }
    ]
  },
  {
    path: '**',
    loadComponent: () => import('./pages/not-found/not-found').then(m => m.NotFound)
  }
];

Navbar Component

src/app/components/navbar/navbar.ts:

import { Component, inject } from '@angular/core';
import { RouterLink, RouterLinkActive } from '@angular/router';
import { Auth } from '../../services/auth';

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

  authService = inject(Auth);

  isLoggedIn = this.authService.isLoggedIn;
  userName = this.authService.userName;

  logout(): void {
    this.authService.logout();
  }
}

src/app/components/navbar/navbar.html:

<nav>
  <a routerLink="/" class="logo">DevBlog</a>

  <div class="nav-links">
    <a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Home</a>
    <a routerLink="/blog" routerLinkActive="active">Blog</a>

    @if (isLoggedIn()) {
      <a routerLink="/dashboard" routerLinkActive="active">Dashboard</a>
      <span class="user-chip">{{ userName() }}</span>
      <button class="logout-btn" (click)="logout()">Logout</button>
    } @else {
      <a routerLink="/login" routerLinkActive="active" class="login-link">Login</a>
    }
  </div>
</nav>

src/app/components/navbar/navbar.css:

nav {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0 48px;
  height: 60px;
  background: white;
  border-bottom: 1px solid #e8e8e8;
  position: sticky;
  top: 0;
  z-index: 100;
}

.logo {
  font-size: 20px;
  font-weight: 800;
  color: #0070f3;
  text-decoration: none;
  letter-spacing: -0.5px;
}

.nav-links {
  display: flex;
  align-items: center;
  gap: 24px;
}

.nav-links a {
  color: #555;
  text-decoration: none;
  font-size: 15px;
  transition: color 0.2s;
}

.nav-links a:hover { color: #0070f3; }
.nav-links a.active { color: #0070f3; font-weight: 600; }

.user-chip {
  background: #f0f7ff;
  color: #0070f3;
  padding: 4px 12px;
  border-radius: 20px;
  font-size: 13px;
  font-weight: 600;
}

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

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

.login-link {
  background: #0070f3;
  color: white !important;
  padding: 7px 18px;
  border-radius: 8px;
  font-weight: 600;
}

.login-link:hover { background: #005ac1; }

Home Page

src/app/pages/home/home.ts:

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

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

src/app/pages/home/home.html:

<div class="hero">
  <h1>Welcome to DevBlog</h1>
  <p>Tutorials on Angular, TypeScript, and modern web development.</p>
  <div class="hero-actions">
    <a routerLink="/blog" class="btn-primary">Read Articles</a>
    <a routerLink="/login" class="btn-secondary">Start Writing</a>
  </div>
</div>

src/app/pages/home/home.css:

.hero {
  text-align: center;
  padding: 80px 24px;
  max-width: 700px;
  margin: 0 auto;
}

h1 {
  font-size: 52px;
  font-weight: 800;
  color: #1a1a2e;
  line-height: 1.1;
  margin-bottom: 16px;
  letter-spacing: -1px;
}

p {
  font-size: 18px;
  color: #666;
  margin-bottom: 36px;
}

.hero-actions { display: flex; gap: 16px; justify-content: center; }

.btn-primary {
  background: #0070f3;
  color: white;
  padding: 13px 28px;
  border-radius: 8px;
  text-decoration: none;
  font-weight: 600;
  font-size: 15px;
  transition: background 0.2s;
}

.btn-primary:hover { background: #005ac1; }

.btn-secondary {
  background: white;
  color: #0070f3;
  border: 2px solid #0070f3;
  padding: 12px 28px;
  border-radius: 8px;
  text-decoration: none;
  font-weight: 600;
  font-size: 15px;
  transition: all 0.2s;
}

.btn-secondary:hover { background: #f0f7ff; }

Blog List Page

src/app/pages/blog/blog.ts:

import { Component, inject } from '@angular/core';
import { RouterLink } from '@angular/router';
import { Blog as BlogService } from '../../services/blog';

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

  blogService = inject(BlogService);
  posts = this.blogService.allPosts;
}

src/app/pages/blog/blog.html:

<div class="blog-page">
  <h1>All Articles</h1>
  <p class="subtitle">{{ posts().length }} articles published</p>

  <div class="post-grid">
    @for (post of posts(); track post.id) {
      <article class="post-card">
        <span class="category">{{ post.category }}</span>
        <h2>{{ post.title }}</h2>
        <p class="excerpt">{{ post.excerpt }}</p>
        <div class="post-meta">
          <span>{{ post.author }}</span>
          <span>·</span>
          <span>{{ post.readTime }} min read</span>
          <span>·</span>
          <span>{{ post.date }}</span>
        </div>
        <a [routerLink]="['/blog', post.id]" class="read-more">Read Article →</a>
      </article>
    }
  </div>
</div>

src/app/pages/blog/blog.css:

.blog-page {
  max-width: 900px;
  margin: 0 auto;
  padding: 48px 24px;
}

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

.subtitle {
  color: #888;
  margin-bottom: 40px;
}

.post-grid {
  display: flex;
  flex-direction: column;
  gap: 24px;
}

.post-card {
  background: white;
  padding: 28px;
  border-radius: 12px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.06);
  transition: box-shadow 0.2s;
}

.post-card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.1); }

.category {
  font-size: 12px;
  font-weight: 700;
  color: #0070f3;
  text-transform: uppercase;
  letter-spacing: 1px;
}

.post-card h2 {
  font-size: 22px;
  color: #1a1a2e;
  margin: 10px 0 8px;
  font-weight: 700;
}

.excerpt {
  color: #666;
  line-height: 1.6;
  font-size: 15px;
  margin-bottom: 16px;
}

.post-meta {
  display: flex;
  gap: 8px;
  font-size: 13px;
  color: #999;
  margin-bottom: 20px;
}

.read-more {
  color: #0070f3;
  font-weight: 600;
  text-decoration: none;
  font-size: 14px;
}

.read-more:hover { text-decoration: underline; }

Post Detail Page

src/app/pages/post-detail/post-detail.ts:

import { Component, inject, OnInit, signal } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { Blog, Post } from '../../services/blog';

@Component({
  selector: 'app-post-detail',
  imports: [RouterLink],
  templateUrl: './post-detail.html',
  styleUrl: './post-detail.css'
})
export class PostDetail implements OnInit {

  private route = inject(ActivatedRoute);
  private blogService = inject(Blog);

  post = signal<Post | null>(null);
  notFound = signal<boolean>(false);

  ngOnInit(): void {
    const id = Number(this.route.snapshot.params['id']);
    const found = this.blogService.getPostById(id);

    if (found) {
      this.post.set(found);
    } else {
      this.notFound.set(true);
    }
  }
}

src/app/pages/post-detail/post-detail.html:

<div class="post-detail">

  @if (notFound()) {
    <div class="not-found">
      <h2>Post not found</h2>
      <a routerLink="/blog">← Back to Blog</a>
    </div>
  }

  @if (post()) {
    <article>
      <a routerLink="/blog" class="back">← Back to Blog</a>

      <header class="post-header">
        <span class="category">{{ post()!.category }}</span>
        <h1>{{ post()!.title }}</h1>
        <div class="meta">
          <span>By {{ post()!.author }}</span>
          <span>·</span>
          <span>{{ post()!.readTime }} min read</span>
          <span>·</span>
          <span>{{ post()!.date }}</span>
        </div>
      </header>

      <div class="post-content">
        <p>{{ post()!.content }}</p>
      </div>
    </article>
  }

</div>

src/app/pages/post-detail/post-detail.css:

.post-detail {
  max-width: 760px;
  margin: 0 auto;
  padding: 48px 24px;
}

.back {
  color: #0070f3;
  text-decoration: none;
  font-size: 14px;
  font-weight: 600;
  display: block;
  margin-bottom: 32px;
}

.post-header { margin-bottom: 40px; }

.category {
  font-size: 12px;
  font-weight: 700;
  color: #0070f3;
  text-transform: uppercase;
  letter-spacing: 1px;
}

h1 {
  font-size: 40px;
  font-weight: 800;
  color: #1a1a2e;
  line-height: 1.2;
  margin: 12px 0;
  letter-spacing: -0.5px;
}

.meta {
  display: flex;
  gap: 8px;
  color: #999;
  font-size: 14px;
}

.post-content p {
  font-size: 17px;
  line-height: 1.8;
  color: #444;
}

.not-found { text-align: center; padding: 80px 0; }
.not-found h2 { color: #333; margin-bottom: 16px; }
.not-found a { color: #0070f3; text-decoration: none; }

Login Page

src/app/pages/login/login.ts:

import { Component, inject, signal } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { Auth } from '../../services/auth';

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

  private authService = inject(Auth);
  private router = inject(Router);
  private route = inject(ActivatedRoute);

  name: string = '';
  password: string = '';
  error = signal<string>('');

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

    if (success) {
      const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/dashboard';
      this.router.navigateByUrl(returnUrl);
    } else {
      this.error.set('Invalid credentials. Use any name with password "password".');
    }
  }
}

src/app/pages/login/login.html:

<div class="login-page">
  <div class="login-card">
    <h1>Sign In</h1>
    <p class="subtitle">Welcome back to DevBlog</p>

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

    <div class="field">
      <label>Your Name</label>
      <input type="text" [(ngModel)]="name" placeholder="Rahul Sharma">
    </div>

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

    <button class="login-btn" (click)="onLogin()">Sign In</button>

    <p class="hint">Hint: any name + password "password"</p>
  </div>
</div>

src/app/pages/login/login.css:

.login-page {
  min-height: calc(100vh - 60px);
  display: flex;
  align-items: center;
  justify-content: center;
  background: #f9fafb;
}

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

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

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

.error-msg {
  background: #fef2f2;
  border: 1px solid #fecaca;
  color: #dc2626;
  padding: 12px 16px;
  border-radius: 8px;
  font-size: 14px;
  margin-bottom: 20px;
}

.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 #e0e0e0;
  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: 15px;
  font-weight: 600;
  cursor: pointer;
  margin-top: 8px;
  transition: background 0.2s;
}

.login-btn:hover { background: #005ac1; }

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

Dashboard and Child Pages

src/app/pages/dashboard/dashboard.ts:

import { Component, inject } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { Auth } from '../../services/auth';

@Component({
  selector: 'app-dashboard',
  imports: [RouterLink, RouterLinkActive, RouterOutlet],
  templateUrl: './dashboard.html',
  styleUrl: './dashboard.css'
})
export class Dashboard {

  authService = inject(Auth);
  userName = this.authService.userName;
}

src/app/pages/dashboard/dashboard.html:

<div class="dashboard-layout">
  <aside class="sidebar">
    <div class="sidebar-user">
      <div class="avatar">{{ userName()[0] }}</div>
      <span>{{ userName() }}</span>
    </div>

    <nav>
      <a routerLink="my-posts" routerLinkActive="active">📝 My Posts</a>
      <a routerLink="write" routerLinkActive="active">✏️ Write</a>
    </nav>
  </aside>

  <main class="dashboard-main">
    <router-outlet></router-outlet>
  </main>
</div>

src/app/pages/dashboard/dashboard.css:

.dashboard-layout {
  display: flex;
  min-height: calc(100vh - 60px);
}

.sidebar {
  width: 220px;
  background: #1a1a2e;
  padding: 24px 16px;
  flex-shrink: 0;
}

.sidebar-user {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 8px 20px;
  border-bottom: 1px solid #233554;
  margin-bottom: 16px;
  color: #ccd6f6;
  font-size: 14px;
  font-weight: 600;
}

.avatar {
  width: 36px;
  height: 36px;
  background: #0070f3;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-weight: 700;
}

.sidebar nav {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.sidebar nav a {
  display: block;
  padding: 10px 12px;
  color: #8892b0;
  text-decoration: none;
  border-radius: 8px;
  font-size: 14px;
  transition: all 0.2s;
}

.sidebar nav a:hover { color: #ccd6f6; background: rgba(255,255,255,0.05); }
.sidebar nav a.active { color: #64ffda; background: rgba(100,255,218,0.08); }

.dashboard-main { flex: 1; padding: 36px; background: #f9fafb; }

src/app/pages/dashboard/my-posts/my-posts.ts:

import { Component, inject } from '@angular/core';
import { Blog } from '../../../services/blog';
import { Auth } from '../../../services/auth';
import { RouterLink } from '@angular/router';

@Component({
  selector: 'app-my-posts',
  imports: [RouterLink],
  template: `
    <div>
      <h1>My Posts</h1>
      <div class="posts">
        @for (post of myPosts; track post.id) {
          <div class="post-row">
            <div>
              <h3>{{ post.title }}</h3>
              <p>{{ post.date }} · {{ post.readTime }} min read</p>
            </div>
            <a [routerLink]="['/blog', post.id]">View →</a>
          </div>
        } @empty {
          <p class="empty">No posts yet. Start writing!</p>
        }
      </div>
    </div>
  `,
  styles: [`
    h1 { font-size: 26px; font-weight: 700; color: #1a1a2e; margin-bottom: 24px; }
    .posts { display: flex; flex-direction: column; gap: 12px; }
    .post-row {
      background: white;
      padding: 18px 20px;
      border-radius: 10px;
      display: flex;
      justify-content: space-between;
      align-items: center;
      box-shadow: 0 1px 4px rgba(0,0,0,0.06);
    }
    h3 { font-size: 16px; color: #1a1a2e; margin-bottom: 4px; }
    p { font-size: 13px; color: #999; }
    a { color: #0070f3; font-size: 14px; font-weight: 600; text-decoration: none; }
    .empty { color: #999; font-size: 15px; }
  `]
})
export class MyPosts {

  private blogService = inject(Blog);
  private authService = inject(Auth);

  get myPosts() {
    return this.blogService.allPosts().filter(
      p => p.author === this.authService.userName()
    );
  }
}

src/app/pages/dashboard/write-post/write-post.ts:

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

@Component({
  selector: 'app-write-post',
  imports: [FormsModule],
  template: `
    <div>
      <h1>Write a Post</h1>
      <div class="field">
        <label>Title</label>
        <input type="text" [(ngModel)]="title" placeholder="Post title...">
      </div>
      <div class="field">
        <label>Content</label>
        <textarea [(ngModel)]="content" rows="10" placeholder="Write your post..."></textarea>
      </div>
      <button class="publish-btn" [disabled]="!title || !content">Publish Post</button>
    </div>
  `,
  styles: [`
    h1 { font-size: 26px; font-weight: 700; color: #1a1a2e; margin-bottom: 24px; }
    .field { margin-bottom: 18px; }
    label { display: block; font-size: 13px; font-weight: 600; color: #555; margin-bottom: 6px; }
    input, textarea {
      width: 100%;
      padding: 12px 14px;
      border: 1px solid #e0e0e0;
      border-radius: 8px;
      font-size: 15px;
      font-family: inherit;
      box-sizing: border-box;
    }
    input:focus, textarea:focus { outline: none; border-color: #0070f3; }
    textarea { resize: vertical; }
    .publish-btn {
      background: #0070f3;
      color: white;
      border: none;
      padding: 12px 28px;
      border-radius: 8px;
      font-size: 15px;
      font-weight: 600;
      cursor: pointer;
    }
    .publish-btn:disabled { background: #ccc; cursor: not-allowed; }
  `]
})
export class WritePost {
  title: string = '';
  content: string = '';
}

Not Found Page

src/app/pages/not-found/not-found.ts:

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

@Component({
  selector: 'app-not-found',
  imports: [RouterLink],
  template: `
    <div class="not-found">
      <h1>404</h1>
      <p>This page does not exist.</p>
      <a routerLink="/">← Go Home</a>
    </div>
  `,
  styles: [`
    .not-found { text-align: center; padding: 100px 24px; }
    h1 { font-size: 96px; font-weight: 800; color: #e0e0e0; line-height: 1; margin-bottom: 16px; }
    p { font-size: 18px; color: #888; margin-bottom: 24px; }
    a { color: #0070f3; font-weight: 600; text-decoration: none; font-size: 15px; }
  `]
})
export class NotFound { }

App Component

src/app/app.ts:

import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { Navbar } from './components/navbar/navbar';

@Component({
  selector: 'app-root',
  imports: [RouterOutlet, Navbar],
  template: `
    <app-navbar></app-navbar>
    <router-outlet></router-outlet>
  `
})
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: #f9fafb;
  color: #1a1a2e;
}

Run ng serve -o. Your complete blog application is running with:

Home page at /. Blog list at /blog. Individual posts at /blog/1, /blog/2, etc. Protected dashboard at /dashboard with nested /dashboard/my-posts and /dashboard/write. Login at /login with redirect to the page the user was trying to reach. 404 page for any unknown URL. All routes lazy loaded — each page's code downloads only when navigated to. Active nav links highlighted with RouterLinkActive. Programmatic navigation after login using Router.navigateByUrl.


Phase 6 — Complete Summary

Here is everything you learned in this phase.

What routing is — Angular reads the URL and renders different components without page reloads. The URL updates so users can share, bookmark, and navigate with browser history.

Defining routes — Routes are objects in app.routes.ts. Each route maps a path string to a component. The ** wildcard catches all unmatched URLs. redirectTo redirects one path to another.

router-outlet — The placeholder in your template where matched components render. Root outlet in app.html. Child outlets inside parent routed components.

RouterLink — Use routerLink instead of href for all internal navigation. Never use href inside Angular apps.

RouterLinkActive — Adds a CSS class when the route is active. Use [routerLinkActiveOptions]="{ exact: true }" for the home route.

Programmatic navigationinject(Router) then this.router.navigate(['/path']) to navigate from TypeScript code. navigateByUrl for full URL strings.

Route parameters:id in the path creates a parameter. Read it with inject(ActivatedRoute) then this.route.snapshot.params['id']. Always convert to the right type — params are always strings.

Query parameters?key=value in URLs. Set with queryParams option on navigate. Read with this.route.snapshot.queryParams['key'].

Nested routeschildren array on a parent route. Parent must have its own <router-outlet>. Child routerLink without leading / is relative to the parent path.

Route guards — Functions that run before navigation. canActivate blocks unauthorized access. canActivateChild protects all children at once. canDeactivate warns about unsaved changes. Return true to allow, false to block, or redirect and return false.

Lazy loadingloadComponent with a dynamic import. Component code only downloads when the route is first visited. Dramatically improves initial load performance.

Route data — Static data attached to routes with the data property. Read with this.route.snapshot.data['key']. Useful for page titles and breadcrumbs.


What's Next — Phase 7

In Phase 7 we cover Forms in complete depth — both approaches Angular provides:

Template-driven forms — simple forms where most logic is in the HTML. Reactive forms — complex forms where all logic is in TypeScript. Form validation — built-in validators and custom validators. Showing error messages — displaying the right message at the right time. Dynamic forms — adding and removing form fields at runtime. Async validators — checking things like username availability against an API.

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 8 — HTTP Client & APIs

Chapter 1 — What is HTTP and Why Do You Need It? 1.1 — Your App Needs to Talk to the Outside World Everything you have built so far has used...