Introduction to Angular Services
Within the Angular framework, an effective tool for creating web applications, Angular Services offer reliable ways to manage and inject dependencies into your components. Requirement injection and Angular Services simplify the structure and organization of your code, making it more modular, reusable, and maintainable. In this post, we will explore the Angular concepts of dependency injection and Angular Services in detail, supported by a comprehensive example that shows these notions in action. with the conclusion, you will see how the organization and efficiency of your application are improved with Angular Services.
Understanding Angular Services
A service in Angular is a class that contains common data, functionality, or business logic that may be used by several components. By letting you group various components of your program into independent classes, Angular Services support the idea of single responsibility. Your codebase becomes cleaner and easier to maintain as a result.\
Key Benefits of Using Services
- Code Reusability: By allowing services to be shared among several components, code duplication is decreased.
- Separation of Concerns: You may maintain a lean and UI-focused component architecture by assigning business logic to Angular Services.
- Testing: Your code will be more reliable if services are independently tested.
What Are Angular Services?
A TypeScript class with a specified function, such handling user authentication, retrieving data from an API, or encapsulating any business logic, is all that constitutes a service in Angular. By injecting Angular Services into components, other services, or even directives, you may neatly and modularly share functionality.
The following summarizes how services improve your Angular applications:
- Encapsulation: Services abstract away data administration and business logic, freeing up components to concentrate only on display logic.
- Reusability: Reusing services across different components minimizes code duplication and facilitates maintenance.
- Testability: Unit testing is made easier by the fact that services may be tested apart from the components that utilize them.
Why Make Use of Services?
There are several advantages of using services in Angular:
- Separation of Concerns: Services manage data and logic, while components manage the display and user interactions.
- Reusability: Multiple components can make use of the same services.
- Testability: Services are testable without reference to their component parts.
- Maintainability: Services encourage well-structured, easily maintained code.
Creating a Angular Services
Creating a service in Angular is straightforward. You define a service as a class, decorate it with the @Injectable
decorator, and then register it with Angular’s dependency injection system.
Here’s a simple example of a service that manages a list of tasks:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class TaskService {
private tasks: string[] = [];
constructor() { }
addTask(task: string) {
this.tasks.push(task);
}
getTasks(): string[] {
return this.tasks;
}
}
In this example, the TaskService
class encapsulates the logic for adding and retrieving tasks. The @Injectable
decorator with { providedIn: 'root' }
ensures that the service is available application-wide as a singleton.
Using Services in Components
To use a service in a component, you need to inject it through the component’s constructor. Here’s how you can use the TaskService
in a component:
import { Component, OnInit } from '@angular/core';
import { TaskService } from '../task.service';
@Component({
selector: 'app-task-list',
templateUrl: './task-list.component.html',
styleUrls: ['./task-list.component.css']
})
export class TaskListComponent implements OnInit {
tasks: string[] = [];
newTask: string = '';
constructor(private taskService: TaskService) { }
ngOnInit(): void {
this.tasks = this.taskService.getTasks();
}
addTask(): void {
if (this.newTask) {
this.taskService.addTask(this.newTask);
this.newTask = '';
this.tasks = this.taskService.getTasks();
}
}
}
In this TaskListComponent
, the TaskService
is injected into the constructor. The ngOnInit
lifecycle hook is used to initialize the list of tasks by calling the getTasks
method of the service. The addTask
method allows users to add new tasks via the service.
A singleton, or one shared instance of a service, is created by Angular when a service is given in the root injector (using providedIn: ‘root’). For services that maintain state or transfer data between components, this guarantees that the same instance of the service is utilized throughout the application, which is frequently desired.
Specific Angular Services
Sometimes you may want a service to be application-wide, while in other circumstances you may want it to be available exclusively within a particular module or component. This may be accomplished by offering the service within the module or component’s providers array:
@NgModule({
providers: [TaskService]
})
export class SomeModule { }
// or in a component
@Component({
providers: [TaskService]
})
export class SomeComponent { }
Every time a module or component is created, Angular generates a new instance of the service if it is supplied at the module or component level. In situations when you require separate instances of the service, this is helpful.
Tokens for Injection
Angular offers InjectionToken for more intricate dependency injection situations. For injecting items that are not class-based, such configuration settings or basic values, injection tokens are very helpful.
This is an illustration of how to inject a configuration object using an injection token:
import { InjectionToken } from '@angular/core';
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
export interface AppConfig {
apiUrl: string;
}
@NgModule({
providers: [
{ provide: APP_CONFIG, useValue: { apiUrl: 'https://api.example.com' } }
]
})
export class AppModule { }
// Injecting the token in a service
import { Inject, Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class ApiService {
constructor(@Inject(APP_CONFIG) private config: AppConfig) { }
getApiUrl(): string {
return this.config.apiUrl;
}
}
In this example, the APP_CONFIG
token is used to provide a configuration object that can be injected into any service or component that needs it.
Lazy Loading Services
Angular’s dependency injection system supports lazy loading, which can improve the performance of your application by loading services only when they are needed. To lazy load a service, you provide it in a lazy-loaded module rather than the root module:
@NgModule({
providers: [LazyLoadedService]
})
export class LazyLoadedModule { }
When the LazyLoadedModule
is loaded, Angular creates an instance of LazyLoadedService
, which is then available for injection within the module.
It is imperative to comprehend and apply dependency injection and services in Angular to construct modular, tested, and maintainable apps. You may maintain the simplicity and presentation logic of your components by encapsulating business logic and sharing functionality through services. A strong framework for managing dependencies is provided by dependency injection, which makes your code more modular and testable.
Creating an Angular Service: Step-by-Step
We will build a basic Angular service that retrieves information from a public API and applies it to a component. We’ll do the subsequent actions:
- Angular project setup: Start a fresh Angular project.
- Creating a Service: To create a service, using the Angular CLI.
- Putting the Service in Place: Create logic in the service to retrieve information from an API.
- Utilizing the Service Within an Elements: Display the retrieved data after injecting the service into a component.
- Error Handling: Include error handling in the component and service.
Step 1: Setting Up the Angular Project
First, ensure you have the Angular CLI installed. If not, install it using npm:
npm install -g @angular/cli
Create a new Angular project by running:
ng new angular-service-example
Navigate to the project directory:
cd angular-service-example
Step 2: Generating a Service
Generate a new service using the Angular CLI:
ng generate service data
This command creates two files:
src/app/data.service.ts
: The service class file.src/app/data.service.spec.ts
: The test file for the service.
Step 3: Implementing the Service
Open the data.service.ts
file and add the logic to fetch data from an API. We’ll use the HttpClient
module to make HTTP requests. First, import the necessary modules and inject HttpClient
into the service:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class DataService {
private apiUrl = 'https://jsonplaceholder.typicode.com/posts';
constructor(private http: HttpClient) { }
getPosts(): Observable<any[]> {
return this.http.get<any[]>(this.apiUrl).pipe(
catchError(this.handleError)
);
}
private handleError(error: any) {
console.error('An error occurred:', error);
return throwError('Something went wrong; please try again later.');
}
}
In this example, we use HttpClient
to make a GET request to a public API endpoint. The getPosts
method returns an observable that emits the response data. The catchError
operator is used to handle any errors that occur during the HTTP request.
Step 4: Using the Service in a Component
Next, we’ll use the DataService
in a component. For this example, let’s use the AppComponent
. Open the app.component.ts
file and modify it to use the service:
import { Component, OnInit } from '@angular/core';
import { DataService } from './data.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
posts: any[] = [];
errorMessage: string = '';
constructor(private dataService: DataService) {}
ngOnInit() {
this.dataService.getPosts().subscribe(
(data) => this.posts = data,
(error) => this.errorMessage = error
);
}
}
In this component, we inject the DataService
and use it to fetch data in the ngOnInit
lifecycle hook. The fetched data is stored in the posts
array, and any error message is stored in the errorMessage
string.
Modify the app.component.html
file to display the fetched data:
<div *ngIf="errorMessage">
<p>{{ errorMessage }}</p>
</div>
<div *ngIf="!errorMessage">
<ul>
<li *ngFor="let post of posts">
<h3>{{ post.title }}</h3>
<p>{{ post.body }}</p>
</li>
</ul>
</div>
This template displays an error message if there is one. If there’s no error, it lists the posts fetched from the API.
Step 5: Handling Errors
The catchError operator is already used by the DataService to handle simple errors. The handleError function logs errors and produces a message that is easy to understand if something goes wrong. This message is shown in the template by the AppComponent.
Examining the Service
We can develop tests to make sure our service functions as intended. To add tests for the DataService, open the data.service.spec.ts file and open it.
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { DataService } from './data.service';
describe('DataService', () => {
let service: DataService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [DataService]
});
service = TestBed.inject(DataService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('should fetch posts', () => {
const dummyPosts = [
{ userId: 1, id: 1, title: 'Post 1', body: 'Body 1' },
{ userId: 1, id: 2, title: 'Post 2', body: 'Body 2' }
];
service.getPosts().subscribe(posts => {
expect(posts.length).toBe(2);
expect(posts).toEqual(dummyPosts);
});
const request = httpMock.expectOne(`${service['apiUrl']}`);
expect(request.request.method).toBe('GET');
request.flush(dummyPosts);
});
it('should handle error', () => {
const errorMessage = 'Something went wrong; please try again later.';
service.getPosts().subscribe(
() => fail('Expected an error, not posts'),
error => expect(error).toBe(errorMessage)
);
const request = httpMock.expectOne(`${service['apiUrl']}`);
request.flush('Error', { status: 500, statusText: 'Server Error' });
});
});
In these tests, we use the HttpClientTestingModule
to mock HTTP requests. The first test checks if the service correctly fetches posts. The second test checks if the service correctly handles errors.
We have discussed the idea of Angular services and their advantages in this blog. An example of building a service, utilizing it in a component, and managing faults has been covered in detail. We’ve also looked at how to test the service to make sure everything functions as it should.
Angular’s use of services encourages a well-structured, readable codebase. It lets components concentrate on the user interface and the view, with services taking care of business logic, data collecting, and other duties. You may create scalable and reliable Angular apps by using this method.
Recall that this is only the start. To improve the efficiency and manageability of your application, Angular services may be expanded to handle a variety of activities, communicate with many APIs, and incorporate more complicated logic. Have fun with coding!