Skip to content

Navigation and Routing Additional Techniques

Adarsh Kumar Maurya edited this page Dec 10, 2018 · 1 revision

Introduction

Now that we know the basics of routing, we are ready for more. Welcome back to Angular: Getting Started, from Pluralsight. My name is Deborah Kurata, and in this chapter, we look at several additional routing techniques. With what we learned in the prior chapter, a user can now navigate to any defined route in our application, but that only covered the very basic routing scenarios. What if we need to pass parameters to a route or activate a route with code instead of with a clickable element? Or what if we need to protect a route and only allow access to it in special cases or by certain users? Or ask a user to save changes before leaving a route? In the second chapter on routing, we examine how to pass parameters to a route, how to activate a route with code, and how to protect our routes with guards. When we're finished with this chapter, we'll know how to handle additional routing scenarios, including routing to a component that requires parameters, such as our product detail component. Let's get started.

Passing Parameters to a Route

We sometimes need to pass parameters to a route. For example, to navigate to the Product Detail view, we need to define which product's details to display. The first step to passing parameters to a route is to configure the route with parameters. We've already done this step to route to our product detail component. Here we define a slash, a colon, and a placeholder for the parameter. If multiple parameters are needed, we'd repeat this with another slash, colon, and placeholder. With the route definition in place, we can decide where we want the user to activate this route. Will we add a menu option or a data link? It is there we set the routerLink and pass in the required parameter. In the product list component template, we display a table of products. Each table row contains the product name, so we add a routerLink to this anchor tag and assign it to the link parameters array. The first element to the array is the string path of the route. The second element of the array is the value for the route parameter. When the router composes the URL, it uses this array element to construct the defined parameter. To display the appropriate product, the product detail component reads this parameter from the URL. It then uses the parameter to retrieve the appropriate product and display it in the view. To get the parameter from the URL, we use the ActivatedRoute service provided by the router. We want an instance of the service, so we'd define it as a dependency in our constructor. We've seen this syntax before. This line of code defines a private variable called route and assigns it to the instance of the ActivatedRoute provided by the Angular service injector. Then we use the instance of the ActivatedRoute service to get the desired parameter. There are two different ways to get the parameter. We could use a snapshot or we could use an observable. Use the snapshot approach if you only need to get the initial value of the parameter. The code is then a one-liner, as shown here. In our example, the user is always returned to the list page before navigating to another product, so the snapshot approach would be sufficient. If you expect the parameter to change without leaving the page, use an observable. For example, if we had a Next button on the Product Detail page to display the next product, the URL will change to the next product's ID, so you'd want to use an observable instead. We use the ActivatedRoute snapshot method here and access the appropriate parameter from its parameter array. The string specified here must match the name of the parameter from the path. Let's give this a try.

Demo: Passing Parameters to a Route

In this demo, we pass a parameter as part of the product detail route and read the parameter in the product detail component. We already have the product detail component's route configured with a parameter. Looking at that route, here in the path we provide any parameters prefixed with a colon and separated by slashes. For the product detail component, we pass a product ID so the view knows which product's detail to display. Next, we need to decide where to tie this route to a user action. We can't add the product detail to the menu because we don't have an easy way for the user to specify the ID of the desired product. Let's instead modify the list of products in the Product List view, such that each product name is a link. Then the user can click on the product to display its details. In the product list template, we'll add an anchor tag so the product name becomes a link. We add the routerLink directive to the anchor tag and set up the link parameters array. In the array, we define the path of the route to active in the first element and pass the parameter value in the second element. In this example, we want to pass the product's ID. When we view the result in the browser, we see that the product names are now links. And if we click a link, we see the ID and the URL, and we navigate to the product detail component, but that component does not yet get the parameter from the URL. Let's do that next. We add code in the product detail component to get the parameter passed in on the URL. We use the ActivatedRoute service provided by the router to help us. First, we import the service. We don't have to register this service because it is registered as part of the router chapter we added to the imports array in the last tutorial chapter. We then set ActivatedRoute as a dependency by defining it as a parameter to the constructor function. The ActivatedRoute instance is then injected into this component class. Now let's get the ID from the route and store it in a local variable. We'll use the snapshot approach here because we don't expect the URL to change. Where do we put the code to read the parameter? We don't want it in the constructor, we'll instead use the OnInit lifecycle hook. We start by reading the parameter into a variable. We use let here, which is new in ES2015, and defines a block scoped variable. We then use this.route.snapshot to get the parameter. We pass in the name of the parameter we want to read. The parameter name we defined in the route configuration is id, so that's the parameter name we specify here. And because the parameter is provided as a string, we'll add a plus here at the beginning. The plus is a JavaScript shortcut to convert the parameter string to a numeric ID. At this point, we could add code here to retrieve the desired product using this ID. But since we are focused in this chapter on routing, and not on HTTP, we'll hardcode the product here. To make it easy to see the ID we got from the URL, let's display it as part of the page title. Here we use the ES2015 backticks to define a template string and display the ID. Let's see the result in the browser. Click the Menu option to display the product list, then click a product. The URL changes to include the parameter, and the Product Detail page is displayed. If everything worked, the title displays the same parameter as shown in the URL. Yay! To get back to the product list, we could use the Menu option here, but it would be nicer to have a Back button. Let's add a Back button and see how to activate a route with code.

