Lesson Monday

In the last lesson we added functionality to create new Album objects through our application, and save them into Firebase. However, we can no longer view each album's detail page, because we commented out the contents of the goToDetailPage() method responsible for navigating there:

src/marketplace/marketplace.component.ts
...
  goToDetailPage(clickedAlbum: Album) {
    // this.router.navigate(['albums', clickedAlbum.id]);
  };
...

This is because we removed the id property from our Album model. Firebase automatically assigns each entry in the database an ID, regardless of whether its model contains an id property, so keeping our id property on the model would be redundant and unnecessary. Our objects don't need two ids. The code in goToDetailPage() referenced the old id attribute on the Album model, so we had to comment it out to avoid compiler errors, and test our new form.

In this lesson, we'll learn how to work with Firebase-assigned ids. This will include dynamic routing using Firebase id's, and locating and retrieving specific records from our database. This will allow us to navigate to album detail pages using Firebase's id properties.

Firebase Keys

First off, let's explore these Firebase-assigned IDs a little further. In the last lesson we saved our first piece of information to our database directly from our app. Its entry in Firebase looked like this:

new-firebase-entry

Notice the string of numbers and letters above its properties, reading -KaxOv1ecybMflsoCCwh. This is the object's Firebase-assigned id. Additionally, know that while we've been calling them id's so far, Firebase refers to these as keys. This is because Firebase stores our data in key-value pairs. -KaxOv1ecybMflsoCCwh is the key. Its corresponding value is our Album object. Each property in the Album object is yet another key-value pair, too.

Accessing Firebase Keys

But how can we anticipate what key Firebase will assign to an object? And how do we use Firebase keys in our application if we removed the id property from our model?

Well, when Firebase returns data to our application, like the list of Albums in our MarketplaceComponent it adds an extra property to each item. This property is called $key, and it contains the Firebase key that corresponds to that item's entry in the database. Let's check it out!

We'll temporarily display the $key property of each Album right in our MarketplaceComponent template, just to see what it looks like:

src/app/marketplace/marketplace.component.html
<h2>Marketplace</h2>

<div *ngFor="let album of albums | async" (click)="goToDetailPage(album)" class="panel panel-default">
  <div class="panel-body">
    <h3><em>{{album.title}}</em> by {{album.artist}}</h3>
    <h1>{{album.$key}}</h1>
  </div>
</div>

If we reload our application, we should see the $key property of each Album:

firebase-keys-on-page

Notice these match the keys in our database exactly:

firebase-keys-in-firebase

Retrieving Data from Firebase with Keys

We'll use these keys as the dynamic segment to our detail view's dynamic route. Then, when we navigate from the MarketplaceComponent to the AlbumDetailComponent we'll retrieve the key from the URL (just like we did with our own ids), and use this information to request the specific Album details from Firebase.

We'll begin by updating our goToDetailPage() method in the MarketplaceComponent. Remember, this is the method that constructs the path to our detail page route, and invokes our router to navigate there:

src/app/marketplace/marketplace.component.ts
...
  goToDetailPage(clickedAlbum) {
    this.router.navigate(['albums', clickedAlbum.$key]);
  };
...

Here, we're simply placing the Firebase $key in the :id dynamic segment, instead of the now-defunct id property from our Album model. When this line of code runs, the router will match this request to the albums/:id route, and load the corresponding AlbumDetailComponent. Let's update it next.

The AlbumDetailComponent currently contains code in both its constructor() and ngOnInit() methods that run automatically whenever a new instance is created. It looks like this:

src/app/album-detail/album-detail.component.ts
...
constructor(private route: ActivatedRoute, private location: Location, private albumService: AlbumService) { }

  ngOnInit() {
    this.route.params.forEach((urlParameters) => {
     this.albumId = parseInt(urlParameters['id']);
   });
   this.albumToDisplay = this.albumService.getAlbumById(this.albumId);
  }
...
  • The constructor provides an instance of ActivatedRoute, Location and AlbumService to the AlbumDetailComponent.
  • Then, in ngOnInit() we parse the parameters sent with the route, and gather the value placed in the :id dynamic segment of the URL in the line this.albumId = parseInt(urlParameters['id']);.
  • We call the AlbumService's getAlbumById() method, providing the id from the URL.
  • Finally, the service uses this id to locate and return the Album to display on the detail page.

However, now that Firebase is managing our Albums, we'll need to alter this slightly. Let's return to the getAlbumById() method on our service. We previously commented it out:

