Saturday, August 16, 2025
0 comments

Master Angular 20 Services & Dependency Injection: A Beginner’s Ultimate Guide with Hands-On Projects and Troubleshooting (Part-4)

 


Angular 20 is a powerful framework for building dynamic single-page applications (SPAs), and services combined with dependency injection (DI) are key to managing shared logic and data across components. As a beginner, you’ve already explored components, templates, directives, and routing. Now, it’s time to learn how services enable reusable logic and data sharing, making your Angular apps modular and maintainable. In this guide, we’ll dive into services, dependency injection, and singleton services, and build a hands-on project to create a UserService that stores and fetches user data, injected into the UserComponent and AdminComponent from previous lessons. We’ll also troubleshoot common issues like the TS2305 error and integrate with routing, directives, and pipes.

This tutorial assumes you have a basic Angular 20 project (created with ng new angular-routing-demo) and familiarity with components, routing, and pipes. Let’s master services and dependency injection!
IntroductionServices in Angular are classes that encapsulate reusable logic, such as data fetching, business rules, or state management. Dependency injection allows components to request these services, promoting modularity and testability. In this guide, we’ll cover:
  • What is a Service?: Understanding its role in Angular.
  • Generating a Service: Using ng g service.
  • Dependency Injection: How Angular provides services to components.
  • Sharing Data: Using services to share data between components.
  • Singleton Services: Ensuring one instance with providedIn: 'root'.
  • Hands-On Project: Creating a UserService for user data management.
We’ll build on the routing project (User and Admin dashboards, User Details page) and integrate services for a cohesive application.
What is a Service?A service in Angular is a TypeScript class that handles specific tasks, such as:
  • Fetching data from an API.
  • Managing shared state (e.g., user data).
  • Performing calculations or business logic.
Services are injectable, meaning components can request them via dependency injection, keeping components focused on UI logic and delegating other tasks to services.Example Use Case: A UserService could fetch user data from a server and share it between UserComponent and AdminComponent.
Generating a ServiceAngular CLI simplifies service creation with the ng g service command.Step 1: Generate UserServiceRun:
bash
ng g service services/user
This creates:
src/app/services/
├── user.service.ts
├── user.service.spec.ts
Step 2: Inspect UserServiceThe generated service looks like:
typescript
// src/app/services/user.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  constructor() {}
}
Explanation:
  • @Injectable: Marks the class as injectable, allowing Angular’s DI system to provide it to components.
  • providedIn: 'root': Registers the service as a singleton at the root level, available throughout the app.

Dependency Injection ConceptDependency Injection (DI) is a design pattern where a class receives its dependencies (e.g., services) from an external source (Angular’s injector) rather than creating them itself. In Angular:
  • Injector: Maintains a container of services and provides them to components.
  • Providers: Define how services are created (e.g., providedIn: 'root').
  • Constructor Injection: Components request services via their constructors.
Example:
typescript
import { Component } from '@angular/core';
import { UserService } from '../services/user.service';

@Component({
  selector: 'app-example',
  standalone: true
})
export class ExampleComponent {
  constructor(private userService: UserService) {
    // Use userService here
  }
}
Explanation:
  • Angular’s injector sees UserService in the constructor and provides an instance.
  • providedIn: 'root' ensures a single instance is shared across the app.

Sharing Data Between Components via ServiceServices are ideal for sharing data between components, avoiding prop-drilling or redundant logic.Scenario: Both UserComponent and AdminComponent need access to a list of users. A UserService can store and manage this data.Steps:
  1. Store user data in the service.
  2. Inject the service into components.
  3. Use service methods to get or update data.

Singleton Services in AngularA singleton service is a single instance shared across the application. In Angular 20, providedIn: 'root' makes a service a singleton by registering it at the root injector.Why Singleton?
  • Ensures consistent data (e.g., one user list shared by all components).
  • Reduces memory usage by avoiding multiple instances.
  • Simplifies state management.
Alternative: You can provide services at the component level (not singleton) using providers: [UserService] in the component’s metadata, but providedIn: 'root' is preferred for app-wide services.
Using Service with providedIn: 'root'The providedIn: 'root' metadata in @Injectable ensures the service is available globally and created only once.Example:
typescript
@Injectable({
  providedIn: 'root'
})
export class UserService {
  private users = [
    { id: 1, name: 'Alice', role: 'admin' },
    { id: 2, name: 'Bob', role: 'user' }
  ];