Activating a Route with Code

We want to add a Back button to our Product Detail page that navigates back to the Product List page. We could activate this route using the RouterLink directive in the component template, just like we did with the menu options, but it's also possible to route with code, so let's try that out instead. When would you want to navigate with code instead of the RouterLink directive in a template? One example is a Save button where you need to execute some code to save the data, and then route. To route with code, we use the Router service. We import the Router service from angular/router. We define a dependency on the Router service using a constructor parameter. The router instance is then injected into this component class. Every time we inject a service dependency into a class, we should ask ourselves, hmm, did we register this service with the Angular injector? In the case of router, it is registered in router chapter, which we added to our application's Angular chapter imports array. We use this router instance to activate a route. Here we define a method that we can call from the template based on a user action. The code uses the navigate method of the Router service and passes in the same link parameter array we used when binding the RouterLink. In this example, we route to the products route and don't pass any arguments. Let's give this a try. Here we are looking at the product detail component. We want the product detail component to navigate back to the product list component. We define the router as a dependency by adding another constructor parameter. When this component class is constructed, we'll get an instance of both the ActivatedRoute service and the Router service. Now we can build a method that navigates with code. Since the purpose of this method is to navigate back, we'll call it onBack. It doesn't return anything, so we'll set its return type to void. In this method, we use the this.router instance and call the navigate method. We pass it a linked parameters array. In this example, we want to navigate back to the product list component, and we don't need any parameters. We just define the route path, which is products. In the product detail template, we'll add a button. We again use the Twitter Bootstrap style classes to give the button some style. We use event binding to bind the click event of the button to the onBack method we defined in the class. Now let's check it out in the browser. Click on the Menu option to display the product list, then click on a product. The Product Detail page is displayed. We click our Back button, and we are back on the Product List page. Let's try another one. We see the Detail page. Now we're back on the Product List page. Our code-based navigation is working. So routing with code involves importing the router and using its navigate method to activate the route. Now that we have several routes in place, let's look at how to protect them with guards.

Protecting Routes with Guards

There may be times that we want to limit access to a route. We want routes only accessible to specific users, such as an administrator, for example, or we want the user to confirm a navigation operation, such as asking whether to save before navigating away from an edit page. For that, we use guards. The Angular router provides several guards, including CanActivate to guard navigation to a route, CanDeactivate to guard navigation away from the current route, Resolve to pre-fetch data to before activating a route, and CanLoad to prevent asynchronous routing. In this clip, we work through how to implement the CanActivate guard. You can use the same techniques we're covering here to implement any other type of route guard. We'll build a guard that prevents navigation to the product detail route, unless a specific condition is true. Building a guard clause follows the common pattern used throughout Angular. Create a class, add a decorator, and import what we need. Here we define a guard class. Since we are implementing this guard as a service, we use the Injectable decorator. This class implements CanActivate. To create one of the other kinds of guards, change this to implement one of the other guard types. We then implement the CanActivate method. For simple cases, this method can return a Boolean value, true to activate the route, and false to cancel the route activation. For more complex cases, we could return an Observable or a promise from this method. Using a guard is simple. We build the guard to protect the product detail route, ao we add the guard to the product detail route. We add CanActivate and set it to an array containing the guards to execute before this route is activated. In our case, there is only one. Let's give this a try.

Demo: Protecting Routes with Guards

