We need to make our collection of Album
s accessible throughout our application. That way, the Album
object properties are available to view in its detail page. To do this we need to add a service. In Angular, services rely on the concept of dependency injection we just covered.
In this lesson, we'll walk through creating a service, injecting it throughout our program, and retrieving information from a service in our components.
Let's jump right in and craft our new service! We'll discuss the details and specifics of services, and Angular-specific dependency injection along the way.
Our list of Album
s currently resides in the MarketplaceComponent
. Let's separate this into its own file for the service to manage instead. Create a new file in src/app folder called mock-albums.ts.
We'll import our Album
model at the top of this file:
import { Album } from './album.model';
And move our array of Albums
from the MarketplaceComponent
to our new file:
import { Album } from './album.model';
export const ALBUMS: Album[] = [
new Album("Pulse", "Pink Floyd", "A live album by the English progressive rock band originally released in 1995, on the label EMI in the United Kingdom.", 1),
new Album("Funhouse", "The Stooges", "The second album from the American rock band, released in 1970 by Elektra Records.", 2),
new Album("Twilight of the Thunder God", "Amon Amarth", "Seventh album by the Swedish band, released in 2008, based on Thor's battle with the serpent Jörmungandr.", 3),
new Album("Dilate", "Ani DiFranco", "Her highest-selling and most acclaimed album, released in 1996.", 4),
new Album("Chopin - Complete Nocturnes", "Brigitte Engerer", "Released in 2010, this is Engerer's own rendition of the classical composer Chopin.", 5),
new Album("Axis Bold As Love", "The Jimi Hendrix Experience", "Second studio album by the English-American band, released in 1967.", 6)
];
Notice we are using the const
keyword to make this array a constant. We are naming the array of Albums
in all capitals because this is standard naming convention for constants.
We'll keep the albums
property in the MarketplaceComponent
, but we'll simply declare it, instead of defining it:
...
@Component({
...
})
export class MarketplaceComponent {
albums: Album[];
...
Next, we'll create the file responsible for defining our service. We can generate this with Angular CLI using the following command:
$ ng g service album
Which should be met with a response like this:
installing service
create src/app/album.service.spec.ts
create src/app/album.service.ts
This creates the following file, and a test file we won't use:
import { Injectable } from '@angular/core';
@Injectable()
export class AlbumService {
constructor() { }
}
Notice this is structured similarly to a component file. It begins with imports statements, ends with an exported class, and includes a decorator.
But notice the decorator reads @Injectable
, not @Service
. This decorator makes the class available to the injector responsible for delivering this service wherever it's needed in the project.
Next, let's import our Album
s into the file, to give our service access to theAlbum
model, and the list of Album
objects from mock-albums.ts:
import { Injectable } from '@angular/core';
import { Album } from './album.model';
import { ALBUMS } from './mock-albums';
...
We must also define a method responsible for retrieving the Album
s from the list in mock-albums.ts, so the service may provide this information wherever it's injected. It'll look like this:
import { Injectable } from '@angular/core';
import { Album } from './album.model';
import { ALBUMS } from './mock-albums';
@Injectable()
export class AlbumService {
constructor() { }
getAlbums() {
return ALBUMS;
}
}
As you can see, it simply returns the list of Album
s we imported into the file in the line import { ALBUMS } from './mock-albums';
Our MarketplaceComponent
file needs access to these Album
s, since we removed the array from its class a moment ago. Because the service is now responsible for managing this information, we'll inject our service here.
First we'll import the new service into the MarketplaceComponent
's file:
import { AlbumService } from '../album.service';
...
Then we'll add the service as a parameter in its constructor. The MarketplaceComponent
should already have a constructor()
method with one parameter, a Router
object. We'll add AlbumService
as a second parameter:
...
constructor(private router: Router, private albumService: AlbumService) {}
...
Just like the Router
already present, this ensures all new instances of MarketplaceComponent
also have an instance of AlbumService
, accessible by calling this.albumService
anywhere in the MarketplaceComponent
class.
Next we'll need to add a new property to the component's decorator called providers
. It will correspond with an array containing our AlbumService
:
...
@Component({
selector: 'app-marketplace',
templateUrl: './marketplace.component.html',
styleUrls: ['./marketplace.component.css'],
providers: [AlbumService]
})
...
As discussed, Angular's dependency injection system has three primary parts: Services, Injectors and Providers. The providers
array above tells Angular to create a new instance of AlbumService
when it creates a new MarketplaceComponent
.
Listing a provider in a component is known as registering a provider. The providers
array tells Angular to create a fresh instance of AlbumService
when it creates a new MarketplaceComponent
. The component may then use this service to access the data it controls, like our list of Album
s.
So far we have:
A source of data. This could be an API, or a database in other applications, but for us it's simply a mock-albums.ts file.
A service that imports that data, and contains a getAlbums()
method to return it.
A component that has both registered the service as a provider, and has an instance available to it (because it was included in the constructor()
)
So, we have a path in place for data to travel into the MarketplaceComponent
, but we still need to call the service's getAlbums()
method somewhere to retrieve that data.
Where do we call it? Well, we don't want the user to have to click a button, or submit a form to receive the list of Album
s in the MarketplaceComponent
. They should just be there already when it loads!
Thankfully, there's a place we can call the getAlbums()
method automatically: ngOnInit()
!
We previously removed the interface and lifecycle hook method from this component, but that's alright! We'll add it right back in. First, we'll import the interface:
import { Component, OnInit } from '@angular/core';
...
We'll implement the interface in the class declaration:
...
export class MarketplaceComponent implements OnInit {
...
And we'll define an ngOnInit()
method in the class declaration:
...
export class MarketplaceComponent implements OnInit {
albums: Album[];
constructor(private router: Router, private albumService: AlbumService){}
ngOnInit(){
}
goToDetailPage(clickedAlbum: Album) {
this.router.navigate(['albums', clickedAlbum.id]);
};
}
Within ngOnInit()
we'll retrieve our service, and call its getAlbums()
method, like this:
...
ngOnInit(){
this.albums = this.albumService.getAlbums();
}
...
Here, we're redefining the component's existing albums
property as the result of our new service's getAlbums()
method (which returns the Albums
array in our mock-albums.ts file).
The completed file should now look like:
import { Component, OnInit } from '@angular/core';
import { Album } from '../album.model';
import { Router } from '@angular/router';
import { AlbumService } from '../album.service';
@Component({
selector: 'app-marketplace',
templateUrl: './marketplace.component.html',
styleUrls: ['./marketplace.component.css'],
providers: [AlbumService]
})
export class MarketplaceComponent implements OnInit {
albums: Album[];
constructor(private router: Router, private albumService: AlbumService){}
ngOnInit(){
this.albums = this.albumService.getAlbums();
}
goToDetailPage(clickedAlbum: Album) {
this.router.navigate(['albums', clickedAlbum.id]);
};
}
And, if we build and serve our application, we should see the Marketplace page still lists all our Album
s, just like before, even though this data is now being gathered from our new service. Nice work!
However, we also need to use our service in the AlbumDetailComponent
. That way, when the user clicks an individual Album
in the MarketplaceComponent
they may navigate to a page displaying all the details for that particular Album
.
We can currently display the Album
id
from the URL in the AlbumDetailComponent
. However, now that we have a service, we can inject the service into the AlbumDetailComponent
, and retrieve the entire Album
object that matches the id
from the URL.
To do this, we'll follow the same steps to inject a service as we did above:
ngOnInit()
.We'll import the service into the AlbumDetailComponent
:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { Location } from '@angular/common';
import { Album } from '../album.model';
import { AlbumService } from '../album.service';
...
Add a providers
array containing AlbumService
:
...
@Component({
selector: 'app-album-detail',
templateUrl: './album-detail.component.html',
styleUrls: ['./album-detail.component.css'],
providers: [AlbumService]
})
...
Add the service as a parameter to the constructor:
...
constructor(
private route: ActivatedRoute,
private location: Location,
private albumService: AlbumService
) {}
...
(When constructor()
parameter lists get long, you can enter line breaks between the items for easier readability, if you'd like).
Now we can gather the specific Album
object the user has requested to view from the service in the component's existing ngOnInit()
method. However, right now our service only contains a method to return all Album
s. Let's define another method that can return a specific Album
:
import { Injectable } from '@angular/core';
import { Album } from './album.model';
import { ALBUMS } from './mock-albums';
@Injectable()
export class AlbumService {
constructor() { }
getAlbums() {
return ALBUMS;
}
getAlbumById(albumId: number){
for (var i = 0; i <= ALBUMS.length - 1; i++) {
if (ALBUMS[i].id === albumId) {
return ALBUMS[i];
}
}
}
}
Here, we've defined a getAlbumById()
method that takes the id
of the Album
we're seeking as a parameter. It simply loops through all Albums
in our ALBUMS
constant, and returns the one with the id
we're looking for.
Now, let's define a property in AlbumDetailComponent
to contain the Album
object whose details we'd like to display:
...
export class AlbumDetailComponent implements OnInit {
albumId: number;
albumToDisplay: Album;
...
Next, we'll add logic to the ngOnInit()
method to retrieve the album AlbumDetailComponent
should display from the service, using its new getAlbumById()
method:
...
ngOnInit() {
this.route.params.forEach((urlParameters) => {
this.albumId = parseInt(urlParameters['id']);
});
this.albumToDisplay = this.albumService.getAlbumById(this.albumId);
}
...
This method passes our albumId
property into our new getAlbumById
method in our service, which returns the album we're interested in so that we can store it in the Album Details Component property albumToDisplay
.
Next, let's update the AlbumDetailComponent
's template to display details about the selected Album
:
<div>
<h3>{{albumToDisplay.title}}</h3>
<h4>{{albumToDisplay.artist}}</h4>
<p>{{albumToDisplay.description}}</p>
</div>
Finally, we should be able to build and serve our application, click on an Album
in our marketplace, and see its details:
Nice work! We've successfully injected a service that makes data available throughout our entire application.