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?A service in Angular is a TypeScript class that handles specific tasks, such as:
Generating a ServiceAngular CLI simplifies service creation with the ng g service command.Step 1: Generate UserServiceRun:This creates:Step 2: Inspect UserServiceThe generated service looks like:Explanation:
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:Explanation:
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:
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?
Using Service with providedIn: 'root'The providedIn: 'root' metadata in @Injectable ensures the service is available globally and created only once.Example:Explanation:
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:Step 2: Implement UserServiceExplanation:Step 4: Update main.tsStep 5: Update AppComponentStep 6: Update UserComponentInject UserService to display and add users:Explanation:Explanation:Explanation:Step 10: Update NotFoundComponentStep 11: Test the Application
Troubleshooting Common ErrorsTS2305: Module '"./app/app"' has no exported member 'App'Cause: main.ts imports App instead of AppComponent.Fix:Steps:
Best Practices for Services
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.
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.
Generating a ServiceAngular CLI simplifies service creation with the ng g service command.Step 1: Generate UserServiceRun:
bash
ng g service services/user
src/app/services/
├── user.service.ts
├── user.service.spec.ts
typescript
// src/app/services/user.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class UserService {
constructor() {}
}
- @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.
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
}
}
- 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:
- Store user data in the service.
- Inject the service into components.
- 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.
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;
}
}
- 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
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);
}
}
- 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.
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 }
];
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));
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; }
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; }
- 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.
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
- 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.
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; }
- Injects UserService to fetch the shared user list.
- Reuses FilterByRolePipe and [ngClass] from previous lessons.
- Maintains child routes for Dashboard and Settings.
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 {}
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 {}
- Run ng serve and visit http://localhost:4200.
- 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.
- User Dashboard (/user):
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));
- Verify app.component.ts exports AppComponent.
- Check the file path (./app/app.component).
- Run ng serve.
- Ensure @Injectable({ providedIn: 'root' }) is in user.service.ts.
- Verify the import path in components (e.g., ../../services/user.service).
- 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:
Post a Comment