Lesson Thursday

Look what happens when we print an object returned from Firebase in our console:

album-detail.component.ts
...
export class AlbumDetailComponent implements OnInit {
  ...
  albumToDisplay;
  ...
  ngOnInit() {
    this.route.params.forEach((urlParametersArray) => {
     this.albumId = urlParametersArray['id'];
   });
   this.albumToDisplay = this.albumService.getAlbumById(this.albumId);
   console.log(this.albumToDisplay);
  }

}

We get something like this:

firebase-object-observable-in-console

As you can see, this is a FirebaseObjectObservable object. We can expand it to see all sorts of information, but nothing about our actual object or its properties:

expanded-firebase-object-observable

Furthermore, look what happens if we attempt to access the properties of a FirebaseObjectObservable in our code:

album-detail.component.ts
...
ngOnInit() {
    this.route.params.forEach((urlParametersArray) => {
     this.albumId = urlParametersArray['id'];
   });
   this.albumToDisplay = this.albumService.getAlbumById(this.albumId);
   console.log(this.albumService.getAlbumById((this.albumId).title);
  }
...

We can't even build our application after making the changes above! We receive the following compiler error in the terminal:

ERROR in ./src/app/album.service.ts
Module build failed: Error: /Users/staff/Desktop/online-store/src/app/album.service.ts (23,72): Property 'title' does not exist on type 'FirebaseObjectObservable<any>'.)
    at _checkDiagnostics (/Users/staff/Desktop/online-store/node_modules/@ngtools/webpack/src/loader.js:145:15)
    at /Users/staff/Desktop/online-store/node_modules/@ngtools/webpack/src/loader.js:172:17
 @ ./src/app/marketplace/marketplace.component.ts 12:0-48
 @ ./src/app/app.module.ts
 @ ./src/main.ts
 @ multi main

Yet, we know properties of the database entry we're returning exist in there somewhere, because our template successfully displays them in the browser:

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

So, what is this thing anyway? It's a FirebaseObjectObservable one moment, and we can't parse our properties out from it. But when this information reaches our template, our application is then able to access its properties. Why does this happen?

This optional lesson will explore observables; including what they are, how Angular uses them, and how we can interact with them and use them to our advantage. Know that nothing covered here will be required in any code review, project, or other assessment. This lesson serves as a further exploration into topics you may find beneficial as you build more complex Angular applications; especially during team week!

Observables

As the name suggests, FirebaseObjectObservables and FirebaseListObservables are both specific types of observables. Observables are used to open continual channels of communication in which multiple values of data can be emitted over time.

Remember, Firebase is a "realtime" database. As you've seen, it continually communicates with our applications, automatically updating application-side data whenever database data changes. It uses observables (FirebaseObjectObservables and FirebaseListObservables, specifically) to do this.

Angular relies on observables in its own internal code, too. For instance, EventEmitters use observers to open channels of communication between components. And remember the async pipes we include wherever information retrieved from Firebase is displayed in templates? Read its description in the Angular 2 Documentation again:

"The async pipe subscribes to an Observable or Promise and returns the latest value it has emitted...."

We previously stated that async instructs Angular to wait for database information to return. And that's true. It does this by subscribing to the FirebaseObjectObservable or FirebaseListObservable returned from Firebase, as we saw in the console above. It then displays the most recent value sent via the observable.

Remember, FirebaseObjectObservable and FirebaseListObservable objects don't represent the actual data we're returning from the database. Instead, they represent the automatically-updating connection between our application and its database.

Subscribing to Observables

And, in the same way the async pipe subscribes to observable to return the latest value they've emitted, we can subscribe to them in our own code too! To do this, we use the subscribe() method.

Have you ever pre-ordered a game, book, or movie? subscribe() is a bit like that. The video game or book isn't available yet, but we're signing up to receive it when it's available, and to receive updates on its progress. Similarly, the object in a FirebaseObjectObservable might not be fully available yet, but we're preemptively signing up to have access and do something to it when it is available.

Subscribing to something looks like this:

album-detail.component.ts
...
export class AlbumDetailComponent implements OnInit {
  ...
  albumToDisplay;