  getUsers() {
    return this.users;
  }
}
Explanation:
  • The service is registered at the root injector.
  • All components share the same users array.

Hands-On Project: Create UserService and Inject into ComponentsLet’s build a UserService to store and fetch user data, integrating it into the UserComponent and AdminComponent from the routing project. We’ll also reuse the FilterByRolePipe and routing setup.Step 1: Set Up the ProjectIf you haven’t already, create the project:
bash
ng new angular-routing-demo
cd angular-routing-demo
ng g component components/user
ng g component components/admin
ng g component components/user-details
ng g component components/not-found
ng g component components/admin-dashboard
ng g component components/admin-settings
ng g pipe pipes/filter-by-role
ng g service services/user
Step 2: Implement UserService
typescript
// src/app/services/user.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private users = [
    { id: 1, name: 'Alice', role: 'admin' },
    { id: 2, name: 'Bob', role: 'user' },
    { id: 3, name: 'Charlie', role: 'admin' },
    { id: 4, name: 'David', role: 'user' }
  ];

  getUsers() {
    return this.users;
  }

  getUserById(id: number) {
    return this.users.find(user => user.id === id);
  }

  addUser(user: { id: number; name: string; role: string }) {
    this.users.push(user);
  }
}
Explanation:
  • users: A private array simulating a database.
  • getUsers(): Returns the full user list.
  • getUserById(id): Fetches a user by ID for the User Details page.
  • addUser(): Adds a new user to the list.
Step 3: Update app.routes.tsEnsure routes are configured:
typescript
// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { UserComponent } from './components/user/user.component';
import { AdminComponent } from './components/admin/admin.component';
import { UserDetailsComponent } from './components/user-details/user-details.component';
import { NotFoundComponent } from './components/not-found/not-found.component';
import { AdminDashboardComponent } from './components/admin-dashboard/admin-dashboard.component';
import { AdminSettingsComponent } from './components/admin-settings/admin-settings.component';

export const routes: Routes = [
  { path: 'user', component: UserComponent },
  { path: 'user/:id', component: UserDetailsComponent },
  {
    path: 'admin',
    component: AdminComponent,
    children: [
      { path: 'dashboard', component: AdminDashboardComponent },
      { path: 'settings', component: AdminSettingsComponent },
      { path: '', redirectTo: 'dashboard', pathMatch: 'full' }
    ]
  },
  { path: '', redirectTo: '/user', pathMatch: 'full' },
  { path: '**', component: NotFoundComponent }
];
Step 4: Update main.ts
typescript
// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';

bootstrapApplication(AppComponent, {
  providers: [provideRouter(routes)]
}).catch(err => console.error(err));
Step 5: Update AppComponent
typescript
// src/app/app.component.ts
import { Component, OnInit } from '@angular/core';
import { Router, NavigationStart, NavigationEnd, RouterOutlet, RouterLink } from '@angular/router';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  standalone: true,
  imports: [RouterOutlet, RouterLink]
})
export class AppComponent implements OnInit {
  constructor(private router: Router) {}

  ngOnInit() {
    this.router.events.subscribe(event => {
      if (event instanceof NavigationStart) {
        console.log('Navigation started:', event.url);
      } else if (event instanceof NavigationEnd) {
        console.log('Navigation ended:', event.url);
      }
    });
  }
}
typescript
// src/app/app.component.html
nav:
  a: routerLink="/user" routerLinkActive="active": User Dashboard
  a: routerLink="/admin" routerLinkActive="active": Admin Panel
