Services
Angular services are classes that can be injected into and shared with your Angular components and other services.
Services are a great feature that you will use regularly as usage will help you re-use data and methods that need to be used in more than one place on your application. While you could get very creative with your input values on your components, often you will find it much easier just to access data already loaded in a service from each component. If using the Angular router for presenting different pages of data, information stored in a service is the best way to share this data between pages.
Creating a service is as simple as creating a new component with the Angular CLI. Simply type in "ng generate service" followed by the name of the service you wish to create.
Let's create a new service for our people object now.
-
Make sure your command line is open to your project folder and type in:
ng generate service people
-
Cut and copy the following code from app.component.ts into your newly created
people.service.ts file. This should be immediately after the "export class
PeopleService {" line and before the constructor.
public people = [ {nameFirst: 'John', nameLast: 'Simpson', age: 51, gender: 'Male'}, {nameFirst: 'Bob', nameLast: 'Smith', age: 36, gender: 'Male'}, {nameFirst: 'Mary', nameLast: 'Smith', age: 32, gender: 'Female'}, {nameFirst: 'Theresa', nameLast: 'Banner', age: 25, gender: 'Female'} ];
-
Our app.component.html file should now be displaying errors in the editor. To fix this we need first inject our new service into app.component.ts.
Insert the following import on line 2 of app.component.ts.
import { PeopleService } from './people.service';
-
Our app.component.ts does not include a constructor method by default. Any other
classes we create do, and it is necessary to have a constructor method to inject your custom services. Add a
new constructor method inside app.component.ts after your variable declarations and
inject the PeopleService as a new object called peopleService.
constructor(public peopleService: PeopleService) { }
The service is now available to our component as peopleService. -
In your app.component.html file, replace all occurrences of "people" with
"peopleService.people" as shown below.
{{ peopleService.people | json }} <div *ngFor="let person of peopleService.people; index as i"> {{ i }}.{{ person | json }} </div> <app-name-card *ngFor="let person of peopleService.people" [person]="person"></app-name-card>
- View your application in the browser. It should be working the same way it did before we implemented our service.
Interfaces
One of the wonderful things about TypeScript is that if you put the effort into defining your data types instead of relying on the "any" data type, your code editor will become much more restrictive and alert you when making mistakes. These mistakes are better caught during development rather than later in front of an end user.
Creating interfaces for your custom data types is very easy, however it does require writing a little more code.
The first step is to define your interface. It's a good idea to put your interface in a file that makes the most business sense. In our example, we will be creating an interface for our people data so storing the interface in people.service.ts makes the most sense.
The syntax for defining interfaces is:
export interface InterfaceName {
fieldName: dataType;
}
Your interface may be defined in the same file as your PeopleService class, but it is not defined inside the class declaration. Instead, you need to define your interface after your import statements and either before your class declaration or after it. I prefer to place all interfaces at the top of my code.
Field declarations inside your interface definitions can use all internal data types including standard types (e.g. string, number, boolean, any, undefined, unknown, etc.) or other custom interface types (e.g. address: IAddress).
Sometimes you may have fields in your interface that are optional. Optional fields can be defined by placing a question mark immediately after the field name (e.g. nameMiddle?: string;). If using optional fields in your type definitions you must take care to check for the existence of your field before using it. (e.g. {{ person.nameMiddle ?? 'No Middle Name' }} ).
Lets define our interface to show the benefits of improved syntax checking.
-
Open people.service.ts and add the following code directly after the import statement
and before the @Injectable({ statement.
// IPerson Interface export interface IPerson { nameFirst: string; nameLast: string; nameMiddle?: string; age: number; gender: string; }
This interface represents the fields we have in our person record with the addition of a nameMiddle field to represent the middle name of a person. -
Next, change the "public people = [" line, so it is declared as an IPerson array. You can also add some
middle names if you would like. You should see the field name "nameMiddle?" appear in your context-sensitive
help as the editor knows your data is a "IPerson" type.
public people: IPerson[] = [ {nameFirst: 'John', nameLast: 'Simpson', age: 51, gender: 'Male'}, {nameFirst: 'Bob', nameLast: 'Smith', nameMiddle: 'Barney', age: 36, gender: 'Male'}, {nameFirst: 'Mary', nameLast: 'Smith', age: 32, gender: 'Female'}, {nameFirst: 'Theresa', nameLast: 'Banner', age: 25, gender: 'Female'} ];
At this point, the people object is now type protected inside the service class. This means that inside people.service.ts your ability to make syntax mistakes is greatly reduced. Go ahead and try to add an invalid field, misspell a field name or even omit one of the required fields to see your editor respond (be sure to correct these mistakes before moving on). With the errors above, not only will the editor complain, the running development server will error out and you will not be able to compile your application until the mistakes are corrected. -
Our people object is now protected inside of people.service.ts, however it is still
being treated as an "any" type when used as an input in the name-card.component.ts
file. Correct this by changing the input line in name-card.component.ts to the
following.
@Input() person!: IPerson;
The "any" type accepts all data types but eliminates the syntax checking that makes TypeScript so great. When we changed our person type from "any" to "IPerson", the data type of "undefined" was immediately eliminated as a valid option. Unless assigned in the constructor, TypeScript does not know if the data type being passed to it from outside the component is really an "IPerson" data type. By adding an exclamation mark to the end of the field name (person!) we are notifying the editor that a data type of "IPerson" will be sent to the person input property. -
At the moment you save this latest change your application will complain of a syntax error in
name-card.component.html. Hover over the error where you will see a message stating that
type 'number' is not assignable to type 'string'.
This error occurs because our app-labeled-field component expects only string values as valid values for the
"value" property and our editor knows that the "age" field in our person object is a number. In this case,
we want to display our value if it is a number or string. To do this, open
labeled-field.component.ts and use the "or" operator "|" to allow both string and number
values as valid options for the "value" field.
@Input() value: string | number = '';
- Go ahead and view your application in the browser. It should appear on screen with no errors.
Creating and applying your own data types in Angular takes a little of work but go a long way to reducing syntax errors. In addition to error prevention, you as the developer also benefit from an improved editor experience with auto-completion enhancements as your field names appear as you type.
Not Just for Data
While being able to share data across components with Angular services is fantastic, being able to share method calls is equally important. Methods can be created in your Angular service in the same manner as any other class. Simply provide the scope of the method (e.g. public or private), the name of the method, incoming parameters and a return value.
-
Create a new public method in your people.service.ts file called peopleByGender. This
class can be anywhere between the "export class PeopleService {" and the closing "}" for the class. I typically
place new methods after the constructor as shown below.
export class PeopleService { public people: IPerson[] = [ ... values for our people ... ]; constructor() { } // Retrieve array of people filtered by gender public peopleByGender(gender: string): IPerson[] { return this.people.filter((person: IPerson) => { return person.gender === gender }); } }
Our method above takes an incoming string value called gender and outputs an array of IPerson objects. While in the method, we use the JavaScript array filter method with an arrow function to return our filtered list of people.
info Arrow Functions
Arrow functions are widely used in TypeScript and are something you should get used to seeing and using in your projects. Arrow functions are essentially inline methods that perform a task within the context of their containing element.
You may recall from our Change Detection (Part 1) section that we used an arrow function as the code being called to increment our counter.
setTimeout(() => { this._counter += 1; });
This could have easily been a call to another named method such as "addToCounter" as shown below.
setTimeout(this.addToCounter()); private addToCounter(): void { this._counter += 1; }
The primary reason we wouldn't do this and use an arrow function instead is that having a addToCounter() method would get sloppy and lead to spaghetti code. An arrow function keeps the necessary logic inline with the task we are trying to accomplish.
Arrow functions can be placed anywhere in your TypeScript code but are especially useful when it comes to dealing with arrays. Arrays have a number of extremely useful methods such as find, filter, sort, and forEach. These methods combined with arrow functions allow a simple approach to manipulating your array data.
To use an arrow function with an array method, you need to pass at least one parameter. This parameter represents a single row of data from your array.
this.people.filter((person: IPerson) => { your code });
In the example above, every row of data in your array is read and passed as a parameter called person to the block of code written inside your arrow function. In our example, we are returning true for every row where the "person.gender" value is equal to the "gender" being passed in the string of our peopleByGender method.
public peopleByGender(gender: string): IPerson[] { return this.people.filter((person: IPerson) => { return person.gender === gender }); }
Along with the object row parameter (person: IPerson), an additional index parameter can be assigned to your arrow function. This index parameter represents the zero based array row being processed in your arrow function.
this.people.filter((person: IPerson, index: number) => { your code });
-
Open app.component.html and add the following line of code to see our new method in
action. This line can go at the bottom of the file.
<h2>Males</h2> <app-name-card *ngFor="let person of peopleService.peopleByGender('Male')" [person]="person"></app-name-card>
- When you view your page you should see a list titled "Males" that only show people with a gender of "Male"
Our code above is working as designed however we have a flaw in our design. If you recall from our lesson in change detection, the peopleByGender() method is being called on every change detection cycle. This isn't a big deal when we only have four people in our array, but what happens to our application performance when we have more?
To remedy this situation, you could add code to track the last results retrieved in your service and only collect the data once however Angular offers different change detection strategies that are more suitable for this task.
These change detection strategies will be covered in the next section. First however, we need to make a few more changes to set ourselves up for the next two sections by creating a new component and moving our code out of the app.component.ts file.
warning Keep app.component.ts and app.component.html simple
You may be feeling that our Angular application has become restrictive due to having the majority of our code in the main app.component files. Even though we are just performing a simple display of values, it is easy to see that our application is not scalable.
You should limit app.component.ts to initialization code and your app.component.html should be limited to global HTML elements such as a menu and/or footer along with a way to present your starting component. When using routing later, it's not uncommon to have app.component.html nothing more than the following content:
<ng-container *ngIf="ready"> <app-menu></app-menu> <router-outlet></router-outlet> </ng-container>* <ng-container> is a very useful Angular directive that lets you add conditional logic to your HTML without adding any HTML elements to the DOM. In the example above, the HTML document will have no content in the HTML body until the variable "ready" is equal to true. Once "ready" = true, the <app-menu> and <router-outlet content will be embedded into the page.
Moving your app.component content to a new component
-
From your command line, create a new component called home.
ng generate component home
- Move all of the content from app.component.html into the newly created home.component.html file. app.component.html should be blank.
-
Remove the PeopleService import statement from app.component.ts and add it to
home.component.ts making sure to correct the path to be "../people.service".
import { PeopleService } from '../people.service';
-
Move all of the custom code inside the export class AppComponent block out of
app.component.ts and place it inside the HomeComponent class export inside
home.component.ts. Your final home.component.ts should
have the following code:
import { Component, OnInit } from '@angular/core'; import { PeopleService } from '../people.service'; @Component({ selector: 'app-home', templateUrl: './home.component.html', styleUrls: ['./home.component.scss'] }) export class HomeComponent implements OnInit { public name = 'John Simpson'; private _secret = 'Not for you'; private _counter = 0; constructor(public peopleService: PeopleService) { } ngOnInit(): void { } // Return the secret as a standard method public secretMethod(): string { return this._secret; } // Return the secret as a get method public get secret(): string { return this._secret; } public set secret(secret: string) { this._secret = secret; } public get counter(): number { setTimeout(() => { this._counter += 1; }); return this._counter; } }
-
Verify that app.component.ts has the following code:
import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent { }
-
The final step in our preparation for the next section is to display our new home component. Open
app.component.html and add the following line to display our component.
<app-home></app-home>
- View your application in your web browser. It should be working correctly.
With our content now contained inside its own component, the scalability of our Angular application has improved significantly. We can now use our new component anywhere in our application and use it multiple times if needed.