  ...

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

     console.log(this.albumToDisplay);
   })
  }

...

Here, we've added subscribe() to the same area of code that printed a FirebaseObjectObservable to our console at the beginning of the lesson.

  • this.database.object('/items/' + albumId) will result in a FirebaseObjectObservable, as we saw a moment ago.

  • We call subscribe() upon the FirebaseObjectObservable. Note that this method can only be called upon Observable objects.

  • Also, notice subscribe() uses the fat arrow notation we discussed previously, in the Dynamic Routing lesson.

  • The dataLastEmittedFromObserve variable in subscribe()'s parameters represents the data passed from Firebase using the FirebaseObjectObservable. Remember, the observer itself is not the data being provided by Firebase. Instead, it's the pipeline by which that data enters our application.

Now let's see what our console.log() prints:

object-after-subscribing

And look! It's a plain old object. All the properties we need are right there!

Observables and Templates

But the album details are no longer visible on the page! This is because our template still includes async pipes:

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

Remember the description of async pipes from the Angular documentation? They handle subscribing to Observables for us. However, we're now manually subscribing to them with the subscribe() method. We don't need to subscribe twice, so we'll remove the async pipe:

album-detail.component.html
<div>
  <h3>{{albumToDisplay?.title}}</h3>
  <h4>{{albumToDisplay?.artist}}</h4>
  <p>{{albumToDisplay?.description}}</p>
</div>

In addition to being cleaner template code, this also creates room for us to implement our own custom pipe if we'd like!

Why Subscribe?

When should we subscribe to an observable? Especially if our async pipe can handle this process for us already?

Well, in Angular 2 especially, consider subscribing to an observable whenever you want to do something to data returned from Firebase before it reaches the template. For instance, notice albumToDisplay doesn't have a defined data type:

album-detail.component.ts
...
export class AlbumDetailComponent implements OnInit {
  ...
  albumToDisplay;

  ...

  ngOnInit() {
    this.route.params.forEach((urlParametersArray) => {
     this.albumId = urlParametersArray['id'];
   });
   this.albumService.getAlbumById(this.albumId).subscribe(dataLastEmittedFromObserver => {
     this.albumToDisplay = dataLastEmittedFromObserver;
     console.log(this.albumToDisplay);
   })
  }

...

This is because Firebase does not record model types; only their data. (Notice the data we just logged to the console is an Object not an Album:

object-after-subscribing

We might want to turn this data to turn it back into an Album object. This would allow us to both declare a data type for albumToDisplay, and, if we had any methods declared in our Album class, it would allow us to call them upon this object. We could transform this data back into an Album instead of a generic Object within subscribe(), like this:

album-detail.component.ts
...
export class AlbumDetailComponent implements OnInit {
  ...
  albumToDisplay: Album;

  ...

  ngOnInit() {
    this.route.params.forEach((urlParametersArray) => {
     this.albumId = urlParametersArray['id'];
   });
   this.albumService.getAlbumById(this.albumId).subscribe(dataLastEmittedFromObserver => {
     this.albumToDisplay = new Album(dataLastEmittedFromObserver.title,
                                      dataLastEmittedFromObserver.artist,
                                      dataLastEmittedFromObserver.description)
   })
  }

...

Essentially, if you need to do something to a Firebase entry, but you can't access its properties or call class-specific methods on it because it's an Observable, consider using subscribe().

Or, if you want to use your own custom pipe in a template, consider subscribing to your data manually instead of relying on async. This will allow you to apply your own pipe in the template instead.

Lastly, know that observables aren't exclusive to Angular or Firebase, either! They're actually a new proposed standard for managing asynchronous data, and will be included in the ES7 version of JavaScript. There are already JavaScript libraries like RxJS that allow developers to utilize observers in almost any type of project, in any JavaScript framework.

Additional Resources

For more information about observables, check out the RxJS documentation. This is the same library Angular uses to handle its own observables, too (check out the package.json automatically-created by Angular CLI. You should see rxjs included here.)

For further reading about how Angular in specific uses observables, check out the Using Observables article from Rangle.io.