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.


No comments:

Post a Comment

Phase 5 — Services & Dependency Injection

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