Lesson Monday

We need to make our collection of Albums 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.

Creating a Service

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.

Separate Data

Our list of Albums 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:

src/app/mock-albums.ts
import { Album } from './album.model';

And move our array of Albums from the MarketplaceComponent to our new file:

src/app/mock-albums.ts
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:

src/app/marketplace/marketplace.component.ts
...

@Component({
  ...
})


export class MarketplaceComponent {
  albums: Album[];
...

Create a Service File

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:

src/app/album.service.ts
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 Albums into the file, to give our service access to theAlbum model, and the list of Album objects from mock-albums.ts:

src/app/album.service.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 Albums from the list in mock-albums.ts, so the service may provide this information wherever it's injected. It'll look like this:

src/app/album.service.ts
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 Albums we imported into the file in the line import { ALBUMS } from './mock-albums';

Injecting a Service into a Component

Our MarketplaceComponent file needs access to these Albums, 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.

Import the Service

First we'll import the new service into the MarketplaceComponent's file:

src/app/marketplace/marketplace.component.ts
import { AlbumService } from '../album.service';
...

Add Service to Constructor

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:

src/app/marketplace/marketplace.component.ts
...
  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.

Registering Providers

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:

src/app/marketplace/marketplace.component.ts
...
@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 Albums.

Automatically Retrieving Data from a Service

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 Albums 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:

src/app/marketplace/marketplace.component.ts
import { Component, OnInit } from '@angular/core';
...

We'll implement the interface in the class declaration:

src/app/marketplace/marketplace.component.ts
...
export class MarketplaceComponent implements OnInit {
...

And we'll define an ngOnInit() method in the class declaration:

src/app/marketplace/marketplace.component.ts
...
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:

src/app/marketplace/marketplace.component.ts
...
  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:

src/app/marketplace/marketplace.component.ts
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 Albums, just like before, even though this data is now being gathered from our new service. Nice work!

Injecting Services in Multiple Components

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:

  • Import the service into the component.
  • Declare the service as a provider in the component.
  • Instantiate the service in the component's constructor.
  • Retrieve data from the service in ngOnInit().

We'll import the service into the AlbumDetailComponent:

src/app/album-detail/album-detail.component.ts
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:

src/app/album-detail/album-detail.component.ts
...
@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:

src/app/album-detail/album-detail.component.ts
...
  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 Albums. Let's define another method that can return a specific Album:

src/app/album.service.ts
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:

src/app/album-detail/album-detail.component.ts
...
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:

src/app/album-detail/album-detail.component.ts
...
  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:

src/app/album-detail/album-detail.component.html
<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:

album-detail-view

Nice work! We've successfully injected a service that makes data available throughout our entire application.