Wednesday, April 19, 2017

Angular 2 - Routing Part Deux

This is the fourth in a series of posts describing how to build an Angular 2 application from the ground up by using the Angular CLI.  Previous installments in the series can be found here, here, and here.  I'm going to build on what I did in those posts so it would probably help to at least be a little bit familiar with what I did there.  If you want to skip those first three installments you can get the code that was generated during the third installment by visiting Github (here) and switching to the routing branch.

In this installment we're going to continue learning about routing and finish up the routing we started in the last installment.  Right now we have an application that has routes configured to view all students and one individual student (by id) as well as a redirect route from our root URL and a route for when the page is not found.  That's great and everything, but we're missing a couple of key pieces.  First, our individual student page just doesn't work.  If we navigate to /student/1 (or any number for that matter) we get a blank page and if we check our console we see a bunch of errors, so what gives?  Well, our individual student component (StudentComponent) isn't designed to work with our individual student route (/student/1).  Our StudentComponent has an input property of student, but when we navigate directly to the StudentComponent via the router, there is nothing input so our template parsing fails.  We can fix it pretty easily, though, so let's do it.

First we'll create a service in the common folder.  The command for a service is ng g service studnet. Make sure to run this command from inside the common folder (D:\Dev\Learning\Angular2\cli\ProveIt\src\app\common) so that your service files get created in the right location. Unlike when it creates a component, when the CLI generates a service it does not generate a new folder for the service. Since we want it in common we need to be in common when we run the command. We want to move the students array out of the StudentsComponent and into this service so the array can be shared between components more easily. You can just copy/paste the entire students array directly from students.component.ts to student.service.ts. Once we've done that we need to import the service in the StudentsComponent, inject the service into the component through the constructor, and initialize the students array of the component so it can be displayed to the user. Here's what my students.component.ts file looks like after I do all of that:
import { Component, OnInit } from '@angular/core'; import { Student } from '../common/student'; import { StudentService } from '../common/student.service'; @Component({   selector: 'app-students',   templateUrl: './students.component.html',   styleUrls: ['./students.component.css'] }) export class StudentsComponent implements OnInit {   selectedStudent: Student;   students: Student[];   constructor(private studentService: StudentService) { }   ngOnInit() {     this.students = this.studentService.students;   }   show(student) {     this.selectedStudent = student;   } }

I want to take a quick moment to discuss the constructor up there. What you see in there is actually a shortcut to create a private variable called studentService that is of type StudentService, and also assign the StudentService provider to the studentService variable. That constructor code allows us to assign the students property that's in the StudentService to the students variable in the StudentsComponent, which we do in the ngOnInit function. The next change we need to make is actually at the module level. We need to add the new StudentService as a provider to the entire module. To do that, import StudentService in app.module.ts, then add StudentService to the providers array (it will be the only item in the array after you add it).

Now that we have our students all stored in a service we can add the piece that we need to fix the StudentComponent.  In student.component.ts we have an ngOnInit method and that's where we want to get the student whose id matches the id passed in the URL.  The first thing we need to do to facilitate that lookup is import the StudentService in this component and inject it in the constructor.  That code is identical to what we did in the StudentsComponent so I won't repeat it here.

Next we want to change the ngOnInit function to get a single student.  Since we're doing this in small steps, let's just get the first student from the students array and use it.  To do that we'll be setting this.student to the first element of the students array on the StudentService, like this:this.student = this.studentService.students[0];

Now if you navigate to /student/1 you should see the details of the student with id 1.  Huzzah!  But that's not really what we want because if you navigate to /student/2 you'll still see the details of the student with id 1.  So we need to wire up our URL to our lookup the correct student based on which id was passed.  To do that we actually need to bring a couple more pieces into our StudentComponent: ActivatedRoute and Params from @angular/router, and we want to import switchMap, Observable, and observable.of from rxjs.  Here are the imports you'll want:
import { Observable } from 'rxjs/Observable'; import { ActivatedRoute, Params } from '@angular/router'; import 'rxjs/add/operator/switchMap'; import 'rxjs/add/observable/of';

Next, we need to modify the constructor to inject the ActivatedRoute and set it to a private variable called activatedRoute (again, you can call it whatever you want, but in my code that's the name I used).

Now we want to use them in our ngOnInit function:
this.activatedRoute.params.switchMap((params: Params) => {   const internalStudent = this.studentService.students.find((student) => {     return student.id === +params['id'];   });   return Observable.of(internalStudent); }).subscribe(student => this.student = student);
Now if you navigate to /student/2 you see the details for the student with id 2.  Huzzah!  So what's that code actually doing?  I'm glad you asked!

The activated route provides information on the current route (the current "page") and the .params property of the activated route provides us the parameters that came in from the route.  switchMap is an rxjs function that cancels previous calls to the same route with different parameters.  That way if we go to /student/1, but then - before the route is finished - we change it to /student/2, the first call is cancelled and any calls to a remote API are cancelled as well.  This prevents the problem of getting the result of the first call back from a long running process after you get the results of the second call (since those calls would be asynchronous).  Inside switchMap we have a callback function that looks up a student based on their id.  The + in on the params['id'] check converts the parameter value to a number so the lookup is successful.  We're using Observable.of (another rxjs capability) to return an observable as the result of the switchMap function so we can subscribe to the observable.  Observables and subscriptions are an advanced topic (that I don't fully understand yet) that I'll cover in a future installment of this guide.  For now, just accept it.  Our .subscribe function assigns the result of the switchMap (I think) to the student property of the component.

The very last thing we need to do is change our StudentsComponent to take the user to the student detail page when the user clicks on one of the students.  Right now when they click we show the details on the bottom of the page so we want to remove that and replace the click function of our div to an anchor tag that is a routerLink.  Here's the updated markup from students.component.html:

<div *ngFor="let student of students">   <a [routerLink]="['/student/' + student.id]">{{student.id}} {{student.name}} {{student.teacherId}} {{student.age}}</a> </div>


This is pretty straightforward once you know about the [routerLink] directive.  We're still iterating through all of the students and displaying their info, but now we're doing it inside an anchor tag that applies the [routerLink] directive.  We're appending the current student's ID to the /student/ route and voila!  We have a working app.  Of course there are still quite a few mysterious parts that we'll need to clear up, but we'll get there.

The final code from this module can be found on Github (here).  Switch to the routing-part-deux branch.

No comments:

Post a Comment