Lesson Weekend

We're now able to create the URL for a dynamic route and load our AlbumDetail component when the user selects an Album to view. Next we need to display specific details about our album.

In this lesson, we'll continue our exploration of dynamic routing in Angular by passing information into a dynamic route so its component can use it. We'll discuss a cool new ES6 syntax known as fat arrow notation along the way.

Retrieving Data in a Dynamic Route

Let's begin in the AlbumDetailComponent. We'll add code to retrieve the Albums id property from the URL so the detail page has the specific album information it needs.

First, we'll import the necessary packages:

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';
...
  • ActivatedRoute contains information about a route associated with a component loaded in an outlet. This is required for dynamic routing.

  • Params will help us collect parameters for use when routing. Navigating to our Album detail page requires that we send the id of the Album we're attempting to view so we import this functionality to allow us to attach extra information to our request.

  • Location helps normalize URLs when traveling between routes, especially dynamic ones. (Normalizing URLs means ensuring different paths in the application all follow the same pattern consistently).

  • Album refers to the model we created in the previous lesson.

We'll also add an albumId property to our AlbumDetailComponent. For now it's null but later we will redefine the property to correspond to an id passed via our URL:

src/app/album-detail/album-detail.component.ts
...
export class AlbumDetailComponent implements OnInit {
  albumId: number = null;
...

Let's update our AlbumDetailComponent template to display the Album's id so we may test if the id was successfully retrieved from the URL:

src/app/album-detail/album-detail.component.html
<h2>This album has id number: {{albumId}}</h2>

Next we'll add code to the component's constructor() method:

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';

@Component({
  selector: 'app-album-detail',
  templateUrl: './album-detail.component.html',
  styleUrls: ['./album-detail.component.css']
})

export class AlbumDetailComponent implements OnInit {
  albumId: number = null;

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

  ngOnInit() {
  }

}

By placing an instance of ActivatedRoute and Location in constructor(), we're ensuring that any new AlbumDetailComponent instances are created with these objects available to them.

By including the parameters constructor(private route: ActivatedRoute, private location: Location), any instance of AlbumDetailComponent will have route and location properties that can be accessed by calling this.route and this.location.

Component Lifecycle Hooks

Next, let's instruct the AlbumDetailComponent to retrieve the ID value from the URL when the component is created. To do this, we'll use a built-in Angular method called ngOnInit(). The components we create through Angular CLI come with an empty ngOnInit() method by default.

ngOnInit() is a component lifecycle hook. These are a series of methods that Angular automatically calls at certain milestones in a component's life.

Specifically, ngOnInit() is called when a component is initialized. Any code we place here will run when a component is initialized (that is, after constructor() creates it).

There are more lifecycle hooks as well. We won't cover them here, but you can check out the table in the Angular Documentation on Lifecycle Hooks.

Let's place code in our AlbumDetailComponent's ngOnInit() method now:

src/app/album-detail/album-detail.component.ts
...
  ngOnInit() {
    this.route.params.forEach((urlParameters) => {
      this.albumId = parseInt(urlParameters['id']);
    });  
  }
...

Here's what this code does:

  • Gathers the ActivatedRoute object we placed in constructor() by calling this.route.

  • Calls .params on the route to retrieve any parameters. Remember, we added parameters to the route in our goToDetailPage() method in the last lesson with the code this.router.navigate(['albums', clickedAlbum.id]);. This will return an array.

  • Loops through all the parameters using forEach(), temporarily assigning each item parameter the variable name urlParameters.

  • Retrieves the number in a key-value pair in urlParameters with the key id. (This is called id because we named it id in the dynamic segment of our route's path).

  • Assigns the id value we retrieve to the AlbumDetailComponent's albumId property.

This will all occur automatically when the AlbumDetailComponent is initialized because code is in the ngOnInit() lifecycle hook method.

We're also using new notation for the callback function in forEach: the => symbol.

Fat Arrow Notation

This is called a fat arrow: =>. It's a new feature in ES6 and TypeScript that makes code cleaner and easier to read. No, we didn't make this name up.. Let's see how this new syntax functions:

If we launch the application now, we should be able to click any Album in the MarketplaceComponent in order to navigate to the Detail Page (AlbumDetailComponent) that displays the correct id:

id-passed-to-component

Let's temporarily alter our ngOnInit() method, for demonstration purposes:

...
  ngOnInit() {
    this.route.params.forEach(function(urlParameters){
     this.albumId = parseInt(urlParameters['id']);
   });
  }
...

Here, instead of the new "fat arrow" syntax, we've refactored our forEach() loop with the "old" syntax: .forEach(function(urlParameters) … instead of .forEach((urlParameters) => …

Now, let's rebuild and serve our application, and click an Album from our marketplace.

oh-nooo-no-fat-arrow-doesnt-work

And look! The template no longer displays the id even though the only thing we changed was the fat arrow syntax!

Why is this? Well, it has to do with the keyword this. In both versions of the method, we call this.albumId to retrieve the albumId property, and set it equal to the id retrieved from the URL. This allows us to display this information in the template, when we state {{albumId}}.

In the version of ngOnInit() without the fat arrow…

...
  ngOnInit() {
    this.route.params.forEach(function(urlParameters){
     this.albumId = parseInt(urlParameters['id']);
   });
  }
...

this refers to the function itself. That is, ngOnInit(). As you know, the function simply doesn't have an albumId property. We want to define the albumId property outside of the function.

However, in the fat arrow notation version…

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

this refers to the AlbumDetailComponent as a whole. The AlbumDetailComponent does have our albumId property that we can define. Therefore, the {{albumId}} in its template successfully renders the id in the browser.

The fat arrow => notation does not set a local copy of this. Therefore, this still refers to AlbumDetailComponent not ngOnInit().

You might think we could simply "capture" the correctly-scoped this outside of ngOnInit() for use later:

newThis = this;

  ngOnInit() {
    this.route.params.forEach(function(urlParameters){
     newThis.albumId = parseInt(urlParameters['id']);
   });
  }

But then ngOnInit() doesn't know what newThis is. Remember, as we learned last week, we can only access properties in our component classes by preceding them with this. So, we'd have to append another this onto newThis. Like...well...this:

newThis = this;

  ngOnInit() {
    this.route.params.forEach(function(urlParameters){
     this.newThis.albumId = parseInt(urlParameters['id']);
   });
  }

But then we face the same issue all over again! Are you beginning to see why fat arrow notation is necessary here? It refers to the correct this, allowing us to access and re-define the albumId property. .

Make sure to undo our temporary changes to your ngOnInit() method now:

src/app/album-detail/album-detail.component.ts
...
  ngOnInit() {
    this.route.params.forEach((urlParameters) => {
      this.albumId = parseInt(urlParameters['id']);
    });  
  }
...

Moving on, now that we can display an Album's id property in our AlbumDetailComponent template, we probably want to display more interesting things too, right? Like the description, and other details?

Well...unfortunately we cannot pass entire objects over URLs. Only a piece of text (like our id) may fill a dynamic route's dynamic segment. Not an entire object.

Instead, we'll need to create something called a Service. A service allows data, like our array of Albums, to be available to all parts of an application. This means we can continue passing the id to the dynamic route, then to refer to the service to retrieve the rest of the Album object corresponding with that id.

We'll learn about services, and the dependency injection model they follow in the next two lessons.