router-outlet
css
// src/app/app.component.css
nav { margin: 20px; }
a { margin-right: 10px; text-decoration: none; }
.active { font-weight: bold; color: #007bff; }
Step 6: Update UserComponentInject UserService to display and add users:
typescript
// src/app/components/user/user.component.ts
import { Component, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { UserService } from '../../services/user.service';

@Component({
  selector: 'app-user',
  templateUrl: './user.component.html',
  styleUrls: ['./user.component.css'],
  standalone: true,
  imports: [FormsModule, RouterLink]
})
export class UserComponent implements OnInit {
  searchQuery = '';
  users: { id: number; name: string; role: string }[] = [];
  newUser = { name: '', role: 'user' };

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private userService: UserService
  ) {}

  ngOnInit() {
    this.users = this.userService.getUsers();
    this.route.queryParamMap.subscribe(params => {
      this.searchQuery = params.get('search') || '';
    });
  }

  updateQuery() {
    this.router.navigate(['/user'], { queryParams: { search: this.searchQuery } });
  }

  addUser() {
    if (this.newUser.name) {
      this.userService.addUser({
        id: this.users.length + 1,
        name: this.newUser.name,
        role: this.newUser.role
      });
      this.newUser = { name: '', role: 'user' }; // Reset form
      this.users = this.userService.getUsers(); // Refresh list
    }
  }
}
typescript
// src/app/components/user/user.component.html
h2: User Dashboard
div:
  label: Search users: 
  input: [(ngModel)]="searchQuery" placeholder="Search users"
  button: (click)="updateQuery()": Search
p: Search Query: {{ searchQuery }}
h3: Add New User
form: (ngSubmit)="addUser()":
  label: Name: 
  input: [(ngModel)]="newUser.name" name="name" placeholder="Enter name"
  label: Role: 
  select: [(ngModel)]="newUser.role" name="role":
    option: value="user": User
    option: value="admin": Admin
  button: type="submit": Add User
ul:
  li: *ngFor="let user of users":
    a: routerLink="/user/{{ user.id }}": {{ user.name }} ({{ user.role }})
css
// src/app/components/user/user.component.css
form { margin: 20px; padding: 20px; border: 1px solid #ccc; }
input, select { padding: 5px; margin: 5px; }
ul { list-style-type: none; padding: 0; }
li { padding: 10px; margin: 5px 0; border: 1px solid #ccc; }
Explanation:
  • Injects UserService via the constructor.
  • getUsers() fetches the user list.
  • addUser() adds a new user and refreshes the list.
  • Integrates query parameters and routing from previous lessons.
Step 7: Update UserDetailsComponentUse UserService to fetch user details:
typescript
// src/app/components/user-details/user-details.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { UserService } from '../../services/user.service';

@Component({
  selector: 'app-user-details',
  templateUrl: './user-details.component.html',
  standalone: true,
  imports: [RouterLink]
})
export class UserDetailsComponent implements OnInit {
  user: { id: number; name: string; role: string } | undefined;

  constructor(
    private route: ActivatedRoute,
    private userService: UserService
  ) {}

  ngOnInit() {
    this.route.paramMap.subscribe(params => {
      const id = Number(params.get('id'));
      this.user = this.userService.getUserById(id);
    });
  }
}
typescript
// src/app/components/user-details/user-details.component.html
h2: User Details
div: *ngIf="user; else noUser":
  p: Name: {{ user.name }}
  p: Role: {{ user.role | uppercase }}
p: else noUser: User not found
p: Go back to User Dashboard
Explanation:
  • Injects UserService to fetch a user by ID.
  • Uses *ngIf to handle cases where the user is not found.
  • Applies the uppercase pipe to the role.
Step 8: Update AdminComponentInject UserService for the users list:
typescript
// src/app/components/admin/admin.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterOutlet, RouterLink } from '@angular/router';
import { FilterByRolePipe } from '../../pipes/filter-by-role.pipe';
import { UserService } from '../../services/user.service';

@Component({
  selector: 'app-admin',
  templateUrl: './admin.component.html',
  styleUrls: ['./admin.component.css'],
  standalone: true,
  imports: [FormsModule, RouterOutlet, RouterLink, FilterByRolePipe]
})
export class AdminComponent implements OnInit, OnDestroy {
  users: { id: number; name: string; role: string }[] = [];
  filterRole = 'admin';
  isHighlighted = false;

  constructor(private userService: UserService) {}

  ngOnInit() {
    console.log('AdminComponent: Initialized');
    this.users = this.userService.getUsers();
  }

  ngOnDestroy() {
    console.log('AdminComponent: Destroyed');
  }

  toggleHighlight() {
    this.isHighlighted = !this.isHighlighted;
  }

  setFilterRole(role: string) {
    this.filterRole = role;
  }

  logSearch(value: string) {
    console.log('Search:', value);
  }
}
typescript
// src/app/components/admin/admin.component.html
h2: Admin Panel
nav:
  a: routerLink="dashboard" routerLinkActive="active": Dashboard
  a: routerLink="settings" routerLinkActive="active": Settings
router-outlet
h3: Admin Users List
div:
  label: Filter by Role: 
  select: [(ngModel)]="filterRole" (change)="setFilterRole(filterRole)":
    option: value="all": All
    option: value="admin": Admin
    option: value="user": User
button: (click)="toggleHighlight()": Toggle Highlight
ul:
  li: *ngFor="let user of users | filterByRole:filterRole" [ngClass]="{'highlight': isHighlighted && user.role === 'admin'}":
    {{ user.name | uppercase }} ({{ user.role }})
input: #searchInput placeholder="Search users"
button: (click)="logSearch(searchInput.value)": Search
css
// src/app/components/admin/admin.component.css
ul { list-style-type: none; padding: 0; }
li { padding: 10px; margin: 5px 0; border: 1px solid #ccc; }
.highlight { background-color: #e0f7fa; }
nav { margin: 10px 0; }
a { margin-right: 10px; text-decoration: none; }
.active { font-weight: bold; color: #007bff; }
Explanation:
  • Injects UserService to fetch the shared user list.
  • Reuses FilterByRolePipe and [ngClass] from previous lessons.
  • Maintains child routes for Dashboard and Settings.
Step 9: Update Child Components
typescript
// src/app/components/admin-dashboard/admin-dashboard.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-admin-dashboard',
  template: `
    h3: Admin Dashboard
    p: Welcome to the Admin Dashboard!
  `,
  standalone: true
})
export class AdminDashboardComponent {}
typescript
// src/app/components/admin-settings/admin-settings.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-admin-settings',
  template: `
    h3: Admin Settings
    p: Configure settings here.
  `,
  standalone: true
})
export class AdminSettingsComponent {}
Step 10: Update NotFoundComponent
typescript
// src/app/components/not-found/not-found.component.ts
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';

@Component({
  selector: 'app-not-found',
  template: `
    h2: 404 - Page Not Found
    p: Go back to User Dashboard
  `,
  standalone: true,
  imports: [RouterLink]
})
export class NotFoundComponent {}
Step 11: Test the Application
  1. Run ng serve and visit http://localhost:4200.
  2. Test features:
    • User Dashboard (/user):
      • Add a new user via the form.
      • Use the search input to set query parameters (e.g., ?search=john).
      • Click a user name to navigate to /user/:id.
    • User Details (/user/:id):
      • View user details or “User not found” for invalid IDs.
    • Admin Panel (/admin):
      • Filter users by role using the dropdown.
      • Toggle highlighting for admin users.
      • Navigate to /admin/dashboard and /admin/settings.
    • 404 Page (/invalid):
      • Verify the Not Found page appears.
    • Shared Data: Add a user in UserComponent and confirm it appears in AdminComponent’s list.
    • Console Logs: Check for lifecycle hooks and router events.

Troubleshooting Common ErrorsTS2305: Module '"./app/app"' has no exported member 'App'Cause: main.ts imports App instead of AppComponent.Fix:
typescript
// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';

bootstrapApplication(AppComponent, {
  providers: [provideRouter(routes)]
}).catch(err => console.error(err));
Steps:
  1. Verify app.component.ts exports AppComponent.
  2. Check the file path (./app/app.component).
  3. Run ng serve.
Service Not FoundCause: Missing providedIn: 'root' or incorrect import.Fix:
  • Ensure @Injectable({ providedIn: 'root' }) is in user.service.ts.
  • Verify the import path in components (e.g., ../../services/user.service).
Data Not SharedCause: Multiple service instances due to providers in component metadata.Fix:
  • Use providedIn: 'root' in the service instead of providers: [UserService] in components.

Best Practices for Services
  • Use providedIn: 'root': Ensures singleton behavior for app-wide services.
  • Keep Services Focused: Each service should handle a specific domain (e.g., user data).
  • Avoid Business Logic in Components: Delegate to services for modularity.
  • Type Safety: Use interfaces for data (e.g., interface User { id: number; name: string; role: string }).
  • Mock Services for Testing: Use dependency injection to swap real services with mocks.

0 comments:

Featured Post

Master Angular 20 Basics: A Complete Beginner’s Guide with Examples and Best Practices

Welcome to the complete Angular 20 learning roadmap ! This series takes you step by step from basics to intermediate concepts , with hands...

Subscribe

 
Toggle Footer
Top