src/app/album-service.ts
...
  getAlbumById(albumId: number){
    // for (var i = 0; i <= ALBUMS.length - 1; i++) {
    //   if (ALBUMS[i].id === albumId) {
    //     return ALBUMS[i];
    //   }
    // }
  }
...

As you can see, it previously looped through all Albums, and located the album with the id property we needed to display in the detail page.

We could loop through our Firebase-provided list of Albums too. However, this would be unnecessarily resource-intensive. Looping through all Albums doesn't seem like a big deal when only we have 3 or 4. However, imagine what would happen if our application grew: Our method might need to loop through hundreds, even thousands of objects seeking the correct Album. That wouldn't be efficient.

Instead, we can rely on Firebase to do this work for us. We already have the $key corresponding with the Album we need. Using this information, we can request the object from Firebase. It can then handle searching our database, locating the object, and returning it to our application.

To do this, we'll update our getAlbumById() method like this:

src/app/album.service.ts
...
  getAlbumById(albumId: string){
    return this.database.object('albums/' + albumId);
  }
...
  • Notice albumId is now a string, not a number. Firebase keys are strings.

  • Additionally, we're now calling this.database.object() instead of .list(). This is because we're requesting only a single object from Firebase, not an entire list.

  • We're also including albumId as an argument to the object() method. This is because we need to tell Firebase where to look for our object. Remember, each database entry is located under its key. All entries are also nested in a larger albums table. Therefore we specify "albums/" as the location. This prompts Firebase to look in our Albums list, for the Album residing under whatever key we provide this method.

Next, we'll need to make sure similar changes are reflected in the AlbumDetailComponent:

src/app/album-detail/album-detail.component.ts
...
import { FirebaseObjectObservable } from 'angularfire2/database';
...

export class AlbumDetailComponent implements OnInit {
  albumId: string;
  albumToDisplay;

  constructor(private route: ActivatedRoute, private location: Location, private albumService: AlbumService) { }

  ngOnInit() {
    this.route.params.forEach((urlParameters) => {
     this.albumId = urlParameters['id'];
   });
   this.albumToDisplay = this.albumService.getAlbumById(this.albumId);
  }

}
...
  • We import the FirebaseObjectObservable package at the top of the file.
  • We've update the albumId property to be a string here, too.
  • albumToDisplay is no longer an Album object.
  • We remove the parseInt() from ngOnInit(), because Firebase is now expecting a string key.

Displaying A Single Album

Next, we'll update our AlbumDetailComponent template. Because it will take a moment to retrieve our data from Firebase, we use the async pipe again, instructing it to wait:

src/app/album-detail/album-detail.component.html
<div>
  <h3>{{(albumToDisplay | async)?.title}}</h3>
  <h4>{{(albumToDisplay | async)?.artist}}</h4>
  <p>{{(albumToDisplay | async)?.description}}</p>
</div>

You may notice that we are using a new operator, the ?. This operator is know as the existential operator or safe navigation operator. We use it when we are unsure of whether an object will be null or have value. By using it, we tell our program that when the object to the left of it is null, do not try to access the property on the right. If the object is not null, then try to access the property on the right. The existential operator is especially helpful when dealing with responses from API calls as often times the data contains null values in unpredictable places.

An existential operator safeguards against undefined references. This is useful when dealing with asynchronous code. By including a ? between (albumToDisplay | async) and title, we instruct our browser to evaluate whether albumToDisplay is defined before attempting to retrieve the each property. Even though we apply the async pipe, we still need to use it here to make sure that our browser doesn't throw errors before the album is loaded. This is because the async pipe is being applied directly to the object and is not a part of an ngFor directive.

We used this same pipe in our MarketplaceComponent to instruct Angular to wait for Firebase to return our list of Albums.

Finally, if you haven't already, we can remove code displaying our Album keys from the marketplace:

src/app/marketplace/marketplace.component.html
<h2>Marketplace</h2>

<div *ngFor="let album of albums | async" (click)="goToDetailPage(album)" class="panel panel-default">
  <div class="panel-body">
    <h3><em>{{album.title}}</em> by {{album.artist}}</h3>
  </div>
</div>

Now, if we reload and reserve our application, we can see we are now able to navigate to an Albums detail view, and see the Firebase key in our URL. Additionally, our async pipes are correctly waiting for Firebase to return data.

Nice work! In upcoming lessons we'll continue exploring how we can communicate with Firebase. We'll add the ability to update and delete database entries through our application.