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.

No comments:

Post a Comment

Phase 7 — Forms

Chapter 1 — Why Forms Are a Big Deal 1.1 — Forms Are Everywhere Every meaningful interaction a user has with a web application goes through ...