In this demo, we protect our product detail route with a guard. We want to build a guard that prevents navigation to the product detail component if the provided URL parameter is not valid. Recall that the route definition for the product detail component includes an ID, but there is nothing here that defines this ID to be numeric or greater than 0, so our guard should prevent navigation to the product detail route if the ID is 0 or not a number. Our first step is to build our guard. We could build it manually, but why not use the Angular CLI? We open the integrated terminal and type ng for the Angular CLI, g for the generate, g for guard, and the name of our guard. Since this guard is for the product detail route, we add it to the products folder, so products/product-detail. That's all that is required. Press Enter and we see that the CLI created the guard and the start of a unit test for that guard. Let's open the resulting guard. The CLI added all of the basic boilerplate here for us. Yay! Let's talk through it. Since a guard is a service, it needs to be registered with an Angular injector. The CLI registers this guard with the route application injector, using the providedIn property. By default, the CLI generates the code for the CanActivate guard. It implements the CanActivate interface and builds the start of the canActivate method. We can change this as needed if we want to implement a different type of guard. The canActivate method has two parameters, the ActivatedRouteSnapshot to provide current route information, and the RouterStateSnapshot to provide router state information. The method can return an Observable, a promise or a simple Boolean value. All that's left is to write the logic for the guard. What do we want this method to do? We need to check the route URL and ensure that the ID passed in is valid. If it is not valid, we want to navigate back to the Product List page. Navigation requires the router, so the first thing we need is a constructor. Then we inject in the router, letting VS Code import the appropriate package. Next, in the canActivate, we need to read the parameter from the route. Luckily for us, canActivate has a parameter that gives us the ActivatedRouteSnapshot. The ActivatedRouteSnapshot contains the information about a route at any particular moment in time. Now I'll paste the code for the method body and we can talk through it. The product detail route URL is comprised of two segments, product and the requested ID. We only care about the ID, so we pull the path from the second element, which is index of 1. The plus here at the beginning converts the URL path string to a number. If the resulting value is not a number, or less than 1, we display an alert, direct the user to the Product List page, and return false to abort the current operation. Notice that this code is the same code we use to activate a route with code that we saw on the last clip. And here, we return true to continue activating the route. Now, we don't normally want to display an alert from our application. In a real application, we'd route to an error page that would notify the user of the problem, and optionally provide a button for navigating back to the Product List page. But this is good enough for our purposes. Next, we need to hook up this guard to the appropriate route. We add the canActivate property to the route definition for the product detail component and set it to an array. In the array, we specify each guard we want to execute when activating the product detail route. In this case, we have only one. That should do it. Let's give it a try. If we view the Product List page and select a product, our URL is valid and we navigate to the page. If we instead type in a URL that is not a number, we see our message. The product detail route navigation is canceled and we are redirected to the Product List page. It works. Use route guards any time you want to prevent access to a route, confirm navigation away from a route or preload data for a route. So let's finish up this chapter with some checklists.

Checklists and Summary

We can pass any number of parameters to a route separated by slashes. Add the parameter to the route configuration path by specifying a slash, a colon, and the parameter name. Pass the parameter value by adding it to an element of the link parameters array bound to the RouterLink directive. Read the parameter value in the navigated component using the ActivatedRoute service. Notice here that the parameter name, id in this example, is exactly the same as in the route definition. To activate a route with code, use the Router service. Be sure to import the service and define it as a dependency on the constructor. Create a method that calls the navigate method of the Router service instance, and pass in the link parameters array. Add a user interface element and use event binding to call the created method. We can use guards to prevent access to a route, confirm navigation away from a route, or to preload data for a route. To create a router guard, we build a guard service, implement the guard type, in our case CanActivate, and create the associated method. We then register the guard service provider. Lastly, we add the guard to the desired route. In this chapter, we covered some additional routing techniques, including passing parameters to a route, activating a route with code, and protecting routes with guards. But we've only just touched on the basics. If you are interested in learning more about routing, check out my Angular Routing tutorial, here on Pluralsight. You'll learn how to pass required, optional, and query parameters on a route, how to fetch data with route resolvers, how to define child and secondary or named router outlets, and more on router guards. Plus, you'll see how to improve your application performance with lazy loading. We now have routing to our product detail component. Yay! However, the product detail component and its associated template are not finished. We only wired up the bare minimum to demonstrate routing. As a homework assignment, try building the remainder of the product detail template and code. To check your answer, you can find the completed application on my GitHub as described in the First Things First chapter. There you will also find a version of the product data service that retrieves one product by ID. You'll need that to get the data for the product detail component. At this point, our AppModule is looking a little cluttered and hard to manage. Up next, let's spend some more time with Angular chapters and look at how to refactor our application into more manageable pieces.

Clone this wiki locally