What do you want to learn?
Leverged
jhuang@tampa.cgsinc.com
Skip to main content
Pluralsight uses cookies.Learn more about your privacy
Angular Routing
by Deborah Kurata
This course is all about Angular version 2+'s routing features. You'll learn how to define multiple routes, pass data to routes, preload data for your views, and more.
Start CourseBookmarkAdd to Channel
Table of contents
Description
Transcript
Exercise files
Discussion
Learning Check
Recommended
Course Overview
Course Overview
Hello. My name is Deborah Kurata. Welcome to my course, Angular Routing, from Pluralsight. This course is for Angular version 2 and above. There is more to routing in Angular than just moving the user between multiple views of an application. After covering the basics, this course details how to configure child routes to build a tabbed edit form and secondary routes to display multiple routable panels. Sometimes you need to pass a little data to your routes with parameters. You'll see how to use required, optional, and query parameters. To make your app more visually appealing, you'll walk through how to style and animate routes, and react to route events to display a spinner while a route is loading. You'll learn how to pre-fetch data required for a route and guard routes to add permissions or prevent the user from leaving a route without confirmation. And you'll discover how to improve your application startup time by asynchronously loading your routes using lazy loading. By the end of this course, you will have the knowledge you need to leverage more sophisticated routing features and support more real-world routing scenarios. I hope you'll join me on this journey through Angular Routing from Pluralsight.
Introduction
Introduction
Ah, routing, we often just use you to move the user between multiple views of an application. But you can do so much more. Welcome to Angular Routing from Pluralsight. My name is Deborah Kurata, and this course moves quickly beyond the basics to cover more sophisticated routing features to support more real-world routing scenarios. With routing, we have the tools we need to set up navigation through our application. We can define multiple routes, pass data to routes, reload data for our views, group our routes, guard our routes, add styling and animation, and even improve performance by asynchronously loading our routes. This introduction module explores the sample application, details how routing works, suggests several tips for getting the most from this course, and outlines the topics we'll cover in the remainder of this course. Let's get started.
Sample Application: Demo
Welcome to Acme Product Management. As its name implies, this application manages our company's current list of products. You may remember this application from such courses as Angular 2: Getting Started and Angular 2: Reactive Forms. The root app component provides the application shell. It has a title, a navigation bar, and an outlet where the router places the views. When I launch the application, the router activated a default route to display our Welcome page in that outlet. And the router highlights the selection in the menu so the user can keep a sense of where they are in the application. Let me refresh so you can see the route's animation. Cool! If I click on the Product List, the route guard knows I have not logged in so navigates to the Log In component's view. After logging in, I'm taken to the Product List as I had requested. I'll click Show Messages to show a secondary or auxiliary route. With secondary routes, we define multiple outlets and display routed views here or here. Our little message log is not very exciting, but imagine using this feature to display a changing toolbox or a detail view or to build a more complex UI such as a dashboard. Notice how the URL defines the content of the primary outlet and the secondary outlet called popup in parentheses. Let's see the display of the Product List again. I'll click on Home to navigate back to the Welcome page. As I click Product List, watch the little bit of delay between displaying the page header and displaying the Product List. This occurs because we display the page and then go get the data. This delay will be more pronounced for users with a slower connection or more data. Now watch as I click on the product name and navigate to the Product Detail page. We see a wait cursor, and then the page displays in its entirety. There is no awkward partial page display. This demonstrates the routing prefetch feature where we get the data for a component before displaying its template. That prevents the partial display of our pages. We can navigate back to the Product List page. Now I'll filter the list and click to show the product images. I'll again click on a product to view its detail and navigate back to the Product List page. It remembered its settings. It retained the Product List page state using query parameters on the URL. Clicking on the Edit button, we navigate to the Product Edit page. It also uses prefetching so it displays a wait cursor then the entire page. Notice the tabs here. In many real-world applications, we have more data that can fit nicely on a simple form. A common technique for breaking the data onto multiple forms is using tabs. I only show two tabs here, but you can imagine any number of tabs. The tabs are implemented using child routes. The parent route displays the outer portion of the edit page, and the child routes display the tab details. And notice the styling here. It provides an indication of the active tab. This page also demonstrates validation across multiple components. If I have a validation error here and navigate to the other page, the Save button is still disabled. Instead of forcing the user to look through each tab to find the error, an icon appears on the tab containing the error. If I try to navigate away, a route guard asks for a confirmation. This prevents the user from leaving the page accidentally and losing their changes. We'll implement all of these features and more as we navigate through this course. Now let's look at the architecture of this sample application.
Sample Application: Architecture
When building an Angular application, we break the features and user interface pieces into components. For the APM sample application, we have a main application component called App component, and a component for each primary page of the application. The Welcome page has a Welcome component. The dashed line here represents route navigation. The user can navigate from the menu defined in the App component to the Welcome page and the Welcome component. The Log In page has a Log In component. The Product List page has a Product List component. The Detail page has its own component. And we route to the Detail page from the Product List. We can navigate to the Edit page from the Add Product menu option defined in the App component or from the Edit buttons on the Product List and Product Detail pages. Our Product Edit component tabs use child routes to navigate to the Edit Info component and Edit Tags component. An auxiliary or secondary route displays the Message component side by side with the primary routed component. And we route to a Page Not Found component if a navigation request does not match any of our routes. Though we could not see it in the demo, we'll lazy load the entire set of product features to improve our application startup performance. Now that we have our sample application components defined, let's see how we organize them into Angular modules. For the APM sample application, I divided the application pieces into multiple modules--App Module for the basic application and startup pieces, the Product Module is for the product feature pieces, User Module for the pieces that manage the login and user authentication, Message Module for the message feature pieces such as the display of our message log, and Shared Module for pieces sharable across our feature modules. That's the basics of the sample application. In the demo, we saw many routing features in action. But how does routing work?
How Routing Works
Here is a simple example to walk through the basic routing process. We tie a route to a link button or menu option using a built-in routing directive called routerLink. When the user clicks on the Product List option, for example, the Angular router navigates to the products route. The browser's location URL changes to include this route segment, and we see products appear in the address bar. We can think of this URL as a serialization of the router state. Each navigation operation results in a URL change, and any URL change kicks off the router. The router looks in the route configuration for a route definition with a path matching the URL segment. The route definition includes the component to activate, in this case, the ProductListComponent. The Angular router then loads the component's template in the primary outlet. We specify the location of that outlet using the built-in routing directive called router-outlet. And the Product List appears in that location. Now that we've seen simple routing from the user's perspective, let's see how routing works from the router's perspective. The router's job begins when it sees a change to the URL in the address bar. The URL changes when the user clicks on an element that includes the routerLink directive, when the user directly updates the address bar, or when we navigate in code. The router then looks through the array of configured routes checking the path of each route against the URL segments. If the router is not able to match the URL, the navigation fails. Otherwise, the process continues with the route associated with the matching path. If the matching route defines a redirect, the redirect is immediately processed, and the revised URL is matched. The router then checks any route guards to ensure the navigation to this new route is permitted. If the route guards allow access, the router resolves and prefetches any data required by the route. Once any required data is retrieved, the router activates all components associated with the route, instantiating them if needed and places their templates into their defined router outlets. Then the router waits for the next URL change. We'll cover all of the concepts mentioned here throughout this course. Now let's take a moment and look at some tips for getting the most from this course.
Get the Most from This Course
First, the Prerequisites. To get the most from this course, it is important that you minimally know the basics of Angular version 2 or higher. This means understanding Angular modules, components, templates, and services. If you don't have the requisite knowledge, consider taking one of the introductory Angular version 2 courses such as Angular 2: Getting Started or Angular 2: First Look. You do not need extensive routing experience. We'll cover what you need in this course. Another way to get the most from this course is to join the discussion. Thoughts, comments, or questions as you watch this course? Feel free to use the Discussion tab. You can find the link to the discussion on the Pluralsight page for this course, or follow me on Twitter. It would be great to hear about your experiences with routing. There is also a blog post specifically for this course at the URL shown here. This post identifies common issues along with their solutions. If you have problems with the code for the course, check here first. There may already be a solution provided. When building web applications regardless of the technologies we use, there are often lots of steps and places where things can go wrong. That's when a good checklist can come in. I'll present checklists at the end of many of the modules and will use them as a brief review of what was covered in that module. Feel free to jump ahead to the checklist if you have any problems when coding along with the demoes. And consider referencing these checklists as you start implementing routing in your own Angular applications. Coding along through the demoes is another great way to get the most from this course. Though not required, it is often helpful to try out the presented techniques. To get you started, I've set up an public GitHub repository specifically for this course. It is called Angular-Routing, and you can find it at this URL. The starter files for the sample application are here. You can use these files as a starting point if you want to code along with the demoes. If you'd prefer to follow along with the completed sample application code, you can find that here. This is the application we saw in the demo at the beginning of this module. If you are new to GitHub, simply click this button to download all of this code as a zip file. Because this is a course on routing, I've created the majority of the components and templates as part of the starter files but added no routing. The hands-on activities then focus on setting up the routing features. By the end of this course, you'll have a completed sample application with extensive routing features. Now let's finish up this introductory module with a look at the outline for the remainder of this course.
Course Outline
In this course, we began at the beginning with routing basics. Next, we see how to route to features defined in feature modules. We examine how to configure, populate, and read route parameters, optional parameters, and query parameters. To prevent partial page display, we'll see how to prefetch the data for our component before navigating to that component using route resolvers. We examine how to set up child routes and implement a tabbed edit page. We'll look at how to group child routes that don't need to share a component using a component-less route. Then we switch gears a bit and examine how to improve our route UI with styling and animation, and how to watch routing events so we can display a spinner when loading a route. We look at how to configure secondary routes often called auxiliary routes so we can navigate to multiple views at once. We protect our routes with route guards. And to improve our startup time, we'll set up lazy loading and load one of our modules asynchronously. By the end of this course, our sample application will have a route hierarchy that demonstrates all of these features. And the meaning of the items on this illustration will be clear, I promise. We're covering a lot of awesome yet practical techniques. So let's get started.
Routing Basics
Introduction
You have to know where you are to get where you're going, so we begin at the beginning with routing basics. Welcome back to Angular Routing from Pluralsight. My name is Deborah Kurata, and in this module, we walk through the basics of routing. As we design and build our application, we provide ways for the user to navigate its pages and features. This is the purpose of routing. With routing, we define the paths through the application and the user actions required to access each path. In this module, we examine how to set up basic routing in an Angular application. And we look at the differences between HTML5 and hash-based URLs used for routing. By the end of this module, we'll have the basic application routes defined including the route to the Welcome page, the default route, and a wildcard route to the 404 Not Found page. Let's get started.
Setting up Routing
We begin by setting up the sample application. The sample application for this course is called APM for Acme Product Management. It manages a list of products that Acme sells. If you want to work with the code for this application, you can clone the repo or download it as a zip file and unzip it. To look at the completed code for this sample application, use the APM Final Files. If you want to code along with this course, start with the APM Start Files. Copy the APM Start Files to an APM folder. I've already copied the APM Start Files to an APM folder and opened that APM folder in VS Code. But feel free to use your favorite editor. We can use the terminal window provided in VS Code to run npm install and install the required packages. As you can see here, I've created the majority of the components and templates for the sample application. That way, the hands-on activities can focus on the routing features. If you begin with these starter files and code along with this course, by the end you'll have a completed sample application with extensive routing capabilities. If you'd like to walk through the steps for building the basic components and templates for this application, check out the Angular 2: Getting Started course. It looks like the install completed successfully. Before we add routing code to this application, let's run it using npm start. Here is our Welcome page, but none of the menu options work. That's because the starter files have no routing. So we can't navigate to any other page. We'll add all of the routing features we need for this application as we navigate through this course. Setting up routing requires that we define a base path, import the Angular router, configure the routes to define which route path activates which component, identify where to place the activated component's template, and activate routes to navigate based on user actions. Ready to take the first step?
Defining the Base Path
Our first step to setting up routing is to define a base path for the application. A base path is the part of the URL that specifies the application subfolder on the server. This base path is appended to the server's URL. Here the server is accessed with www.mySite.com, and the subfolder containing the application is APM. So the base path for the application is APM. Note that this is not a real website, this is only an example. This base path is used by the router when building a URL for navigation. The router uses this base path to compose the URL and locate the component, template, and module files. And it is used by the browser to prefix relative URLs when downloading and linking to scripts, images, and stylesheets. The base path is set at the top of the index.html file using a base element. For development, we often set the base path to slash, specifying that the routing and downloaded files are relative to the application root. This generates a URL such as this. Using the root base path makes sense during development, but when the application is deployed, it may be deployed to a subfolder on the server such as this. The base path must then be set to /APM/ when the application is deployed to production. How do we deal with this change when moving from dev to production? When deploying our app to production, if the application resides in a subfolder of the website, we need to reset the base path from the root defined with a slash to the appropriate subfolder. One option for making this change is to manually reset the base path in the base element each time we deploy the app to production. This option is fraught with peril as it requires that the developer deploying the app to production remembers to make the change. A better option is to use a task runner such as Gulp to automatically update the base path as part of a production build. Another good option is to use the Angular CLI. The Angular CLI is a set of command line tools for building, testing, and deploying Angular applications. It has a command line switch that modifies the base element when building the application for production. ng is the Angular CLI, build is the command, and the -- defines command options. Here we set the base-href option to /APM/. This CLI allows for abbreviated versions of these options like this. Now to prepare for routing, let's add the base path to our sample application. I have the APM Starter Files open in VS Code. In the index.html file, we add a base element just after the head tag. Since the app folder is in the same folder as the index.html file, we set the href for the base tag to slash. Looking at the application in the browser, it appears as before. Wait a minute! We've only done the first step to set up routing in this application. How are we routing to this Welcome page? Well, we aren't. Let's take a look. Our welcome.component is in the home folder. As part of this component's decorator, I defined a selector. This allows us to use the component as a nested component. And here in our app.component template, I use the selector as a directive. This displays our Welcome page as a nested component. So there is no routing going on here. Not yet. We'll remove the selector and this directive when we have routing in place. Notice all of the class attributes here. This app uses the Bootstrap CSS style classes to make our application look a little nicer. Now that we have the base path set, let's import the Angular router.
Importing the Angular Router
The Angular router is in an external Angular module called RouterModule. The RouterModule provides the router service to manage navigation and URL manipulation, configuration for configuring our routes, and directives for activating and displaying routes. The RouterLink directive ties a clickable HTML element to a route path. When a user clicks on the element, the route is activated, and the associated component's template is displayed. RouterLinkActive associates a style with the active RouterLink. And RouterOutlet defines where to display the template. We'll cover each of these directives in detail. Before we can implement routing in an application or use these directives, we need to import this RouterModule. Since RouterModule is an external module, we add it to one of our Angular modules just like any other external module. We import the RouterModule from the @angular/router library using the import statement. Then we add it to the imports array of the @NgModule decorator. We can add the RouterModule to the imports array of multiple Angular modules. In this example, we add the RouterModule to the root app.module. We'll see later how to add it to multiple feature modules. But since the router service deals with the globally shared resource, the URL location, there can only be one active router service. To ensure that there is always only one active router service, even when importing RouterModule multiple times, RouterModule provides two methods-- forRoot and forChild. RouterModule.forRoot declares the router directives, manages our route configuration, and registers the router service. We use it only once in an application normally when defining the basic application routes. forChild declares the router directives and manages our route configuration, but it does not register the router service again. We normally use this method in every feature module. By using the forRoot method only once in the application, only one router service is registered. Every other time we import RouterModule, we use the forChild method. In this example, we import RouterModule for our basic application routes so we use RouterModule.forRoot. The forRoot function takes in an array that defines our root configuration. We'll add our root configuration here shortly. But, first, let's import the RouterModule in our sample application. We define the primary application routes in the root app.module. So that is where we import the RouterModule. First, we use the import statement to import RouterModule from the router library. Then we add the RouterModule to the imports array here. Since we are defining the basic application routes, we'll call forRoot. forRoot takes in an array of route definitions. We'll add those definitions in the next clip. Since we've imported RouterModule in this Angular module, the routing directives defined in RouterModule are accessible to any components, directives, or pipes declared in this Angular module. And since we called the forRoot method, the router service provided by RouterModule is registered and available to the entire application. Our root @NgModule is now importing BrowserModule to pull in basic directives such as ngIf and ngFor, HttpModule to access the HTTP client services and get and save data. We'll see it in use when we route to our product pages a little later. InMemoryWebApiModule is a separate service developed and provided as part of the Angular documentation quick start files. We use it to simulate calls to a back-end web service to get and save product data without the need to set up a back-end web server. For more information on this service and getting and saving data, check out the Angular 2: Reactive Forms course. In that course, we walk through the create, read, update, and delete or CRUD operations. We just added RouterModule here, and we import our feature modules--ProductModule, UserModule, and MessageModule. We'll talk more about these features modules in the next course module. We are ready for the next step--configuration.
Configuring Application Routes
The router has no routes until we configure them. A route configuration defines a set of components and the route segment required to activate the component and display the component's template. Routing is basically transitioning from one route path to another. Here an empty path or a path of Welcome displays the welcome.component's template. I've used color coding here to designate routes intended for different outlets. The turquoise component template are all slated for the primary outlet. And the green component template is currently earmarked for a secondary outlet. For each outlet, only one route path can be active at a time. So if the welcome.component's template is displayed and the user navigates to the login path, the Log In component is instantiated, and its template replaces the welcome.component's template in the primary outlet. If the user navigates to the messages path, the Message component is instantiated, and its template appears in the secondary outlet. It has no effect on the primary outlet contents. We'll talk more about secondary routes later in this course. We define each path and its associated component as part of the route configuration with syntax that looks like this. A route configuration is an array of route objects. Each route object includes a path and other properties. The path property identifies the URL segment or segments for the route. Any time the address bar URL changes, the router searches this list looking for a match between the path property and the current URL segments. In most cases, we also specify the component associated with this route. It is this component's template that is displayed when the route is activated. This first route definition simply maps a specific path to a specific component. When the URL segments match welcome, the WelcomeComponent is activated, and its template is displayed in the primary outlet. When the application first loads and there are no URL segments, we want to display a default page. We specify that default using an empty path as shown here. In this example when there are no URL segments, we want the router to redirect to the welcome route. A redirect route requires a pathMatch property to tell the router how to match the URL segments to the path property. We only want this default route when the entire client-side portion of the path is empty, so we set the pathMatch to full. But redirects are not limited to use with an empty path. We'll talk more about redirects shortly. The asterisks in the last route definition denote a wildcard path. The router matches this route if the URL segments don't match any prior paths defined in the configuration. This is useful for displaying a 404 Not Found page or redirecting to another route. A few things to note: There are no leading slashes in our path property here in the configuration. Paths are case sensitive, so be sure to carefully match the casing when referring to route paths. The component property is a reference to the component, not a string name, so it is not enclosed in quotes. And since it is a reference, we need an import statement for each referenced component. A key point to keep in mind is that the order of the routes in this array matters. The router uses a first-match-wins strategy when matching route paths. This means that more specific paths should always be before less-specific paths such as the wildcard path. We can use a redirect with an empty path to define a default route. But redirects have other uses as well. For example, we can use them if our URLs ever change such as with a URL refactoring. Say we had a path of welcome, but marketing decides to reorganize this site. And now the path is home. Bummer! Now we have to change every place we activate the route to use this new path. Or we could instead configure a redirect, then none of the navigation needs to change, and any saved bookmarks using the old path will still work. The redirect will replace the URL segment welcome with home. But there is one potential gotcha here. Redirects cannot be chained. The router will do one of these redirects but not both during one navigation. So if the default route is activated, the route is redirected to the welcome route. When it matches the welcome path, it won't redirect again. Instead, we need to change the default route to also redirect to home. Redirects can be local or absolute. Local redirects replace a single URL segment with a different one such as our examples here. An absolute redirect replaces the entire URL. Redirects are local unless we prefix the URL with a slash. Ready to try out some route definitions? Hmm, where did we put our route definitions? Within the array here. In our AppComponent, we only specify the basic application routes such as the route to display our Welcome page. We'll set the path to welcome and specify the WelcomeComponent. When the application first loads, we want to default to the Welcome page, so we'll specify a default route that redirects to our welcome route. And let's define a wildcard path in case the requested URL segments don't match any prior paths defined in the configuration. We'll use this route to activate the PageNotFoundComponent and display a 404 Not Found page. This code then creates three route definitions, configures the router via the RouterModule.forRoot method, and adds the result to the app.module's imports array. When we first launch the application, there is no URL segment specified, so the router will redirect to the default route and display the welcome.component's template. But display it where? We need to identify where we want the router to display the primary routes. If we attempt to view the application in the browser at this point, we'll see errors because we have not finished setting up routing.
Placing the Template
When a route is activated, the associated component's template is displayed in an outlet. We identify where we want that routed template to appear using the router-outlet directive. In this example, we place the router-outlet under the menu. The routed component's template then appears in this location. Any one outlet can only display one routed component's template at a time. If the user clicks Home, the welcome route is activated, and its template is displayed in this primary outlet. If the user then clicks Product List, the products route is activated, and its template replaces the welcome.component template in this primary outlet. However, we can have multiple router outlets, each displaying a different routed component's template. We'll see more on multiple router outlets later in this course. Let's add the router outlet to our sample application and complete our first route. The nav element lays out our menu. We want our templates to appear in the container below the menu. So let's delete this selector here and instead use the router-outlet directive. The templates for our routed component will then appear here. While we're at it, let's also remove the selector from the welcome.component in the Home folder. A component only needs a selector if it will be used as a nested component. Since we are now routing to the welcome.component, instead of using it as a nested component, we no longer need this selector. Let's try it out in the browser. Our Welcome page comes up as before, so how can we tell if our routing is working? Look at the URL. Notice in the address bar that our URL now shows the welcome route path. Yay! What if you're coding along and don't see the welcome page? The first thing to do is open the developer tools and view the console. Are there any errors displayed? Often the first or second error listed is relatively clear on what could be wrong. The second thing to try is to recheck each step of the process. Check out the checklists presented at the end of this module and verify each step. Another option is to download the code from the Pluralsight page for this course. It has the sample code as it should be at the end of each module. Compare your code to the exercise files for module 2. Repeat these steps any time throughout this course if you don't see the expected result. Now if we delete the path and access our app with no routing path, the default route we specify kicks in, and the Welcome page again displays. If we put in an invalid path, our wildcard route kicks in, and the Page Not Found displays. In a real application, you may want something nicer here. Click Home, and we see that our menu is not yet working. Let's hook that up next.
Activating Routes
With routing, the user can navigate through the application in several ways. The user can click a menu option, link, image, or button that activates or navigates to a route. The user can type the associated URL segment in the address bar after the application URL or use a bookmark to that URL. Or the user can click the browser's forward or back buttons. The router handles any changes to the URL so the last two techniques will just work. For the first technique, we must activate the route. we use the routerLink directive to associate visual elements with route paths. The routerLink is an attribute directive, so we add it to a clickable element such as a button image or the anchor tag as shown here. When the user clicks the element, the route with the path matching the defined URL segments is activated. We bind the routerLink directive to a template expression that returns a link parameters array. Since this is an array, we need square brackets. The first element of this array is the root or parent URL segment. Be sure to include a slash before the segment. And we want this URL segment to match one of the paths from our route configuration so be sure to exactly match the path casing. Additional elements can be added to this array to specify route parameters or additional route segments. We'll look more at route parameters later in this course. Since these particular arrays only contain simple string elements, we could use the shortcut syntax and assign the routerLink to a simple string. We can then use a one-time binding and don't specify the square brackets around the routerLink directive. When the user clicks on an element with a routerLink directive, the router uses the link parameters array to compose the appropriate URL, including any provided parameters, and locates the associated route configuration. Now let's add the routerLink directive to our sample application. I already have a menu set up for this application in the app.component's template. Of these menu options, we've only defined a route configure for the Welcome page, so let's add the routerLink director to the Home anchor tag and bind it to that welcome route. We'll add more routing as we progress through this course. Let's view it in the browser. Click Home, and we are routed to the Welcome page. It works! Now our app.component template acts as a shell with a router outlet where the router can display our views. We have the basics of routing in place. Before moving on, let's take a moment to look at the difference between HTML5 style URLs and hash-based URLs when using routing.
Using HTML 5 or Hash-based URLs
When the user clicks on an element with a RouterLink, the Angular router updates the browser's address bar and history with one or more URL segments for that route. The URL segments are managed locally on the client by the router. When changing only the URL segments, the browser should not send this URL to the server, nor should it reload the page. Rather, the router locates the route with a path that matches the URL segment, activates the associated component, and displays that component's template. And if the application has multiple outlets, the router can activate multiple components and display their templates, one in each outlet. There are two ways that the Angular router can compose local URLs for routing--HTML5 style and hash-based. Let's consider the HTML5 style first. The browser's document object model or DOM provides access to the browser's history through a history object. That history object exposes useful methods that allow us to move back and forth through the user's browser history and, starting with HTML5, manipulate the contents of the history stack. The Angular router uses these history features, specifically the pushState, to update the browser's history stack and display URLs in the address bar without triggering a server request. This allows the router to make in-app URLs look like anything it wants. Since the pushState was introduced in HTML5, these are often called HTML5 style URLs. Browsers that don't support HTML5 send page requests to the server whenever the URL changes unless the change to the URL occurs after a hash symbol. The part of a URL after the hash symbol is called a URL fragment. A URL fragment is processed on the client. So when hash-based URLs are used, the routers composes the in-app URLs with a hash prefix. The browser then processes the URL fragment as an in-app URL. HTML5 style URLs are the default used by the router. They are often preferred for routing because they generate a more natural looking URL and they preserve the option to do server-side rendering later. For more information on server-side rendering, check out Angular Universal. But what happens after the app is deployed to production and a user uses a bookmark or enters an HTML5 style URL? The full URL is passed to the server, and the server will try to parse and process each segment of the URL. The server won't understand the in-app portion of the URL and will throw a 404 Not Found error. To prevent this error when using the HTML5 style URLs, configure your web server to perform URL rewriting. With URL rewriting, you configure your server to rewrite any in-app URL segments it receives to index.html so the app is properly loaded. The app then processes the original URL and routes to the desired page. How the URL rewriting is done depends on your web server. See the documentation for your web server on how to configure URL rewriting. To use hash-based URLs instead, set useHash to true as part of the route configuration. The key benefit of hash-based URLs is that they don't require URL rewriting because the server ignores everything after the hash. In this example, the server locates the default index.html file in the app folder and returns it. The app then processes the original URL and routes to the desired page. To use hash-style URLs, we pass an object into the forRoot method with any extra options we want to set. In this case, we set useHash to true. Consider using HTML5 style browser URLs where possible, but know you can use hash-based URLs if needed. Let's finish off this module with some checklists we can use as we implement basic routing in our applications.
Checklist and Summary
Checklists are a great way to recheck our understanding and our work. To set up routing, define the base path using a base element in the index.html file. Add RouterModule to an Angular module's imports array. Use the forRoot method for application routes. And be sure to use it only one time in the application. Use the forChild method for feature routes. When configuring routes, each route definition requires a path, which identifies the URL segments for the route. These are the segments that appear in the address bar. These URL segments allow the user to bookmark and navigate back to a specific component's view, a process known as deep linking. So far, all of our path properties have defined a single URL segment. We'll see additional segments when we get to route parameters. When defining a path, be sure the path has no leading slash. Use an empty path for a default route and two asterisks for a wildcard route, which is matched if no prior path matches. And casing matters. Ensure the casing of the path in the route configuration matches everywhere the route path is specified. Most route definitions also include a component. The component is a reference to the component itself. It is not the string name, and it is not enclosed in quotes. Each reference component must be imported with the import statement. And remember that order matters! The router will pick the first route with a path that matches the URL segments. Use the RouterOutlet directive to identify where to display the routed component's template. The primary RouterOutlet is most often specified in the App component's template. When a route is activated, the routed component's template is displayed at the location of the RouterOutlet directive. Once we have the routes configured, we can activate the routes based on a user action. Add the RouterLink directive as an attribute to any clickable element in a component's template. We can use them in menu options, toolbars, buttons, links, images, and so on. Enclose the RouterLink in square brackets unless you are specifically using the shortcut syntax. Bind the RouterLink directive to a link parameters array. The first element of the link parameters array is the root or parent URL segment. All other elements are values for the route parameters or additional URL segments. This module was all about the basics of routing. We examined how to set up routing including defining the base path, importing RouterModule using forRoot or forChild, configuring application routes, placing the routed component's template using the RouterOutlet directive, and activating routes using the RouterLink directive. Lastly, we looked at the differences between HTML5 style URLs and hash-based URLs when routing. So far in our sample application, we've added a route to our Welcome component, a default route that redirects to the welcome route, and a wildcard route to the Page Not Found component. Up next, let's see how to configure routes and feature modules so we can route to our product features and login.
Routing to Features
Introduction
Most non-trivial applications have their features refactored into separate Angular feature modules. Welcome back to Angular Routing from Pluralsight. My name is Deborah Kurata, and in this module, we look at how to set up routing to features defined in feature modules. What is a feature module? A feature module is an Angular module created with the express purpose of organizing the components for a specific application feature area such as products or users. Using feature modules allows us to keep our code more organized, and it makes it possible to lazy load all of the routes for a particular feature area. We'll look at lazy loading later in this course. Every Angular application has at least one Angular module by convention called AppModule. But as an application gets more features, using a single Angular module can get a little unwieldy. We have no separation of responsibilities. Here we are mixing our basic application features with our product features, our login and user features, and our message features. Using feature modules allows us to consolidate the components for a specific application feature area into their own Angular module. Here we have a Product Module for the product features such as the Product List, Product Detail, and Product Edit. We have a User Module for the login, and we could add other components here for User Management and Reset Password. And we can add more feature modules as our application grows. In this module, we look at how to set up routing differently when working with feature modules. We discuss route path naming strategies. We walk through how to activate a route with code instead of using the RouterLink directive. We look at how to access routes from feature modules and how to refactor our basic application routes into their own routing module. By the end of this course module, we'll have configured basic routing for our Log In and Product List features. Let's get started.
Setting up for Feature Routing
Setting up for feature routing uses a subset of the steps we used to set up our basic application routing. We don't need to redefine a base path because it's already defined for the entire application. We do need to import the router, configure the routes, and activate the routes based on user actions. But we don't need to identify where to place the activated components template because we can use the primary RouterOutlet we've already defined. As we saw in the last module, the Angular router is provided in an external module called RouterModule. The RouterModule provides the router service to manage navigation and URL manipulation, configuration for configuring our routes, and directives for activating and displaying routes. Before we can implement routing in any feature module, we need to again import this RouterModule. And since we have already imported RouterModule in our root AppModule, we need to ensure that we don't re-register the router service. We import the RouterModule without re-registering the router service by using RouterModule:forChild. We then pass our feature route configuration in to the array here. We've already covered how to configure routes in the last course module, and that syntax is the same for both application and feature routes. So let's jump right into a demo. Here is our product feature module. This module is currently importing SharedModule, an Angular module I created to pull in modules that can be shared across all of the feature modules. This product feature module manages all of the product components and a filter pipe as shown here in the declarations. And it provides a product data service to encapsulate the communication with the back-end server to get and post data. To add routing to this feature module, let's start by importing the router and configuring the Product List route. We first add an import statement for the RouterModule from the router library. Then we add the RouterModule to the imports array here. Since we are defining feature routes, we'll use forChild. This method takes in an array of route definitions. Recall how to configure a route? We want to define a route with a path named products that activates the ProductListComponent. That's it. Next, we need to define a user action to activate this route. We already have a menu option defined for the Product List here in the app.component template. Recall how to activate a route? If not, you can cheat because it's right here. We do the same thing for the Product List route. We add the routerLink directive and assign it to a link parameters array. The first element is our root URL segment, which is products. To ensure that the router matches the appropriate route definition, confirm that the casing exactly matches the path in the route configuration. So we've imported RouterModule, configured the route, and activated the route from a user action. Are we ready to try it out in the browser? Of course we are! Our default route redirects to the welcome route, so that comes up first. Click on Product List, and there it is. Excellent! And notice the address bar. The URL includes the root URL segment, products. Click on Home, and it displays Welcome. Going back to the Product List, we can use the Filter by. We can hide and show images, but our detail links and our Edit buttons don't yet work. We'll implement those in the next module of this course. So now we have a route path called products. How do we come up with these path names? Before we move on, let's take a moment to talk about route path naming strategies.
Route Path Naming Strategies
For clarity and to provide flexibility down the road, it is important to give some consideration to route path names, especially when working with related sets of features. This is particularly important when we get to grouping routes and lazy loading later in this course. We could just give each route a logical name. For example, the Product List route path is products. Our route to the Product Detail page could be product or productDetail/:id where :id is a route parameter that represents the Id of the product to display in the detail page. We'll talk more about route parameters in the next course module. A route to the Product Edit page could be productEdit/:id. Are these good route path names? They could be okay, especially for a small app that won't use grouping or lazy loading. But notice that even though these features are related, their route path names are not. To make the routes easier to manage and potentially group or lazy load, consider giving each related feature route a similar root path name. So we could keep the Product List route as products. But then name the Product Detail route as products/:id. Using products, plural, instead of product, singular, gives us a shared root path of products. The Product Edit could similarly be named products/:id/edit. Using common root path names clearly expresses the relationship between these features. As we configure the Product Detail and Product Edit routes later in this course, we'll use the root paths with the common root path names shown here.
Activating a Route with Code
We've seen how to activate a route using the RouterLink directive, but what if we need to execute some code before we perform the routing? For example, when the user clicks the Log Out option, we want to log the user out before navigating back to the Welcome page. To route with code, we use Angular's router service. We import Router from @angular/router. We define a dependency on the router service using a constructor parameter. Angular's dependency injector then injects the Router service instance into this component. We use this router service instance to activate a route. Here we define a logOut method that we can call from the template based on a user action. The code first does some processing to log out the user. Then it calls the navigate method of the router service and passes in the same link parameters array we use when binding the RouterLink directive. In this example, we navigate to the welcome route. So after the user logs out, they are returned to the Welcome page. Actually, behind the scenes, the RouterLink directive calls router.navigate with the provided link parameters array so everything that we can pass to router.navigate, we can assign to the RouterLink directive and vice versa. When the link parameters array contains static strings, we can use a shortcut syntax just as we did with RouterLink. We simply pass in a string instead of an array. When a route is activated using the navigate method or RouterLink, the URL is reformulated based on the current URL segments and the defined link parameters array. Some parts of the current URL such as secondary route information is retained when navigating. If we don't want this behavior, there is another navigate method we can use, navigateByUrl. The navigateByUrl method specifies a complete URL path clearing any other passed segments. Let's consider this difference with an illustration. Say our URL looks like this. The products route is activated, and the Product List component's template is displayed in the primary outlet. The information in parentheses is the serialization of a secondary route. So the messages route is activated, and the Message component's template is displayed in a secondary outlet named popup. We'll talk more about secondary routes later in this course. If we use the navigate method, the root URL segment is changed to welcome, but the secondary outlet information is unchanged. The Message component's template remains in the secondary outlet. If we use the navigate by URL method, the entire set of URL parameters is replaced with a defined path. No secondary route is specified in this example so the secondary outlet is empty. You may find that you won't use navigateByUrl very often, but it's nice to know that the option is available. Now let's implement the login and logout features in our sample application.
Activating a Route with Code: Demo
In this demo, we activate a route with code to implement our login and logout features. If you are coding along, here is your first assignment. You've already seen how to import RouterModule and configure routes in the product feature module. Now do the same in the user feature module. Define a route with a path of login that activates the login component. Pause the video now if you want to give it a try. Are you ready to see my solution? In the user feature module that's in the user folder, I imported the RouterModule here and added it to the imports array here. I used the forChild method since this is a feature module. And I configured the login path specifying the login component. Is this similar to what you came up with? We can't try this out yet because we need to first activate the route. So your next assignment is to activate the login route in the HTML. Here in our app.component template is a login menu option. Activate our new login route when the user selects this option. And if you've forgotten how, here's a hint. Pause the video and give it a try. Are you ready for my solution? I added a routerLink directive here similar to that used for the Home and Product List menu options. Remember that the path strings are case sensitive, so be sure to match the URL segment casing used here against the path entered in the route configuration. Now let's check it out in the browser. Click the Log In option, and there is our Log In form. There is no validation here so you can type in any username and password. When they are both entered, the Log In button is enabled. Click the button, and you are logged in. You can see you're logged in because the menu options change here on the right. But this is not very user friendly. After a login, we should redirect the user to another page like the Product List page. Here in the login.component file is where we perform the login. After the user enters their username and password and clicks to log in, this code logs them in. We then want to take the user to the Product List page so we need to activate the route with code as we saw on the slides. First, we import the router from the router library. Then we add a dependency on the router service using the constructor. Lastly, we use the router's navigate method. We then specify the desired route path as the first element of the link parameters array. We want to route to the Product List so we specify products here. For clarity, I'm not using the shortcut syntax. But feel free if you want to try it out. Let's check it out in the browser. Enter a username and password, click the Log In button, and we are redirected to the Product List page. It works! For extra practice, let's modify the login.component's template to activate the welcome route if the user clicks the Cancel button on the login form. Add the routerLink to the Cancel button here. To complete the Logout feature, we need to again activate a route with code to navigate to the Welcome page after the user has logged out. In the app.component template is our logout menu option. This uses event binding and calls a method in the component class. The logOut method currently logs the user out and displays a message to the console. As an assignment, modify this code to activate the welcome route. After a logout, the user should be returned to the Welcome page. Pause the video now if you want to give it a try. Are you ready for my solution? First, I imported the router from the router library. Then I added a dependency on the router service using the constructor. Lastly, I used the navigateByUrl method and specified the desired route path, in this case welcome. Note that we pass in a simple string, not a link parameters array when using the navigateByUrl. I used the navigateByUrl here instead of the navigate method to ensure that every existing parameter or secondary route is removed when the user logs out. Is that what your code looks like? Now let's try it out in the browser. Click Log In and enter any username and password. Logging in causes the Log Out option to appear. Logging out now takes us back to the Welcome page. Success!
Accessing Feature Routes
Now that we've added navigation to the Product List, Log In, and Log Out options, you may be asking yourself, How is the menu from our app.component accessing all of these product and user routes from the feature modules? The answer is here in the app.module. In the imports array, we import RouterModule and the feature modules. The router merges the application routes explicitly defined here with the routes from the product feature module and user feature module. We have not yet defined any routes for the message module. We'll look at that module in detail later in this course. The application then has access to all of the application and feature routes. Hmm, but didn't we say that the router uses a first-match-win strategy when matching routes? Doesn't this wildcard route match everything else before we get to the feature routes defined in the feature modules? Let's consider this further. In looking at our route configuration in the app.module, it seems that our list of route paths would be ordered like this. First, the basic application route path is welcome, the empty path providing a default when no path is defined, and the wildcard path matching any path that does not match the paths above it. Then the products path from the product feature module, then the login path from the user feature module. But if this were the case, the router would never find the feature routes. The wildcard would match everything but welcome and an empty path and redirect to the Not Found page. So it looks like we shouldn't be able to get to our products and login routes. But looks can be deceiving. Here is the actual order of the route paths from the route configuration defined in the app.module. How is this the order that is generated from this configuration? Any route definitions that are explicitly configured in a module such as these are processed last after any imported modules. So in this configuration, the product feature module routes are defined first, then the user feature module routes, then lastly these explicitly defined routes. That's why our current route configuration works including the products and login feature routes. Keep this ordering rule in mind as you define your app in feature routes. To simplify our root app.module, we can move these explicitly defined routes into their own routing module. Let's do that next.
Defining a Routing Module
Let's start with the Why. Why would we want to separate out routes into their own routing module? Well, it's the same basic reasons we don't put our socks in the same drawer as our office supplies. Separating our route configuration into its own module provides better organization. Socks aren't strewn amongst the paperclips and toner cartridges and notebooks. The routes are then easier to find and review or edit as needed. Just like our socks are easier to find if they are in their own clearly defined drawer. And it provides a separation of concerns. When we inventory our office supplies, we don't have a bunch of socks in our way. So let's refactor our root app.module and move our application routes into their own routing module. We start by creating a new file in the app folder called app-routing.module.ts. We'll move the app.module to the side so we can see it and close the Explorer. Since this is an Angular module, we create it like any other Angular module. We export a class, add the NgModule decorator, and import what we need. This module is for our application routes so we import the RouterModule, and we need to add it to the imports array. Let's just copy the route configuration from app.module and paste it here. Looks like we need import statements for the WelcomeComponent and PageNotFoundComponent. We can copy them from the app.module and paste them here. Now in the app.module, add the import statement for the new AppRoutingModule. Then replace the explicit configuration with the AppRoutingModule and remove the import statement for the RouterModule since it is no longer needed here. Because we removed the RouterModule from the app.module, we need to export RouterModule here. By exporting RouterModule, when the app.module imports AppRoutingModule, the components declared an app.module will have access to the router directives. Will our routing still work? What do you think? Let's try it out in the browser. The Welcome page comes up, but if we click on Product List or Log In, we are instead routed to the Not Found page. I bet you know why. Now that our application routes are in an external module, the route paths are processed in the order they are specified here so the wildcard path matches every path that is not the welcome or empty path preventing navigation to any other path below it. We need to reorder our imports array to ensure that our wildcard route is last in the list of route paths. Let's fix that now. We'll move the AppRoutingModule to the end of our imports array. Let's recheck it in the browser. And it's working again. The bottom line here is that route path order matters. There is another common syntax you may see when working with routes. Some developers prefer to define the route configuration array as a constant and then pass that constant into the forRoot or forChild method, like this. Using a constant simplifies the NgModule decorator. Feel free to use this syntax as desired. Now let's finish up this module with some checklists we can use as we route to features defined in feature modules.
Checklists and Summary
Routing to features in feature modules uses a subset of the steps we used to set up our basic application routes. Import the RouterModule in the feature module. Be sure to use RouterModule.forChild so we don't re-register the router service. Configure the routes using the same syntax as with our basic application routes, and pay close attention to the order of the routes especially when the routes are merged from application and feature modules. When naming routes, consider using a common root path name for related feature routes. This will make it easier to group routes or use lazy loading. We'll see more on grouping and lazy loading later in this course. To activate a route with code, first use an import statement to import the router from the router library. Then add a dependency on the router service using a constructor parameter. The Angular injector then provides an instance of the router service. Use the router service and call the navigate method. And specify the same link parameters array used with a RouterLink directive. The first element in the array is the root URL segment. All other elements are values for the route parameters or additional URL segments. Or use the router service navigateByUrl method and pass in a string with a full URL path. Consider separating the routes out of the root app.module into their own routing modules. This keeps the module code more organized, makes the routes easier to find and edit, and provides for separation of concerns. Simply build a new Angular module and move the routes to that module. Just take care to watch the order of the feature and routing modules in the AppModule imports array. The routes contained in each module are processed first. All explicitly defined routes are processed last regardless of where they are in the imports array. This module was all about routing to features and feature modules. We saw how to set up routing differently when working with feature modules. We discussed route path naming strategies and the benefits of giving feature routes a similar route path name. We walked through how to activate a route with code using the router's navigate or navigateByUrl methods. We looked at how to access routes from feature modules by defining them in the AppModule paying special close attention to the route order. And we refactored our basic application routes into their own routing module. We've now added routes for our Log In and Product List components. Up next, let's see how to configure routes with parameters so we can route to our Product Detail and Product Edit components.
Route Parameters
Introduction
Sometimes we need to just pass a little data to our routes. Welcome back to Angular Routing from Pluralsight. My name is Deborah Kurata, and in this module, we pass data to our routes with route parameters. One component may have data that another component needs. We can swiftly pass that data as we route from one component to the next using route parameters. Parameters are best suited for little bits of data just like a runner would not want to pass a kettle bell, we don't want to use parameters to pass large amounts of data. Instead, we pass simple Id's or keywords. As an example, a Product List component displays a list of products, each with a unique product Id. To navigate to the Product Detail component, the Product List component must pass the Id of the product to display. We pass data as part of the route using route parameters. In this module, we start with required parameters. We look at how to configure a route with required parameters, how to populate the required parameters, and how to read the parameters from the route. We then look at optional parameters and query parameters. By the end of this module, we'll have configured the parameters and routed to the Product Detail and Product Edit routes. And we'll improve the Product List route to retain its filter by and image display settings as query parameters. Let's get started.
Configuring a Parameterized Route
Before we can pass required data on a route, we need to define placeholders for that data as part of the route configuration. The first route here is our simple route to the ProductListComponent. The second route uses a similar root path name, products, and adds a placeholder for the data that must be passed to that route. The colon here denotes that this is a placeholder for a required parameter. We name this particular placeholder id because it defines the location in the URL for the Id of the product to display in the Product Detail page. But we could name it anything. The third route uses a similar placeholder because the Product Edit page also needs an Id parameter. And we add a simple text route segment to distinguish this path from the detail path. So we can see that our path expressions contain two types of URL segments--constant segments that are simple text and variable segments that are placeholders for data passed to the route. Each variable segment is identified with a colon. We can add any number of placeholders to a route definition path as long as each placeholder within a path is defined with a unique name. This first route is a simple route to an OrderListComponent. The second route uses a placeholder for the Id to pass to the OrderDetailComponent. The third route displays details for one item of an order so it needs a placeholder for the order Id and for the order item Id. The second placeholder cannot also be named Id, so we named it itemId. We can continue to add more constant and variable segments to a route definition as needed. Let's configure some route parameters in our sample application. In our sample application, we need a route parameter to pass the desired product Id to the Product Detail and Product Edit routes. We configure the product features routes in the product.module. Here is our current route to the ProductListComponent. Let's add routes for the ProductDetail and ProductEditComponents. As we discussed earlier in this course, we want to name all of our product feature routes with the same root path name, products. So our Product Detail route path is products, and we add /:id for the Id placeholder. When the user navigates to this route, we want to display the Product Detail page, so we specify the ProductDetailComponent. For the Product Edit route, we'll use the same root path name and then Id placeholder. Then we add /edit to make the path unique from the detail route. For this route, we want to display the Product Edit page so we specify the ProductEditComponent. We can't try this out yet because we don't have user actions tied to our new routes. Let's do that next.
Populating Route Parameters
Once we have our routes configured, we define the user actions that activate each route and populate that route's parameters. From our Product List page, we activate the Product Detail route when the user clicks on the name of a product. To display the details for the selected product, we'll pass the product Id on that route as a route parameter. We activate the Product Edit route when the user clicks on the Edit button in the product's row. To display the selected product for edit, we'll again pass the product Id on that route as a route parameter. We also want to activate the Product Edit route when the user clicks on the Add Product menu option. In that case, we display the edit page for entry of a new product. What then do we pass for the product Id route parameter? We can't leave it blank or it won't match our configuration. We have to pass something. We could select a number that represents a newly created product, something that would not be a valid existing product Id such as 0 or -1. The selected number would then indicate a request to add a new product. For our sample application, we'll specify that an Id of 0 means we are working with a newly created product. How then do we populate the route parameters? As we've seen previously in this course, we activate a route on a clickable element at the HTML by binding the routerLink directive on that element to a link parameters array. Or we can activate a routing code using the router's navigate method passing in a link parameters array. We populate the route parameters as part of that link parameters array. The link parameters array contains the root URL segment, any route parameters, and any additional constant URL segments. In the first example, we route to the ProductDetailComponent when the user clicks on the productName link. We specify products as the root URL segment and populate the Id placeholder with the product's Id. In the second example, we route to the ProductEditComponent when the user clicks the Edit button. We specify products as the root URL segment and populate the Id placeholder with the product Id. Then we add the edit URL segment to distinguish this route from the Product Detail route. The third example routes to the Product Edit page when the user clicks the Add Product menu option. This route uses 0 for the product Id parameter indicating a request to add a new product. Since these array elements are all literal values, no variables, we could instead specify all of the URL segments as a simple string using the shortcut syntax. These two router links generate the same URL. This last example uses the router's navigate method passing in a similar link parameters array. It activates the Product Detail route for the defined product Id. Let's jump back to the sample application and activate some routes with route parameters.
Populating Route Parameters: Demo
When the user clicks on the product name here on the Product List page, we want to route to the Product Detail page for that specific product. That's our first task. To accomplish that task, we modify the product-list template. Here is the table data element for the productName. I already have it in an anchor element. We add the routerLink directive to this element. We bind the routerLink directive to a link parameters array. The first element of that array is the root URL segment, which is products. To populate the Id placeholder, we pass the product.id as the second element of the array. When the user clicks on this link, the router builds the appropriate URL based on the contents of this array and navigates to the Product Detail page. Let's check it out in the browser. Click on a product to view its detail. The URL is correctly created with the appropriate Id, but we don't see the product detail. Well, that's because we are not yet reading the parameter value from the URL and getting the appropriate product to display. Before we do that, let's hook up the Product Edit routes. In the product-list template, the Edit button is here. Again we add the routerLink directive and bind it to a link parameters array. The first element of that array is again the root URL segment, which is products. To populate the Id placeholder, we again pass the product.id in as a second element of the array. And we set the edit URL segment as the third element of the array. That distinguishes this route from the Product Detail route. When the user clicks the Edit button, the router will build the appropriate URL based on the contents of this array and navigate to the Product Edit page. Before we give it a try, let's hook up the Add Product menu option as well. The application menu is defined in the app.component template. We add the routerLink directive to the Add Product menu option and bind it to a link parameters array. Here we populate the Id placeholder with 0 to indicate that we want to display the edit page initialized for a new product. Let's check it out in the browser. On the Product List page, click Edit for one of the products. We see the correct URL, and the page header shows us we are on the right page. But we don't see the edit form. That's again because we are not yet reading the parameter value from the URL and getting the appropriate product to display for editing. Now trying selecting the Add Product menu option. We see the URL with the parameter of 0 but still no edit form. Sounds like our next step is to read the parameter values from the URL so we can correctly display these pages. Before we move on, though, I want to call out something that just happened here. To maximize performance and minimize changes to the browser's document object model or DOM, the router reuses a component and its template if only the parameters of the route change. Let's see that again. Click on Product List, and our URL changes to products. This activates the ProductListComponent and displays its template in the primary outlet. Click on Edit, and our URL changes to products/Id/edit. This activates the ProductEditComponent and displays its template or at least as much as it can in the primary outlet. If I then click on Add Product, only the route parameter changes so the ProductEditComponent and its template are reused and not reinitialized. This feature of component reuse is important to understand as we reroute parameters. Let's do that next.
Reading Route Parameters: Snapshot
As we saw in the last demo, passing parameters as part of a route doesn't help much unless the activated component reads and uses those parameters. The router extracts any route parameters from the URL and supplies them to the component through its ActivatedRoute service. The ActivatedRoute service is basically a one-stop shop for route information. It provides access to the set of URL segments, route parameters, query parameters, route data, and more as we'll see later in this course. Any component instantiated by the router can inject its ActivatedRoute service. We inject the ActivatedRoute service into our component just like every other service on the constructor. Once it's injected, there are two basic ways to use the ActivatedRoute service to read route parameters. The ActivatedRoute service has a snapshot that provides the initial state of the route including the initial value of the route parameters. We access the params array of that snapshot to easily retrieve a parameter value from the route in one line of code using the placeholder name. The ActivatedRoute service also provides a params observable. The observable keeps watch on the parameters and receives a notification every time the parameters change. So we can obtain the initial value of the parameters from the static params array that is part of the snapshot or get notified of parameter changes by subscribing to the params observable. For now, let's check out the snapshot approach. For reference, here is the route configuration for the Product Detail route we configured earlier in this module. And here is the code we just added to activate the Product Detail route. To get the parameter from the URL, the ProductDetailComponent uses the snapshot provided by the ActivatedRoute service. We import the ActivatedRoute using an import statement, and we want an instance of that service so we define it as a dependency in our constructor. We use the instance of the ActivatedRoute service to access the snapshot and retrieve the desired parameter from the params array. We access the appropriate params array element using the placeholder name. The name must exactly match including casing. Let's give this a try.
Reading Route Parameters: Snapshot Demo
When we route from the Product List page to the Product Detail page, we pass the Id of the desired product on the route as a route parameter. The ProductDetailComponent needs to read the Id from the route parameter and display the data for the selected product. To read the route parameter using the ActivatedRoute snapshot, we first import the ActivatedRoute service from the @angular/router library. We then add the ActivatedRoute as a dependency on the constructor. When this component is created, the Angular injector injects an instance of the ActivatedRoute service into this component. Before we write the code to use this snapshot, we need to think a moment about where to put that code. In the slides, we added the code to read the parameter to the constructor. But in our sample code, once we get that Id, we'll call this local getProduct method to retrieve the product. We don't normally want to execute asynchronous operations such as getting data from a server in the constructor. Let's instead use the OnInit lifecycle hook. The OnInit lifecycle hook is executed when the component is initialized. It's a great place to perform operations such as getting the data needed for the component. to use the OnInit lifecycle hook, we import the OnInit from @angular/core, so we add it to the import statement here. We then add the implements keyword to our class declaration to implement the OnInit interface. Lastly, we write the ngOnInit method. I'll paste the code, and we can talk through it. In the ngOnInit method, we retrieve the Id from the snapshot. We use the ActivatedRoute instance, access the snapshot, and retrieve the Id parameter from the params array. Since the URL is a string, we add a plus sign at the front to cast to a number. This gives us a numeric product Id. If we hover over the snapshot, we see that it's type is ActivatedRouteSnapshot. We'll use this type directly a little later in this course. We then call the local getProduct method and pass in that product Id. The local getProduct method calls the getProduct method in our product data service to get the product using HTTP. Since this course is on routing, not data access, I've provided all of the data access code as part of the starter files. To see a walkthrough of the data access code and Angular's HTTP features, check out the Angular 2: Getting Started or Angular 2: Reactive Forms course. Now let's check the result in the browser. On the Product List page, click on a product. And there is our Product Detail page. Yay! The URL shows our root URL segment and our route parameter. The ProductDetailComponent accessed the ActivatedRoute service snapshot to read that parameter and used it to retrieve the requested product for display. What about these two buttons here? Use what you've learned previously in this course to implement the Back and Edit buttons on this page. The Back button should activate the product's route and navigate back to the Product List page. The Edit button should activate the Product Edit route passing the Id of the product to edit. Pause the video now if you want to give it a try. Are you ready to see my solution? In the product-detail template, I added the routerLink directive to the Back button here and defined the link parameters array to activate the products route. And I added routerLink to the Edit button specifying the link parameters array to activate the product edit route. Trying it out in the browser, the Back button returns to the Product List page. Viewing the Product Detail again and clicking the Edit button, we see only the partial display of the Product Edit page. To see the Product Edit page, we need to read the product Id from the route in the ProductEditComponent using the ActivatedRoute service just as we did in the ProductDetailComponent. Want to give it a try? If so, pause the video here. Ready for my solution? I imported OnInit and ActivatedRoute. I implemented the OnInit interface. I defined the ActivatedRoute service as a dependency on the constructor. And I added the exact same ngOnInit method code. Is that similar to what you did? Let's check it out in the browser. Click Home to start fresh. Select Add Product. And it works! Our edit page displays for entry of a new product. Yay! Now let's try editing an existing product. On the Product List page, click on an Edit button, and there is our Product Edit page. Cool! It displays the current values of our product and allows the user to update the values. But what if we click on Add Product now? The route parameters change in the URL, but our edit form does not change for entry of a new product. If we look again at the code, we see why. We retrieve the Id from the ActivatedRoute service snapshot in the ngOnInit method when the component is initialized. But as we discussed earlier in this module, if only the parameters of the URL change, the component is not initialized again. So the ngOnInit method is not executed again, and we don't get a new initialized product. How do we handle a change in parameters? Instead of reading the route parameters from a snapshot, we watch for parameter changes using an observable.
Reading Route Parameters: Observable
For reference, here again is the route configuration for the Product Edit route. And here is the code we added to activate the Product Edit route. To display the appropriate product in the Product Edit page, the ProductEditComponent needs the parameter from the URL. But because the route parameters can change without reinitializing the component, we can't just read the initial state of the route parameter from the ActivatedRoute snapshot. Instead, we watch for parameter change notifications and re-get the product Id every time the parameter changes. We watch for events using observables. You can think of an observable as an object that provides a stream of events or notifications that occur over time. Luckily for us, the ActivatedRoute exposes a route params property as an observable. Here we subscribe to that observable to start receiving its notifications. We pass to the subscribe method a function defining the action to take each time a notification is received. The first time this route is activated and every time a route parameter changes without activating a different component, the observable emits a notification and passes in the current set of parameters. We access the appropriate params array element using the exact placeholder name. Let's give this a try. We set up the observable when the component is initialized in the ngOnInit method. In ngOnInit, we'll delete the lines that read the Id from the route snapshot. Instead, we'll access the ActivatedRoute service params observable. I'll paste the code, and we can talk through it. Hovering over params, we see that it does indeed return an observable. We subscribe to this observable so we are notified when the parameters change. The notification provides the current set of parameters, which we'll hold in a params variable. We use the arrow syntax to define the code that should execute each time we receive a notification. Here we retrieve the Id parameter from the passed-in params array. Since the URL's a string, we add a plus sign at the front to cast to a number. Then we call the local getProduct method passing in that Id. This local getProduct method calls the product data service getProduct method to get the product data for display in the edit page. Since this call is within our subscribe, this call will be re-executed each time the parameters change. Before we give this a try, let's take a peek at the product data service getProduct method. Right-click on the method and select Go to Definition. That takes us to the product data service. Here we see special code to handle the Id of 0. If we pass in an Id of 0 to add a new product, this code returns an initializeProduct. Otherwise, it uses HTTP and gets the requested product from a back-end server. Now let's check out the result in the browser. Select Product List. Click the Edit button. And we see the edit page populated with the existing product data. The user can then edit this information as desired. Click Add Product, and the edit form is initialized for entry of a new product. It works! To finish up the features here, hook up the Cancel button to route back to the Product List page and add code for the Save and Delete buttons to route back to the Product List page after the Save or Delete is complete. Pause the video now if you want to give it a try. Ready for my solution? The code to activate the route from the Cancel button is in the product-edit template. It now routes to the products route and displays the Product List page. Notice that the Save and Delete buttons call methods to save or delete the product. Those methods handle the operation, saving or deleting. Each method then calls onSaveComplete to complete the operation. I added the navigation there. This code uses the router service, which must be imported here, added as a dependency here, and then used here. Let's ensure the buttons work. From the Product List, select Edit, Cancel, and we return to the Product List page. Select Edit again. This time let's update the product name and save. The change is saved, and we are returned to the Product List page. We can see the change here. Select Edit one more time, click Delete, confirm the delete, and we are returned to the Product List page. The deleted product no longer appears. It all works! Note that we are using an in-memory data service. So if you stop this server and restart with npm start or refresh the page, the product data is returned to its original values. Let's review when to read route parameters using a snapshot versus when to use an observable. Use the snapshot if you only need to read the initial value of the parameters and those parameters won't change once the component is activated. The code can simply read a parameter from the activated route service snapshot params array. If you expect the parameters to change without navigating to another component, you can watch for parameter changes using an observable. Subscribe to the ActivatedRoute service params observable, and each time the parameters change, read the parameters from the provided params array. Note that when subscribing to an observable in a component, you almost always add code to unsubscribe when the component is destroyed. There are a few exceptional observables where this is not necessary including the activated route observable used here. The router destroys the routed component when it is no longer needed, and the injected ActivatedRoute service and its associated observables are destroyed with it. So no need to unsubscribe. Angular 4 introduced another ActivatedRoute interface called paramMap. When using the snapshot, we call the paramMap.get method to get a parameter value. Since this is a method, we use parentheses and place in the placeholder name. Or if the parameter has multiple values, we use getAll to get the values as an array. When using the observable approach, we subscribe to the paramMap observable, then call the get or getAll method passing in the placeholder name. Feel free to use this syntax instead if you are using Angular version 4 or higher. But what if we want optional route parameters? How do we handle those?
Defining Optional Route Parameters
Say you have a search page for the user to select search criteria such as a product name, product code, or an availability date. You want to pass these criteria to a list page to display a list of products that match the defined search criteria. To accomplish this, we could use required route parameters as we've done previously in this module. We'd add a placeholder in the path for each criterion and populate these placeholders when activating the route. The resulting URL would look something like this. These odd-looking values are an encoding of the spaces and commas in the dates. The ProductListComponent would then read the parameters and display the products that match the selections. There are several issues with using required parameters in this scenario. Complex URLs such as this complicate the pattern matching required to translate the URL segments into a path in the route configuration. And every time we add more search criteria, we need to change the route configuration and everywhere that route is used. Another issue is lack of any naming. And looking at the parameters in the URL, it's difficult to tell what each of these parameters means unless we look at the route configuration. We could add constant route segments in between each of these identifying what each parameter is, but that makes the path even longer, more complex, and more difficult to get right. And sometimes some of these parameters, such as the date range, may not be set by the user based on their particular search. So what do we pass when a particular parameter is not specified? Instead of using required route parameters, a better option for this scenario is to use optional route parameters. Optional route parameters make it easier to pass optional or more complex information as part of the route. This optional information is not part of the configuration. This means that it is not involved in matching route paths for navigation. And if we add more optional parameters over time, we don't affect the application routing. Optional parameters are defined in the link parameters array as a set of key and value pairs. Notice the syntax here. We define the optional parameters within curly braces in one array element. We specify a key and a value for each optional parameter. We can easily add more optional parameters without affecting the route configuration. We can easily tell which parameter is which because each has a unique key. And if a parameter is not set, we can leave off its key and value. Note that any optional parameters must always be after any required route parameters in the link parameters array. The resulting URL looks like this. Notice the syntax. The optional parameters are listed with their keys and values separated by semicolons. Again, the spaces and commas are encoded. Optional parameters are the ideal choice when conveying arbitrary or complex information during navigation. Optional parameters are route specific and scoped to the particular URL segment so there is no risk of key name collisions with other URL segments. And like required parameters, they are not retained when navigating to other components. How do we read these parameters in our components? We read optional parameters the same way we read normal parameters--using the ActivatedRoute service snapshot or an observable. In our example search page, we route to the ProductListComponent and pass along all of the user defined search criteria as optional parameters. Just like reading required parameters, we use the snapshot provided by the ActivatedRoute service. We import the ActivatedRoute using an import statement and define it as a dependency in our constructor. Where we need the values, we retrieve the parameters from the snapshot by their key name. I have not implemented a search page in the sample application. Feel free to add one and try out optional parameters at your leisure. Now we've covered required parameters and optional parameters, but there is one more kind of route parameter--query parameters.
Defining Query Parameters
Here again is our Product List page. If we enter a filter string, the products are filtered by the specified string. And if we click Show Image, the product images appear. But if we then click to view a product's detail, then use the Back button to return to the Product List page, our selections are gone. It would be a much nicer user experience if we retained the user settings when navigating to the ProductDetailComponent and return those settings when the user navigates back to the ProductListComponent. To define parameters that work across multiple routes to the Product Detail page and back again, for example, we use query parameters. Just like optional parameters, we use query parameters to pass optimal or complex information. Unlike optional parameters, they can be retained across routing paths. And notice the syntax here. They look like classic query parameters with a question mark at the beginning and an ampersand separator. Like optional parameters, query parameters are not part of the route configuration and are not involved with matching route paths. We populate required and optional parameters by adding them to the link parameters array. Not so with query parameters. We pass them separately. When populating query parameters in the HTML, we use the queryParams directive and bind it to a set of key and value pairs, one for each query parameter. When routing in code, we add a second argument to the navigate method outside of the link parameters array. That argument is a set of options defined with key and value pairs. To populate the query parameters, we define a key of queryParams and set its value to an object defining the query parameters. Unlike optional parameters, query parameters are not scoped to the route so care must be taken to ensure there are no key name collisions. Let's try it out in our sample application. Our task is to retain the user settings on the Product List page so that when the user navigates to the Product Detail page and returns, the settings are reapplied. So here in the product-list template, we add the queryParams directive and define a set of key and value pairs. We'll call the first key filterBy and set its value to the current listFilter property. The listFilter property is bound to the filterBy input element and will contain the user's current filter selection. We'll call the second key showImage and set its value to the current showImage property. The showImage property tracks whether or not the user selected to show the product image. We can define any number of query parameters here, but two will do for our purposes. Let's check out the result in the browser. Enter a filter and click Show Image. Then click on a product name to view the product details. The URL now includes what looks like a classic set of query parameters with a question mark before the first query parameter and an ampersand separating the parameters. Click Back, and the parameters are lost. Didn't we just say that query parameters can be retained across paths? Well, yes, yes they can, but not by default. By default, the router resets the query parameters during navigation. However, we can elect to preserve the current query parameters using the preserveQueryParams directive in the HTML or the preserveQueryParams key when navigating in code. In Angular version 4, preserveQueryParams was deprecated and replaced with queryParamsHandling, which provides more flexibility. We can set queryParamsHandling to preserve to preserve the current query parameters or merge to merge the query parameters with any existing query parameters. In the upcoming demo, I'll use preserveQueryParams. If you are using Angular version 4 or higher, you can try out the queryParamsHandling instead. Where do we specify this preserveQueryParams? Well in our example, we want the parameters retained when we navigate back from the ProductDetailComponent so that's where we'll add it. Here is the Back button in the product.detail template. We specify the preserveQueryParams directive and set it to true. Now when the user clicks the Back button, any query parameters passed to the Product Detail page are retained and passed back to the Product List page. Let's see how that changes things. Again, enter a filter and click Show Image. Click on a product name to view the product details. The URL has our query parameters. Click Back, and the parameters are still here. Yay! But the page does not yet read these parameters. Let's do that next.
Reading Query Parameters
Reading query parameters is much like reading required or optional parameters. We use the ActivatedRoute service and the snapshot or observable approach. For reference, here is the definition of our query parameters. To read the query parameters, we begin by importing the ActivatedRoute service using an import statement. And we want an instance of that service so we define it as a dependency on our constructor. Here we use the snapshot approach and access the queryParams array instead of the params array as we do with the required and optional parameters. That's really the only difference when reading query parameters. Let's give it a try. We want the ProductListComponent to read the query parameters passed back on the route from the ProductDetailComponent and set the filter and showImage properties based on those parameters. We first import the ActivatedRoute service from the @angular/router library. We then define the ActivatedRoute as a dependency on the constructor. The OnInit interface is already implemented. We add code here to read the query parameters. We'll set the listFilter property to the query parameter value using the ActivatedRoute service snapshot. We'll pull the values from the queryParams array using the query parameter key, which is filterBy in our example. We add an orEmpty string to handle the case when the filterBy is undefined such as when this page is first displayed. We set the showImage property to true if the showImage query parameter is true. But recall that our parameters are always strings, so we need to check for a string value of true. That's it. Let's check out the result in the browser. Once again, enter a filter and click Show Image. Click on a product name to view the product details. The URL has our query parameters. Click Back, and the Product List displays with our prior settings. Success! Let's finish up this module with some checklists you can use as you define required, optional, and query parameters.
Checklists and Summary
Required parameters pass needed data on a route. Use a required parameter whenever the route component must have the parameter value. For example, a detail route must have an Id or it won't know which item to display. Configure required parameters using a placeholder denoted with a colon and a logical placeholder name. Populate required parameters by specifying them as elements in the link parameters array either assigned to the routerLink directive or passed into the router's navigate method. Read the parameters from the route using the ActivatedRoute service snapshot or its params observable. The array element key is the placeholder name. Use an optional parameter when the value is truly optional or if it is complex or requires multiple values. For example, an advanced search component passes any number of search criteria as optional parameters to a list component so it can filter the data in the list. Optional parameters are not configured as part of the route configuration. Populate optional parameters using a set of key and value pairs in one element of the link parameters array. They must be specified after all required parameters in the array. Just like required parameters, read optional parameters from the route using the ActivatedRoute service snapshot or its params observable. The array element key is the key from the key and value pair. Be sure to match the casing of the keys. Use a query parameter to pass optional or complex information to a route when you want that information optionally retained across routes. For example, a list component provides user selections such as a filterBy string. The list component passes those user selections to the detail component so it can pass them back when returning to the list component. Query parameters are not configured as part of the route configuration. Populate query parameters using the queryParams directive and specifying a set of key and value pairs or by passing an object to the router's navigate method and setting the query params to the set of key and value pairs. Read the parameters from the route using the ActivatedRoute service snapshot queryParams array or its queryParams observable. The array element key is the key from the key and value pair. Be sure to match the casing of the keys. This module was all about route parameters. We began with required parameters. We looked at how to configure, populate, and read required parameters using the ActivatedRoute service snapshot or its observables. Next we looked at how to populate and read optional parameters and query parameters. Now that we've examined how to pass data to our routes, we have our Product Detail and Product Edit routes in place. And we improved the Product List route to retain its filter and image display settings as query parameters when navigating back from the Product Detail route. Next up, let's see how to prefetch our application data using resolve.
Prefetching Data Using Route Resolvers
Introduction
For a more user friendly experience, we can retrieve the data required for display in a component's template before routing to that component. Welcome back to Angular Routing from Pluralsight. My name is Deborah Kurata, and in this module, we prefetch data for our component's using route resolvers. When a user navigates to a component with a template that requires data, the page may first appear mostly blank or with only the static text making a less-than-ideal user experience. This happens due to a delay in downloading the data required for the page especially if the user has a slower connection or there is lots of data. We can prevent this partial page display by downloading the data for a component before routing to that component with route resolvers. There are several benefits to prefetching the data for our components. Prefetching prevents display of a blank or partial page while waiting for data to be retrieved. Prefetching allows us to reuse more code. In most applications, we are already reusing the data retrieval code because we encapsulate it in a data access service. By prefetching the data for a set of routes, we can also reuse the code that calls the data access service. We'll see that later in this module. And we can handle data retrieval errors before routing to the component. For example, there is no point in navigating to a detail page if the Id of the item to display is not found. We can instead navigate to an error page or stay on the list page for selection of a different item. In this module, we start by listing the numerous ways we can provide data with a route. Then we outline the steps for using a route resolver to prefetch data. We discover how to create a route resolver and examine the syntax for adding the resolver to a route configuration. We walk through how to obtain the resolver data in the routed component using a snapshot and using an observable. We aren't adding any new routes in this module. Rather, we'll modify the Product Detail and Product Edit routes to prefetch their product data. We could also prefetch the data for the Product List route. But we'll leave it as is for comparison and just focus on these two. Let's get started.
Providing Data with a Route
There are often times we need to provide data with a route. There are multiple ways we can achieve this task. The best way in any particular scenario depends on the amount of data, the scope of data sharing, and how that data is used. Route parameters are great when a route requires a small amount of data in order to display its content such as an Id to display a detail or edit page. We saw how to work with route parameters in the last module. Optional route parameters are useful when we need to pass optional, complex, or multifaceted data from one route to another. In the last module, we saw how to use optional parameters to pass search criteria to a list page that filters by that criteria. Query parameters allow us to obtain small amounts of data between routes. A search or list page can retain its selections when navigating to its detail page and back again, as we saw in the last module. A route has a data property we can use to pass a fixed object to a route. We'll talk more about that in a moment. The router can call a resolver service that can prefetch dynamic data for a component. So instead of routing to a component and displaying a partial page as it gets its data, the resolver can get the data first and then route to the component. The component template then has the data it needs to display fully. We'll see how to build and use resolvers in this module. Another option for sharing data is to use an Angular service. A service is independent from any route so we can use it to share data with any components at any level of the route hierarchy. We'll see how to use a service to share data later in this course. For now, let's take a quick look at the route's data property. We'll spend the rest of this module on route resolvers. A route definition has a data property. We use it to provide any arbitrary data to a route. We pass it an object specifying a set of key and value pairs where the key is a logical name for the data and the value is the data itself. The data defined in the data property cannot change throughout the lifetime of the application. So we use it for static data such as the pageTitle here. To read the data property, we use the same ActivatedRoute service we worked with in the last module. We then use it's snapshot to access the data array using the key to retrieve a specific data element. Note that we would not use an observable here because by definition the data property is static and won't change. So there is no need to watch for changes. We'll use this data property later in this course. For now let's focus on building a route resolver to prefetch dynamic data.
Using a Route Resolver
Here is a common flow for a detail page without a router resolver. On a list page, the user clicks on an item and activates the detail component's route passing the Id of the item to retrieve. The component is activated, and its template displays but does not yet have the data it needs. So it is only partially populated. A component's ngOnInit method executes and requests the item from the back-end web server via HTTP. While the app is waiting for the data, the user sees a partially completed page. When that data arrives from the server, the remainder of the page is populated. With the route resolver service, the flow is a bit different, but it starts the same. On a list page, the user clicks on an item and activates the detail component's route passing the Id of the item to retrieve. The router executes the route resolver, which requests the item from the back-end web server via HTTP. When the data arrives from the server, then a component is activated, and the component's template displays fully populated using the data obtained by the resolver. The key difference between the two approaches is who gets the data and when. Without a route resolver, the component class gets the data after it's initialized. With a route resolver, the resolver service gets the data so the template is not displayed until it has the data it needs. This provides a much cleaner visual appearance and, hence, a much nicer user experience. Prefetching data for a route using a route resolver requires three steps. First, we build and register a route resolver. A route resolver is a custom Angular service that retrieves the data needed for our component. Next, we add the resolve property to the route configuration and assign it to the route resolver. The router executes the resolver service and retrieves the data before activating the associated component. Since the route now has the data that our component needs, we modify the associated component to read its data from the ActivatedRoute service similar to how we retrieve route parameters. We read that data using a snapshot or an observable. We use a snapshot to read the data from the ActivatedRoute service if we don't expect the resolver to get different data while staying on the current page. For example, when displaying the Product Detail route, the resolver will get the data for the specific product to display and won't get data for another product until the user first navigates back to the list page. We use an observable to read the data from the ActivatedRoute service if it possible for the resolver to get different data while staying on the current page. For example, when displaying the Product Edit page for a specific product, the user could select the Add Product option. This will cause the route resolver to get an initialized product, thereby changing the resolver data. In this case, we use an observable to read the data from the Activated Route service. Now that we know the basic process, let's jump into the first step--creating a route resolver.
Building a Route Resolver Service
A route resolver is often created as an Angular service similar to how we create any other Angular service. We export a class, decorate it with the injectable decorator, and import what we need to make this service behave as a route resolver. We implement the Resolve interface. Notice that this interface is generic. That is to say that it uses a generic parameter to define the type of data it retrieves. And since we want the resolver to retrieve products, we import our product interface. When we implement the Resolve interface, we much define a resolve method. That method takes in an ActivatedRouteSnapshot and a RouterStateSnapshot. The ActivatedRouteSnapshot contains information about the currently activated route. We used this snapshot earlier to retrieve route and query parameters. The RouterStateSnapshot represents the state of the application's router at a moment in time. The RouterStateSnapshot is a tree of ActivatedRoute snapshots. The Resolve method can return an observable, a promise, or just data. In this example, we return an Observable of Product using the IProduct interface. Code in the Resolve method gets the desired data and returns it to the route. When the route is activated, the router calls this Resolve method and waits for the observable to complete before activating the associated component. Let's give this a try. We begin by creating a new file for our route resolver service. Since it will retrieve product data, we'll add it to the products folder. We'll call it product-resolver.service.ts. I'll start the code by pasting the import statements that we know we're going to need. We export a class we'll call ProductResolver. Since this is a service, we'll decorate it with the injectable decorator. We want this service to be a route resolver so we implement Resolve. We set the Resolve generic parameter to our Product interface requesting that this resolver return a single product. We see a syntax error here because we have not yet implemented the method that the Resolve interface requires. I'll paste it in. This method takes in the ActivatedRouteSnapshot and RouterStateSnapshot and returns an Observable of IProduct to return a single product. The only thing left to do here is to actually retrieve and return a product. We are adding this route resolver to prefetch data for the Product Detail and Product Edit routes. So the first thing we need is to retrieve the route parameter from the route. We use the ActivatedRouteSnapshot just like we did in the last module. We access its params array and request the Id parameter. We add a plus sign at the beginning to convert the return string to a numeric Id. Then we use the existing product data service, which uses HTTP to get the product data. We first need to import this service, then we ask the injector to inject the instance of that service into this service using the constructor. Now we can modify the return statement to call our productService passing in the Id of the product to get. Since this resolver is a service, we also need to register it in an Angular module. We'll do that in the next clip. This resolver service can be used any time we want to prefetch a single product where the Id for that product is provided as an Id parameter on the route. Since both our ProductDetailComponent route and our ProductEditComponent route specify an Id parameter, they can both use this resolver. One of the stated benefits of using a resolver is to handle data retrieval errors before routing to the component. So let's add some exception handling here. Often exception handling is not shown in demo applications because that makes the code more complex. Feel free to skip typing in all of this error handling and use the route resolver service as it is now. I'll paste in the changes, and we can talk through them. When thinking about exception handling for this code, the first thing that could go wrong is an invalid product Id. We check whether the Id read from the route parameter is actually a number. If not, we log the issue and navigate back to the Product List page. We then return an observable that is null. Notice that our logging here is just to the console. In a real application, we'd have a more formal logging mechanism. Notice that we don't use the plus cast operator here. That way we can log the original parameter value. We then use the plus cast operator here to ensure we are passing in a numeric Id. Then instead of simply returning the result of the getProduct method, we call the observable map operator so we can access the return data before passing it on. This allows us to check whether we did indeed retrieve a product. If so, we return the product. If not, we log, navigate to the Product List page, and return null. The map operator returns the value as an observable, so we do not need Observable.of here. Lastly, we add a catch operator to catch any other retrieval errors. We again log, navigate to the Product List page, and return Observable.of(null). This code looks much more complex but handles our retrieval errors so our routed components don't have to-- Now let's see how we associate this resolver with a route.
Adding a Resolver to a Route Configuration
To associate a route resolver with a route, we add it to the route configuration that we define within an Angular module. This ensures that the data for the route is retrieved using the resolver before the routed component is activated. We use the route configuration's resolve property to specify the list of resolvers. Here we define a set of key and value pairs where the key is a logical name for the data and the value is a reference to the resolver that returns that data. Since this is a reference and not a string, we need an import statement for this resolver in the module. In this example, we name the resolver data product and define a reference to the ProductResolver service. We add the resolve to both the Product Detail route and to the Product Edit route. We can add any number of resolvers here. Say, for example, that our ProductEditComponent also wants to prefetch data for a category selection box. We could build a route resolver for the categories and add a reference to it here. Just ensure that each key for a resolve is unique. But they don't have to be unique across resolves as shown with the product key here and here. Let's update our route configuration to reference our new resolver. The route configuration for our product routes is in the product.module. We'll need a reference to our ProductResolver service, so let's start by importing it. And since it is a service, we need to register it. We do that by adding it to the providers array as part of this module's NgModule decorator. Next, we add the resolve to the route configuration. For the Product Detail route, we add resolve, then define a key and value pair for each resolver. We specify product as the key because this resolver provides a single product and reference ProductResolver, our resolver service. We can reformat this for readability. When the Product Detail route is activated, this route configuration tells the router to use the resolver service to prefetch the product data. Let's make this same change to the Product Edit route configuration. Before we can try this out, we need to modify the components to read this data from the route.
Reading Resolver Data - Snapshot
Once we have the resolver service in place and add it to our route configuration, all we have left to do is read the data from the route. If we don't expect the resolver to re-fetch different data unless leaving the page, we can use the ActivatedRoute snapshot to read the resolver data. Simply access the data property of the snapshot referencing the desired element using the name of the data we defined in the route configuration. We assign the result to our local property. So this code reads the product data from the route and assigns it to the product property. If the template binds to this property, all of the data immediately appears when the template is displayed. No more partially presented pages. Yay! Accessing the data array from the ActivatedRoute service gives us a reference to the data instance. All code that retrieves and works with the same element from the data array shares the same instance. So any change made to this property in any one component is seen by all components that reference the same property. This sharing of the data instance is useful when we begin to work with child routes as we'll see later in this course. For now, let's try reading the resolver data from the route. Let's start with the product-detail.component. Previously, we imported the ActivatedRoute and defined it as a dependency here in the constructor. We no longer need to read the params array to get the product Id nor do we need to get the product by passing that Id to our product data service. We don't even need our product data service instance. Instead, we set the local product property to the route.snapshot data element containing the product data. We named our product data product. Wow, that simplifies our code here. Let's check it out in the browser. Select Product List. And notice the partial page display. That's because we have not defined a resolve for the list of products. Click on a product. Notice the pause while the route gets the data, then the entire Product Detail page appears. No partial page. It works! Before modifying the Product Edit to also use the resolver data, let's review what it looks like now. An empty header displays. Then a few moments later, the edit page appears. This would also look better using a resolver. Now we can make similar changes to the product-edit.component to read the product data from the route using the snapshot. Note that the product-edit.component does still need the product data service instance in order to save or delete the product. Pause the video now if you want to give it a try. Are you ready for my solution? We no longer need to get the Id from the route parameter so we can delete all of the code in the ngOnInit. And we don't need the getProduct method. The code to get the product is now instead in the ProductResolver service and it does not need to be duplicated here. Now in the ngOnInit, we simply call the onProductRetrieved method passing in the product from the route snapshot. The onProductRetrieved method then assigns the product to the local product variable and sets the appropriate header text. Let's check it out in the browser. Select Product List and click to Edit a product. Notice the slight pause while the route gets the data, then the entire Product Edit page appears. It works! But what if the user clicks on the Add Product menu option when this page is displayed. The route changes. We can see that here. But the page is not correctly displayed for entry of a new product. As we saw earlier in this course, the ngOnInit method is not re-executed when the route is changed if we stay on the same page. So our code does not know that it needs to re-read the product data from the route. How do we fix this? If you said observables, you are correct.
Reading Resolver Data - Observable
Instead of reading the data from the ActivatedRoute snapshot, we subscribe to the ActivatedRoute data observable. If the route changes without leaving the page, like we saw in the last demo, the route data is re-read from the route. Let's give this a try. We want to change the product-edit.component code from using this snapshot approach to using an observable. So let's start by deleting this snapshot code. We'll enter this.route.data to use the router's data property, which is an observable. And we'll call this subscribe method to subscribe to changes to the prefetched resolver data. We provide a function to the subscribe method. The resolved data is passed into this function so when we receive a notification, we call onProductRetrieved and pass along the product element of that resolved data. One thing to make clear here--we are only notified when the resolver re-fetches data. We won't receive a notification if our code changes that data. For example, if we are on the Product Edit page, and the user selects Add Product, we'll get a notification when the resolver fetches the initialized product. If we modify one of the product properties such as editing the product name, we won't receive a notification. Are we ready to check it out in the browser? Click to edit a product and Bang! The Product Edit page appears fully populated. Click Add Product and Bang! The Product Edit page appears initialized for entry of a new product. No partially presented page. Using a resolver ensures the user does not see partial pages appear when waiting for data retrieval. This makes for a much nicer user experience. Before moving on, let's sidetrack for a moment. Now that we have our resolver working, let's look at an alternative way to create a resolver. Instead of creating a service and registering the service using the providers array, we could instead define the resolver directly here as a function. We will be undoing the code changes I'm about to make, so feel free to skip coding along with this part of the demo if desired. In the providers array, we define a provider function by specifying an object. I'll paste in the code, and we can talk through it. We set the provide property to the name of this resolver. I call it productProvider here. Then we set the useValue property to a function. We can't call our data access service here, but we can hard code in some data. If we did want to call our data access service, we could use the useFactory property instead, which allows us to pass in injected services. In this example, our function needs no parameters and returns the desired data. Note that we could access the ActivatedRouteSnapshot and RouterStateSnapshop parameters here if we needed them. Next, we change the resolve in the route configuration to use this provider. Since this is a string and not a reference, ensure it is enclosed in quotes. Angular detects whether the route resolver is a function or a class. If it is a function, it uses the function defined in the useValue or useFactory property. If it is a class, it calls the resolve method in the class. Personally, I like building the resolver as a service and keeping this logic out of our Angular module. But this technique does work, and you will see it. I'll undo this change, and we'll instead use our resolver service. Now let's finish off this module with some checklists you can use as you prefetch data using route resolvers.
Checklists and Summary
To build a route resolver, create an Angular service. Then implement the Resolve generic interface specifying the type of data to retrieve as the generic parameter. And add a resolve method that retrieves the desired data. Since this is a service, be sure to register it in an Angular module. Configure the route resolver in an Angular module using the resolve property. Give each type of prefetch data a logical name and specify a reference to its associated route resolver service. In the component, read the data from the route. Use the ActivatedRoute snapshot if the route never changes while on the page. Use the data observable if the route could change while on the page such as our Product Edit example, or the user can navigate to the Add operation from the edit page. In this module, we began with a look at several techniques for providing data to a route. We examined how to use a route resolver. We then built a route resolver as an Angular service. We added the resolver to two of our route configurations and saw how to read the resolver data using the ActivatedRoute snapshot and using the ActivatedRoute data observable to watch for changes. Our Product Detail and Product Edit routes now prefetch the product data before routing to these components. You can use the techniques shown in this module to prefetch data for other routes such as the Product List route. Next up, we look at child routes.
Child Routes
Introduction
We can define routes that are displayed within other routes. Welcome back to Angular Routing from Pluralsight. My name is Deborah Kurata, and this module is all about child routes. Using child routes, we define a route hierarchy to better organize, encapsulate, and navigate through our application. Plus it makes it easier to lazy load routes improving the startup performance of the application. Here we have two primary routes--welcome and products. The products route is a component list route meaning it does not activate a component. Rather it simply acts as a parent route allowing us to group the product routes as child routes. This products route has three children. Further down the hierarchy, the Product Edit route has two children--Edit Info and Edit Tags. Our focus in this module is on the basics of child routes. So we'll add the two Product Edit child routes. We'll look at grouping and component list routes in the next module. In this module, we begin with an overview of child routes, what they are, how they work, and when to use them. We then look at how to configure child routes, place the child view, and activate child routes. We examine how to obtain data for child routes and how to validate data across child routes when splitting an edit operation across child views. In our sample application, we'll change the ProductEditComponent to display a tabbed container. Then we'll add a child route for each tab, one for editing basic product information and the other for editing product search tags. Let's get started.
Using Child Routes
Before we look at how to use child routes, it's important to remember how our primary routes work. This snippet lays out a navigation menu and other elements that make up the outermost container of an application. Recall from earlier in this course that we used the router-outlet directive to define where to place the routed component's template for display. When the user views the application in the browser and routes to a component, that component's template is displayed within the outlet. Of course the router-outlet directive doesn't appear in the UI. It is shown here for instructional purposes only. If the user clicks one of the Edit buttons, the product-edit.component template is displayed in place of the Product List within this same outlet. But what if we have too many visual elements to display nicely on one page? One common technique for displaying lots of data elements on a page is to organize related information on tabs. How then do we display the appropriate template for each tab? Define the Product Edit page as a second container with its own outlet. The header tabs and other shared UI elements are defined in the containing component, and the specific data elements for each tab are defined in their own components. When the user clicks a tab, we route to that tab's component using child routing and display the result in the inner container's outlet. This is a key purpose of child routes, to define routes that are displayed within other routes, or more technically accurate, to display routed component templates within other routed component templates. Tabs are a great way to show off child routes, but they are not the only use of this technique. We can use child routes to build a master/detail style page. We can take advantage of child routes any time we want to embed routed component templates within an existing routed component template. If we want to truly encapsulate all of the logic for each of our feature modules such as a product module or customer module, we could use child routes to display all of the feature's routes. We'll see how later in this course. And child routes are required for lazy loading. We'll talk more about lazy loading later in this course as well. Now let's go configure some child routes.
Configuring Child Routes
Currently, our sample application only defines primary routes. The routed component templates appear in the App component's template primary outlet. We want to modify our ProductEditComponent to display a set of tabs and use child routing to route to each tab's component. The child route component templates appear in the parent's outlet. Let's see how to configure these child routes. As we've seen, routes are configured as part of router module in an Angular module. Child routes are defined within a children array associated with a parent route. Here we add child routes to the Product Edit route. Within the child array, we define each route. First, we set the path. Child routes extend the path of the parent route so we don't repeat the parent's path. This first path is an empty default path. When navigating to the Product Edit parent route without a specified child route, this path redirects to the info route and displays it within the parent's outlet. We covered redirect in detail earlier in this course. We define the info route by specifying info as the path. The full path to this component is then products/:id/edit/info. We then define the component associated with this path. When this route is activated, the ProductEditInfoComponent is activated, and it's template is displayed within its parent's router outlet. The tags path is defined similarly. Let's give it a try. In our sample application, we want to change our ProductEditComponent template to a container with tabs and use child routes to route to the content for each tab. The product routes are in the product.module. We add the routes for each tab as children of the edit route. So we define the children array as part of the Product Edit route configuration. I'll paste the code and the import statements. And we can talk through it. For each child route, we first define the path. This first route is for an empty path. This defines the default path to display if no child path has been specified. Here is the info path. We specify the component to activate when this route is activated. We repeat a similar entry for the tags path. Now we've just referenced two new components, so we need to add them to the declarations array. I included the code for both of these components and their associated templates as part of the starter files. That allows us to keep focused on the routing and not on building components and templates. We can't yet try out these changes because we have nowhere to display these child views. Let's do that next.
Placing the Child View
To display a template for a child route within the parent's template, the parent's template must contain a router-outlet directive. This router-outlet directive identifies the location where the child's template is displayed within the parent's template. The outlet for display of the child routes looks basically the same as the syntax for the primary outlet. Notice that we have no name or other specifier on the router-outlet directive. The router knows whether to display a template in the primary or a child outlet based on the route hierarchy defined in the route configuration. Let's give this a try. Our product-edit.component's template currently displays a form for editing a product. We want to display a set of tabs instead. Let's delete the form and replace it with two tabs. We could of course add any number of tabs here, but two are enough for our purposes. Then we'll add the router-outlet directive where we want the child route component templates to appear. Even though we aren't yet activating any of our child routes, our empty path should kick in. So let's see how it looks in the browser. Navigate to the Product List, click on the Edit button for one of our products, and here it is. The router displays the primary routes such as our Product Edit route in the App component's outlet and displays the child routes such as our Product Edit Info route in the ProductEditComponent's outlet. We have child routes working. Yay! But notice the data here. Regardless of the product we select for edit, we will always get the same test data. That's because we are not yet retrieving the data in the ProductEditInfoComponent. Currently, the component has hard-coded data. We'll fix that shortly. Notice also that the tabs don't work. We'll fix that as well. First, let's add a set of buttons for the user to select to save, cancel, or delete. I'll paste the code for the buttons, and we can talk through it. We want the buttons to appear under the routed child component. The Save button calls the saveProduct method to save the product. The Cancel button uses a routerLink directive as we saw earlier in this course to route back to the Product List page. And the Delete button calls the deleteProduct method to delete the product. Viewing the result in the browser, we see the row of buttons below the tabs. Lookin' good! But our tabs still don't work. Let's look at how to activate child routes and hook up our tabs.
Activating Child Routes
There are two ways to activate a child route, with an absolute path or with a relative path. An absolute path begins with a slash and requires definition of each segment of the URL path as part of the link parameters array. The router matches an absolute path starting from the top of the route configuration. Using an absolute path ties the route activation to the current route hierarchy, so if any levels are later added or changed, each of these paths also needs to be changed. Often a better option is to use a relative path. A relative path does not begin with a slash and is relative to the current URL segment. If the ancestor route segments ever change, this path is unaffected. We can also activate child routes in code similar to activating a primary route. Each route segment is defined within the link parameters array. We can also use relative routing, but the navigate method requires an additional parameter. We pass in object with a relativeTo property set to this.route where route here is the ActivatedRoute. This provides the router with the current route so it can append this relative path. Be sure to leave off the slash here if the first element in the link parameters array starts with a slash, the navigation is absolute regardless of whether we specify the relativeTo property. Let's give this a try. The visual elements for our tabs are defined in the product-edit.component template. When the user clicks the Info tab, the router should navigate to the info path. So we specify a routerLink directive here and assign it to the info path. We use relative routing so changes to the ancestor routing segments won't affect this route. Notice that we are using the link parameters array syntax. Since we are assigning to a simple string, we could instead use the shortcut syntax we learned earlier in this course. We add a similar routerLink directive to this Search Tags tab and route to the tags path. That's all we need to do. Let's check it out in the browser. Display the Product List, click on an Edit button, and the Edit Product page appears. Click the Search Tags tab, and the Tags page appears. We can add a single search tag or multiple tags separated by commas. Click the Add button, and the tags are added to the list. Click any existing search tag to delete it. Now click on the Basic Information tab, and we are returned to the info page. Cool! But our changes are not retained. And we are still seeing test data. So let's look at how to obtain the actual data for our child routes.
Obtaining Data for Child Routes
There are several ways to obtain data for a child route. We could use our product data service to get the data for this tab. But as we saw in the last module, getting the data in the component itself can cause the user interface to partially appear while it is waiting for the data. Another option is to define a route resolver and retrieve the data before routing to the child route's component. This is a great option if each child route requires different data. If the child route's work with the same data such as in our product.edit example Product Edit example, we can instead use a route resolver on the parent route. Even though we have multiple tabs, the display data is all part of the same product. So we get the product when routing to the parent route and read it in each child route's component. Notice that we then specify the route.parent here. these last two approaches use the ActivatedRoute snapshot for reading route data. As we saw in the last module, we could instead use the ActivatedRoute's data observable to watch for changes to the route resolver's data. Since our edit pages all work with the same set of data, let's use the parent route's resolver. Recall that this syntax provides a reference to the product data instance so our parent route and each child route with a reference will share that instance. Let's give it a try. We start with the Info tab component and delete the hard-coded data here. We'll instead define the product type as IProduct. Next, we add code to the ngOnInit method to retrieve the data from the parent's route. We could use the snapshot method to retrieve the product data, but since we are working with the edit page, we know from experience that we'll want to subscribe to the data observable instead. That way the code is notified if the user selects the Add Products option, and we need to redisplay for entry of a new product. We'll use this.route, which is the ActivatedRoute, .parent.data, and subscribe to the parent route's data. We provide a function to the subscribe method. The resolved data is passed into this function, and we use the arrow syntax to define the code that is executed whenever the router re-fetches data. Here we set our local product property to the product element of the resolved data. Where is this product data coming from? We defined it in the product.module here in the last course module. We set up a resolver to prefetch product data and added the resolver to our Product Edit parent route definition, so the child routes can read this resolver data from the parent route. Viewing the app in the browser, we now see that the Info tab displays the correct data. Wahoo! Want to try making this same change to the Search Tags tab component? If so, pause the video here. Are you read to see my solution? I deleted the hard-coded product data and set the product type to IProduct. I then added code to the ngOnInit method to subscribe to the ActivatedRoute parent's data observable. In the passed-in function, I set the local product property to the product data from the route. This is now sharing the same product instance as the parent component and the other child route. This is important because our parent component still contains the code for saving changes to the product. Any changes made to the product instance in any child component is reflected in the parent component's product instance. Let's check that out in the browser. We see that the Info tab displays the correct data. Click the Search Tags tab and it too displays the appropriate product data. We can now edit any of the product data. Click Save to save the changes, and we are returned to the Product List page. Here we see that the changes were indeed saved. Note that we using an in-memory Web API to save these changes so the next time we restart the application with npm start or refresh the page, the data will be reset to its original values. But we do have a little validation problem. Let's try and edit again. If we clear the Product Code, the validation lets us know that the Product code is required. But if we now pick the Add Product menu option, the form values are cleared but the validation errors are not. Let's make one more change to fix this. The template for each of our tab components define a form for data entry. In the product-edit-info.component template, the form is called productForm. In the Component class, we use the ViewChild decorator to obtain a reference to the template's form. We can then use that reference to reset the form every time we get new data. Resetting the form clears the form state including any validation errors. We want this code within the subscribe function. That way our forms data is reset if the user selects to add a product. First, we ensure that we successfully obtain a reference to the form. If so, we call reset. This resets the validation state on the form. Let's see if this improves the validation experience. Click to edit a product, clear the Product Code, and the validation lets us know that the Product code is required. Pick the Add Product menu option. The form values are cleared, and the validation errors are reset. Great! We could make the same change to the Product Edit Tags component, but it really isn't necessary. Every time the user selects Add Product, the application always defaults to the Basic Information tab. The Search Tags tab component template is then unloaded so none of the validation settings are retained. There is one other validation issue we should fix. If we again edit a product and clear the Product Code, we see the validation errors. But we can still click Save to save the data. Yikes! We should disable this Save button if the form is not valid. But the button is on the parent form, which doesn't know the state of the child form validation. As it is, this could be confusing for the users and potentially cause issues in our database. How do we fix this?
Validating Across Child Routes
Recall our current route hierarchy? When the user clicks the Info tab, the product-edit-info.component template is displayed in the outlet. When the user clicks the Search Tags tab, the product-edit-info.component template is completely removed from the outlet and replaced with the product-edit-tags.component template. This means that the Product Edit Info component and its form is unloaded along with any of its validation settings. And when the user clicks the Info tab, the product-edit-tags.component template is completely removed from the outlet and replaced with a product-edit-info.component template. That means that the Product Edit Tags component and its form is unloaded along with any of its validation settings. If only one form and, hence, one form's validation is accessible at any time, how do we validate across child routes? Let's think through some possible solutions. What if we define the form in the parent component and put the form's input elements in the appropriate child components? Then the parent component can track the state of the form. Nah, that doesn't work. An Angular form does not recognize input elements defined within a router-outlet, so if you are using template-driven forms, the input elements never appear in the forms model. What if we define a form in each child component? That's what we have now and saw that this option doesn't work either. Only one form is active at a time, so only its validation is known since validation is form-based. We could forget about child routes and instead put the form and all of the tab elements into the product-edit.component template. Then hide and show the different tab elements as needed. While this would work, it would be more difficult to build and maintain, especially as more tabs are added. What if we define a form in each child component like we have now but then manually perform the validation against the data instead of relying on the form? That might work. Let's give it a try. Since the Save button is on the parent product-edit.component, let's add the validation there. First, we need some type of data structure to hold the validation state of each tab. Since we could have any number of tabs, let's define a property called dataIsValid and set its type to be a set of key and value pairs where the key is the tab path name and the value is true for valid and false for invalid. Then let's add a validate method to perform all of our validation. We'll add it here at the bottom. I'll paste the code, then we can walk through it. We'll use the product data, not the form input elements, to perform our manual validation. Recall that the ActivatedRoute service data property gives us a reference to the product instance, so any changes to the product instance made on any of the tabs is reflected in the parent component's instance as well. First, we clear the validation data structure. This ensures that each time we validate, we start with a clean structure. Then we add if logic to check our validation rules for the info tab elements. Each of the validation rules defined in the HTML for this tab are repeated here. Yeah, I know, we are repeating our validation. And if someone later adds validation to the HTML, that someone will need to remember to add it here as well. But if we want field-level validation as the user tabs through and correct validation of all of the tabs, then at least at this point in time, we need validation in both places. If all of the validation rules pass, we set the value in our data structure for this tab to true. Otherwise, we set it to false. We repeat similar code for each tab. Let's also add an easy way to determine if the form is valid by adding an isValid method. I'll paste the code, and we can talk through it. This method takes in the path of the tab to check. It then performs the validation. If checking a specific tab, it returns a result from the validation data structure for that tab. Otherwise, it checks every entry in the data structure and returns true only if the validation of all tabs is true. Now we can ensure the entire product is valid before saving. In the saveProduct method, we'll call isValid passing in null to check all tabs. And now we can disable our Save button if the data is not valid. In the HTML, we bind to the disabled property, disabling the button if the form is not valid. Let's check it out in the browser. Select to edit a product. Now if we clear the Product Code, we see a validation message, and the Save button is disabled. When we select the Search Tags tab, the Save button remains disabled. Our technique works, though it does require duplicated validation logic. We still have a small user interface issue. If there were a lot of tabs, how would the user know which tab contained the validation error that is causing the Save button to be disabled? We could add an error icon to each tab that has a validation error. We'll do that later in this course. For now, let's finish off this module with some checklists you can use as you define child routes.
Checklists and Summary
When configuring a child route, add a children array to the parent route, define the child routes within that array, and remember that the child paths extend the parent route so only the child's route segment is specified here. Place the child view by defining a RouterOutlet directive within the parent component template. Each child route will then appear in this location. Every parent route component should have a RouterOutlet in its template. We can activate a child route using an absolute path. Be sure to start the path with a slash and define each URL segment. A better approach is often to activate a child route using a relative path. Then there is no starting slash, and only the child's URL segment is specified. Note that when activating the route in code using relative routing, the relativeTo property must be set to the ActivatedRoute. Read data for the child route using the ActivatedRoute snapshot or by subscribing to the ActivatedRoute's data observable. When reading the data for a child route that was resolved in the parent route, be sure to specify the ActivatedRoute parent instead. In this module, we examined what child routes are, how they work, and when to use them. We configured several child routes, placed the child view in the parent's template using a RouterOutlet directive, and activated the child routes. We looked at how to obtain data for the child route from the child route itself or from its parent's route. And since our specific scenario was a multi-tabbed edit page, we walked through how to validate across child routes. Our ProductEditComponent is now an edit shell with a the tab bar buttons and an outlet for the child tab components. And we defined a child route for each tab displaying a child component template in that outlet. Up next, let's look at how to group child routes under a component-less parent route and discern what that even means.
Grouping and Component-less Routes
Introduction
We may want to organize routes such as feature routes under a single parent route without defining another outlet. Welcome back to Angular Routing from Pluralsight. My name is Deborah Kurata, and in this module, we group child routes under a component-less parent route. As we saw in the last module, we define child routes to display routed component templates within other routed component templates. In our sample application, we display the templates from the tab components within the ProductEditComponent outlet. But there may be times that we want to organize our routes under a single parent route without defining another outlet. Here we group the product routes under a parent that has no component and, hence, no outlet, the child component templates then appear in the next higher-level outlet. For the color coding here, these are technically child routes and could be denoted with purple as with our other child routes. But since they appear within the primary router-outlet, I opted to make them turquoise to match that router-outlet. Why? Why should we group routes under a component-less route? Grouping helps us better organize our routes, especially as an application gets larger. Grouping allows us to share resolvers and other guards. We'll see how to use grouping to share a guard later in this course. And by grouping all of the routes for a feature area under a single parent, we can lazy load the routes for that feature area. We'll see how to use lazy loading later in this course as well. In this module, we group routes under a single parent route and define that parent as a component-less route. We are making a big change to our route hierarchy in this module. Instead of defining our product routes as the same level of the route hierarchy as the Welcome, Log In, and other routes, we group our product routes under a component-less parent route. Let's get started.
Grouping Routes
Currently, the configuration for our product routes looks like this. We could instead group our routes such that the other product routes are children of the products route. Since child routes extend the path of the parent route, we specify relative paths making our paths shorter and more durable as paths change over time. But, spoiler alert, these grouped routes don't work as they are shown. We'll see why in a moment. For now, let's try grouping our routes. Our product routes are configured in the product.module. I've reformatted them a bit to fit better on the page. We want to group the Product Detail and Product Edit routes as children of the products route. We start by adding a children property to the products route. Then we move the other product routes within the children array and reformat to line everything up. Now that the Product Detail and Product Edit routes are child routes, we can use relative routing. Just delete the products/ from the path. Don't forget to remove the slash to ensure the route recognizes these as relative routes. Let's check it out in the browser. Click Product List and select a product name. We are not routed to the Product Detail page. Click an Edit button, and we are not routed to the Product Edit page. We broke it!
Component-less Routes
Recall how child routes work. Child routes are displayed in a router-outlet defined within the parent component's template so the info-tab.component template appears within this child router-outlet. To display our new child routes, we'd need a child router-outlet in the parent component's template, which in this case is the Product List page. We don't want the Product Detail and Product Edit pages appearing within the Product List page. How do we get around this? We use a component-less route. Here is our current grouped route configuration. We move the Product List route as a child as well. So now the parent route has no component. Hence, it's called a component-less route. Other than its cool name, this may not sound like a big deal, but it changes how the router displays these child routes. Since the parent no longer has an associated component, the router won't attempt to route its children into the parent's outlet. Instead, the children are displayed in a higher-level outlet. Our route hierarchy then looks like this. We've defined a component-less route with three children. Since the parent route is component-less, the child component templates appear in this primary outlet. Let's give this a try. In the product.module, we add another child route definition. We give it a relative path, which in this case is the empty path, and route to the ProductListComponent. We then remove the component property from the parent route. This changes our parent route to a component-less route. And let's reformat it to match the others. Let's see if that fixes our routes. Click on Product List, and the router navigates to the Product List page. Click on a product name, and we see the Product Detail page. Click Edit, and we see the Product Edit page. Select Add Product, and we see the Product Edit page for entry of a new product. We've got it all working again. Yay! Now let's finish off this module with some checklists you can use as you group your routes under a component-less parent route.
Checklists and Summary
To group routes, define routes as children of one parent route. Be sure to specify relative paths for the child routes. But grouping alone is not sufficient. As we saw in this module, the router wants to put these child routes into a router-outlet defined in the parent component's template. To define a component-less route, add a default path that routes to the desired component. Remove the component from the parent route making it a component-less route. The child routes are then displayed in a higher-level outlet. In this module, we grouped our product routes under a single parent route and defined that parent as a component-less route. We've added a component-less parent route and grouped our product routes under it as children. We can now share resolvers and other guards and lazy load the product routes as we'll see later in this course. Next up, we'll switch gears a bit and see how to style, animate, and watch our routes.
Styling, Animating, and Watching Routes
Introduction
When working on client-side code, simply implementing features is often not enough. The app must also be user friendly and visually appealing. Welcome back to Angular Routing from Pluralsight. My name is Deborah Kurata, and in this module, we explore how to style, animate, and watch our routes. Oh my! We don't have to go over the top with our route styling or animation. A little goes a long way. In this module, we learn how to style the selected route to provide a visual indication of where the user is in the application. We look at ways to animate our route transitions to add some polish to the display. We discover how to watch our route events to better resolve routing issues. And we examine how to react to routing events to display a spinner while the route is loading. We won't add any new routes in this module. Rather, we'll polish the ones we have. Let's get started.
Styling the Selected Route
To provide context and help users keep track of where they are in the application, we may want to add styling classes to visually indicate the active route. This cascades down through each level of the route tree so parent and child routes can appear active at the same time. For example, we may want to highlight the menu option and tab that the user selected. We style the activated route using the routerLinkActive directive. We add this directive to the anchor or to its parent element. We assign this directive to a space delimited string of style classes. Here we add one style class, the Bootstrap class named active. The router adds the classes when this route is activated and removes them when the route is inactive. In this example, the selected tab is highlighted when it is selected. Let's gives this a try. Before jumping into the code, let's run the application. Select Product List and navigate to the Product Edit page. Click on the Search Tags tab, and we can see that there's a very little visual indication of the selected tab. Looking closely, the text changes from one shade of blue to another. But that isn't very easy to see. Let's instead change the tab color so the selected tab is more prominent. Our tabs are defined in the product-edit.component template. We simply add the routerLinkActive directive to the anchor tag and assign it to the name of the desired style class or set of classes. We are using the Bootstrap CSS style classes, so we set the directive to active. And since we are assigning the directive to a string and not to a property or expression, we don't need the square braces around the directive. We'll repeat this for each tab. That's it! Looking back at the application in the browser, the tabs are now clearly indicated. Cool! Let's add a little more style to the tabs. If the user makes an error here and navigates to another tab, we see that the Save button is disabled. But if there were lots of tabs, it wouldn't be easy for the user to determine which tab has the validation error. Let's add an icon to the tab if the tab's data contains a validation error. Recall that our product-edit.component has an isValid method that takes in the tab path and returns true if the data in the tab is valid. We'll use this method to determine when to add the validation error icon. In the template, we add a span tag next to the text of the tab. We'll use the ngClass directive, which turns on a style class based on an expression. We'll use the Bootstrap CSS glyphicon style to add an exclamation point. And we want this to display this icon if isValid is not true for the Info tab. We'll copy and paste this for the Search Tags tab and change the info text to tags. So the icon will appear here is the Search Tags tab is not valid. Let's check it out in the browser. If we cause a validation error, we see the icon added to the tab. If we navigate to another tab, that icon still appears. This lets the user know which tab contains the validation error. That seems helpful. Now our tabs style indicates which tab is active, and if input elements on that tab have validation errors, we indicate that as well. But what about the menu? If we pick Add Product, there's no visual indication that the menu option was selected. Let's add the routerLinkActive attribute to highlight the selected menu option. The application menu is defined in the app.component template. We can make this same change to the main menu options adding the routerLinkActive directive to each of the anchor tags. Let's check that out in the browser. Click Product List and no style change. Bummer! Looking back at the code, this is because we are using a list to define our menu. Its style is overriding our link style. Let's instead move the routerLinkActive directive to the parent li element. The routerLinkActive directive works on any ancestor of the element containing the routerLink directive. So let's remove these from here and add it to the li elements instead. Viewing the result in the browser, we have styling. Yes! But if we select Add Product, both the Product List and Add Product menu options are marked as active. What? Looking at the routerLink directives for the Product List and the Add Product menu options, we see that the first path segment of each path matches. That's why both options are marked as active. To require an exact path match, we add the routerLinkActiveOptions directive on the Product List route. We add the routerLinkActiveOptions directive to the same element as the routerLinkActive directive. We then assign it to an object with the exact property set to true. Note that we only need to add the routerLinkActiveOptions directive for the Product List route because that is the only one here that could match multiple route paths. Viewing the application in the browser, we see the one selected menu option marked as active. Now our users have a better indication of where they are in our application. Next, let's add a little visual interest with animation.
Animating Route Transitions
Smoothly transitioning from one route to the next adds some polish to an application. We define route transitions with animation. There are two basic ways we can animate our route transitions--with CSS animation or with Angular animation. CSS animation is quick and easy. We define some CSS and every route in every component is animated. However, this is not really animating our routes. It's instead animating specific HTML elements of our application. As we access each route the HTML elements are animated making it look like it's animating our routes. Using Angular animation to truly animate our route transitions is a bit more complex. We need to configure the animation, then apply it to every component we want to animate. At the time this course was recorded, discussions were underway to add animation features that would make it easier to animate routes without modifying every component. For more information on animating route transitions using the current Angular animation features, check out the Angular documentation. Since our goal in this course is simply to make our route transitions look smoother, not to learn Angular's animation features, let's try out the CSS animation technique. Here is the application stylesheet I provided with the starter files. I added two different sets of CSS to animate our route transitions. The first option fades in every div element within our application. The second option slides in our primary panel. In both examples, the selector name is our application selector, pm-app. We then specify div in the first example or div.panel.panel-primary to animate each div defined within these CSS classes. Uncomment one or the other to try them. Let's try out the slide transition. I'll uncomment that one. Let's see it in action. Our panels now slide in from the left instead of just appearing all at once. This gives the impression of route animation. Nice! But notice the animation of the Product List page. The header slides in nicely, but the data just pops in. What's that all about? This occurs because the page first transitions, then waits for its data, then displays the data when it arrives from the server. This would look nicer if we added a route resolver and prefetched the data. Then the entire page of data would slide in. Another good reason to use route resolvers. Whether you use any animation, CSS-based animation, or Angular's animation depends on your user interface design, your users, and your application requirements. Next, let's look at what we can do with routing events.
Watching Routing Events
Any time the user navigates in the application, the router generates routing events that we can use to monitor, troubleshoot, or perform logic. These routing events include NavigationStart, which is triggered when navigation begins, RoutesRecognized, which is triggered when the router has found a valid path in the configuration that matches the parsed URL, NavigationEnd, which is triggered when navigation ends successfully, NavigationCancel, triggered when navigation is cancelled such as by a routing guard or redirect (we'll talk more about routing guards a little later in this course), and, of course, NavigationError, which is triggered when navigation fails. We can see these events occur if we enable tracing. To enable route event tracing, add an object as a second argument after the route definitions in the root route configuration and set the enableTracing property to true. Let's try it out. Our root route configuration is in the app-routing.module. Let's add enableTracing to this configuration. We add an object as a second argument to the forRoot method after the route definitions and set the enableTracing property to true. View the app in the browser and open the developer tools. Refresh the page, and we see each routing event as it occurs. Scrolling up, as expected, the NavigationStart event comes first. Then RoutesRecognized and NavigationEnd. The router assigns a sequential Id number to every navigation, which we can use to match up a set of events. Now let's navigate to the Product List and clear the console. Click the Edit button for one of the products, and we see the routing events log to the console. Scrolling up, let's look closer at the NavigationStart. It has unique identifier and includes the navigation URL. Next is the RoutesRecognized event. Since it is part of the same navigation operation, it has the same identifier. Notice the difference here between the URL and urlAfterRedirects after redirects. The URL has the original navigation URL, but the Product Edit route has a default child route that redirects to the info page, so the urlAfterRedirects includes that redirect. The product information displayed here is from our product data service. Our last routing event is NavigationEnd. It has the same identifier, URL, and urlAfterRedirects. Feel free to add and remove the enableTracing option as desired. Keeping it off until you need it keeps unneeded information from appearing in the console. Route event tracing can be helpful as we get into more complex routing scenarios. There may be times that our routes don't work as expected. Tracing is one tool in our toolbox for resolving routing issues. But we can also use these events in our code and perform an operation reacting to a routing event. Let's look at that next.
Reacting to Routing Events
We can watch for routing events and react to them to display a spinner, log actions, or execute logic such as updating a page title. For example, we could display a spinner when the NavigationStart event occurs and turn off the spinner on NavigationEnd. Any guesses on how we watch for routing events so we can react to them? If you said by subscribing to an observable, you are correct. The Angular router has an events property that is an observable. We can subscribe to this observable to get notification of each routing event in the code. We provide a function to the subscribe method. The routing event, which is of type Event, is passed into this function. In the both of the function, we can check the type of event and react accordingly. Let's see what we can do. Have you noticed the delay from the time we activate the Product Detail route to the time the Product Detail template is displayed? This is because we are using a resolver that retrieves the data during the navigation. Let's let the user know that something is happening during this delay by displaying a spinner. We'll turn the spinner on at the start of the navigation and turn it off when the navigation completes. We want the spinner to appear for every route, so we'll start listening for routing events when our app first loads. This means we'll add the code to the app.component. First, we add the appropriate router imports. As we saw in the slide, we need Event to correctly type our routing event. And we'll need each of the routing events that we want to match. We want to watch NavigationStart and NavigationEnd, but what if an error occurs? We should watch NavigationError so we can turn off the spinner if an error occurs. And we should probably watch NavigationCancel as well. Now let's define a property that specifies whether or not a route is loading. We'll call it loading and set it to true. We'll bind to this property to turn our spinner on and off. The router is already defined as a dependency in the class constructor so the Angular injector will provide an instance of the router when the application is loaded. Since we want to start watching routing events as soon as the application loads, we subscribe to the router's events in the constructor, router.events.subscribe. We then define the function to execute each time we receive an event. The current event is passed into this function. We'll call it routerEvent and define its type as Event. When a routing event occurs, we'll call a method that checks the routerEvent. We'll call it checkRouterEvent and pass in the event. Next, we create the checkRouterEvent method. I'll paste the code, and we can talk through it. In this method, we check the type of navigation event. If the event is NavigationStart, we set loading to true to turn on our spinner. When the navigation ends, is cancelled, or has an error, we set loading to false to turn off our spinner. The only thing left is to add the spinner that appears when the route is loading. Here in the app.component template at the very top of the file, we'll display the spinner using a Bootstrap glyphicon. We'll use ngIf to only display the spinner if the route is loading. I included the style classes for the spinner in the starter files as part of styles.css. The spinner style class increases the size of the glyph and positions it over the current display in the center of the page. The glyphicon-spin style class uses CSS animation to spin the glyph. Looking back in the browser, notice that we don't see the spinner when we display the Home page. That's because it's loading too quickly. We may expect to see this spinner when loading the Product List page. But, no. Any guesses as to why that may be? Let's look at it again. Notice that the page header immediately appears, then the page waits for the data. Since the page appears before the data, it again loads too quickly to display the spinner. Now let's click on a product name to display the Product Detail page. There's our spinner. Yay! Note that if you are not seeing a spinner here, I'll talk about a fix in a moment. How does this work? Recall that we are using a resolver to ensure that we retrieve the data before displaying the page. So the navigation starts, the spinner appears, the navigation waits for the data to be retrieved. Then the navigation ends, the spinner disappears, and the Product Detail page appears. This is also the case for the Product Edit page, so we see the spinner here as well. If you want the spinner to appear while the products are loaded for the Product List page, you can add the spinner directly to the Product List page and turn it off when the product property is populated. Or a better option is to create a resolver for the Product List. If you are coding along and not seeing the spinner when displaying the Product Detail or Product Edit pages, we may need to slow down the data retrieval. Recall that our application is using an in-memory Web API server to serve up the data. That may be occurring too fast for our spinner to appear. So here in the product module, add a delay. This allows us to delay the loading of the data better simulating how the application will work when using an actual back-end web server. Try it again and see if the spinner now appears. Let's finish up this module with some checklists we can use when we style, animate, and watch our routes.
Checklists and Summary
Styling the active route helps the user track where they are in the application. Styling the active route using the routerLinkActive directive. Be sure to specify the directive on the correct element. If the anchor tag is within a list element, the routerLinkActive must reside on that parent list element. For an exact path match when determining the active route, use the routerLinkActiveOptions directive and set exact to true. This can prevent multiple routes from appearing as active when using similar root path names. To animate route transitions or at least give the appearance of animating the transitions, you can use CSS animation or Angular animation. CSS animation is easier because with one set of CSS, you can animate every component in the application. But animating routes using Angular animation may be getting better with future releases. Enable route tracing any time you want to watch routing events in the console. This is especially helpful when troubleshooting routing issues. To turn on route tracing, add enableTracing to the root router module configuration. We can react to routing events by subscribing to the router's events observable. Check the event type as needed and add code to react as desired. This is great for displaying a spinner or logging actions. In this module, we examined how to improve the look of our application routes. We styled the selected route and animated route transitions. We watched our routes by enabling route event tracing. And we reacted to routing events to display a spinner in our example. We didn't add any routes in this module. But at this point, our application routes are looking pretty good. There is only one route remaining in this diagram that we have not yet implemented. Up next, let's look at how to configure and use secondary routes.
Secondary Routes
Introduction
Multiple routes displayed at the same time and at the same level of the hierarchy are referred to as peer, sibling, auxiliary, or secondary routes. Regardless of what we call them, they are useful for building more complex user interfaces. Welcome back to Angular Routing from Pluralsight. My name is Deborah Kurata, and this module is about secondary routes. Secondary routes make it easy to display multiple panels or panes on the page, each containing different content and supporting independent navigation such as for a dashboard like this. If each of these panels displayed a single component, then using nested components is sufficient. But imagine that each panel supports independent navigation. For example, click on a chart to show its details or click on a country to drill down. Then each panel needs its own routing supported by one primary and any number of secondary routes. In this module, we begin with an overview of secondary routes, what they are, and when to use them. We then look at defining a named RouterOutlet to place the secondary views, configuring secondary routes, and activating secondary routes. Lastly, we examine how to clear secondary outlets. In our sample application, we'll add a name secondary outlet to define a popup area on the page. Then we'll add a secondary route to display a message log in that outlet. Let's get started.
Using Secondary Routes
Before we look at how to use secondary routes, let's once again review how our primary routes work. This template lays out a navigation menu and other elements that make up the main page of the application. We use the router-outlet directive to define where to place the routed component's template for display. This is called the primary outlet. When the user views the application in the browser and routes to a component, that component's template is displayed within the primary router-outlet. If we want to display another routable panel of information at that same level of the route hierarchy, we define a second outlet and display a secondary set of routes in that outlet. A secondary route can have its own child routes, its own route parameters, and its own secondary routes. In this example, we display a simple message log, but you can imagine the possibilities. We can use secondary routes to display a dashboard with multiple panels that each support multiple routes. We could build a multi-window application like Outlook or Gmail. We could display a second panel that allows the user to take notes or make comments. We could display messages or other information to the user, and so on. Now let's start by defining the router-outlet for the secondary routes.
Defining a Named Router Outlet
The activated component templates for our primary routes are displayed in our primary outlet. And the activated components for our secondary routes are displayed in the secondary outlet. Hmm. How does the router know where to place which content? We must give each secondary router-outlet a name. Then we use that name to tell the router to place the secondary content in a specific outlet. We can define any number of secondary outlets at the same level of the hierarchy. But each must have a unique name. Let's define a named router outlet for our secondary routes. For our sample application, we want to define a panel on the right side of our main page. We can then route any component to that panel using a secondary route. The elements for our main page are in the app.component template. Let's divide the container into two columns using some Bootstrap CSS style classes. Within the div container, we first define a row, then we make the first column for our primary outlet 10 units wide and the second column 2 units wide. We'll add the secondary outlet within the right set of columns. Every secondary outlet needs a name. We'll call this one popup because we'll use it to route to any component we want to pop up here on the right. We could add any number of other named router-outlets, but one is enough for our purposes. We can view the result in the browser, but the only difference we should see is that our primary content is a bit narrower. Now let's configure our secondary route.
Configuring Secondary Routes
Secondary routes are configured similar to primary routes. We define a path for the route and specify a reference to a component. But for secondary routes, we also define an outlet. This is where we identify the name of the secondary outlet where this component's template will appear. So when this secondary route is activated, the message component's template is displayed in the secondary outlet named popup. Let's give it a try. In our sample application, we want to display messages in the popup outlet when the user clicks Show Messages. I've already created the message component and defined the template inline here. The template displays a set of messages managed by a MessageService. The MessageService demonstrates how to use a service to share data across components. I created the MessageService to share the message data with all of the features of the application. Any part of the application can access this service and add a message that appears in the popup outlet. Since the messages are their own feature, I defined a feature module. It is here that we'll configure the route for the MessageComponent. We first add an import statement to import RouterModule from the @angular/router library. Then we add the RouterModule to the list of imports. Remember how we configure a route? Since this is a feature module, we use the forChild method and pass in an array of route definitions. Since there is only one component in this feature module, we only need one route definition at this time. We'll specify a path of messages, reference the component, and specify the outlet to use to display this route, in this case, the router-outlet named popup. That's it. But we can't try it until we activate this secondary route. Before these most recent changes, our sample application route hierarchy looked something like this. We have a primary router-outlet defined for or primary routes. Our product child routes appear in that outlet as well because their parent route is component-less. We have another router-outlet defined for our Product Edit child routes. We didn't have to add a name to the child outlet because the router could tell which outlet to use based on the route hierarchy. With secondary routes, the outlet is defined at the same level in the route hierarchy as other routes, so we give the secondary outlet a name. We then configure a secondary route specifying a path, a component, and an outlet. In our example, we configured a route for the MessageComponent. We could configure any other secondary routes to use this same outlet. For example, we could define Product Summary component and display a summary in the popup as the user clicks on products in the Product List or as the user edits the product. When routing to a named router-outlet, the URL in the address bar reflects both the primary and secondary route paths. The secondary paths appear within parentheses after the primary path. Inside the parentheses is the name of the outlet, a colon, and the secondary route path. This may look a little odd, but the Angular router understands this syntax and navigates appropriately. So how do we activate a secondary route?
Activating Secondary Routes: RouterLink
As with any other type of route, we activate a secondary route in the HTML using the routerLink directive. But the syntax is a bit different. We define an objects as the first element in the link parameters array and assign the outlets property to a name and value pair where the name is the name of the outlet to use and the value is another link parameters array specifying the secondary path. We can route to multiple outlets at once. This code activates the Product Edit route in the primary outlet and a Product Summary route in the secondary outlet named popup. However, at the time of this recording, this syntax was not working. The router was adding an extra slash between the primary route and the secondary route when building the URL, and then it could not understand the result. Hopefully this will be fixed in an Angular update. So let's give this first syntax a try. We are looking at the app.component template. To activate a secondary route, let's modify the Show Messages menu option to display the MessageComponent in the popup outlet. We'll add the routerLink directive and assign it to a link parameters array. In the first element of that array, we define an object, specify the outlet's property, and provide a key and value pair. The key is the name of our outlet, which is popup in our example, and the value is another link parameters array. In the first element of that array, we specify a path of messages. Let's double-check our syntax here because there are lots of braces. Link parameters array, object, object, link parameters array, okay. Viewing the application in the browser, click on the Show Messages option, and the Message Log appears. Notice the URL in the address bar. It includes the primary route and the secondary route in parentheses. Click Log In and log in using any name and password. Then a message is logged to the message list. Notice again the URL in the address bar. The primary route was changed, but the secondary route stays the same. Now that the messages appear, we want to change the menu to say Hide Messages. Clicking the Hide Messages option or the X here should hide these messages. To modify the menu based on the state of the display, we need a property that tracks when the messages are shown and when they're not. We need code to set that property when routing to the secondary route. So we'll need to activate the secondary route with code instead.
Activating Secondary Routes: In Code
In addition to routing to a secondary route in HTML, we can route in code. The syntax is similar to routing with a routerLink directive. We define an object as the first element of the link parameters array and assign the outlets property to a name and value pair where the name is the name of the outlet and the value is another link parameters array specifying the secondary path. Just like with the routerLink directive, we can route to multiple router outlets at once. We specify the primary route in the first elements of the link parameters array, and then any secondary routes. This code activates the Product Edit route in the primary outlet and the Product Summary route in the secondary outlet named popup. Also like the routerLink directive, at the time of this recording, this syntax was not working. The router was adding an extra slash between the primary route and the secondary routes when building the URL. Hopefully this will be fixed in an update. As a workaround, we can specify both the primary and secondary routes using the outlets property. Even though we don't give it a name, the primary outlet has a name called primary. Here we define key and value pairs for the primary outlet and for the secondary outlet named popup specifying a link parameters array for each. Another option is to use the navigateByUrl method and build the URL manually. But it is often better to let the router build the URL from the link parameters array using the navigate method. Let's try activating the secondary route in code. For our sample application, we want the Show Messages option to activate the second route in code and set a property that tracks whether the messages are shown. When the messages are shown, we want to instead display the Hide Messages menu option. Selecting the Hide Messages option should clear the secondary route. Let's start by adding a property to the MessageService that tracks whether the messages are displayed. We'll call it isDisplayed and set its initial value to false. Since this property is defined within a service, this property is shared and available to any class that injects this service. Here again is the Show Messages menu option in the app.component template. Instead of using the routerLink to activate the secondary route, let's call a method on click. We'll name the method displayMessages. Be sure to add the opening and closing parentheses to indicate that this is a method. In the AppComponent class, we'll add that displayMessages method. It takes no parameters and returns void. In the method, we route to the secondary route. We call this.router.navigate passing in a link parameters array. In the first element of that array, we define an object, specify the outlet's property, and provide a key and value pair. The key is the name of our outlet, which is popup in our example, and the value is another link parameters array. In the first element of that array, we specify the path of messages. Next, we want to set our new isDisplayed property to true. That's in the MessageService. So we need to import that service, add it to the constructor, and then we can set the property here. While we're here, let's add a hideMessages method that simply sets isDisplayed to false. We'll add the code to this method to clear the secondary outlet in the next clip. Going back to the app.component template, we can now use the isDisplayed property to display the correct menu option text. We'll add an ngIf directive here to display the showMessages option if isDisplayed is false. Then copy this option and paste it. We change the text to Hide Messages and display this option if isDisplayed is true. When the user clicks this option, we call our new Hide Messages method. Let's check it out in the browser. Clear the URL to start fresh. Click Show Messages, and our messages appear in the secondary outlet. Our menu option changes to Hide Messages. Click Hide Messages, and the menu option changes to Show Messages. But the secondary router-outlet is not cleared. Let's hook that up next.
Clearing Secondary Outlets
We may want to clear a secondary outlet and close its content. We can do that with the routerLink directive. We define an object as the first element in the link parameters array. We set the outlets property to a key and value pair where the key is the name of the outlet, and the value is null. We clear a secondary outlet in code using the navigate method and the same link parameters array. We can also use navigateByUrl. Notice here that we simply leave off the outlet information so this method clears the secondary outlets and navigates to the login route. Let's try clearing our secondary outlet. We already have a hideMessages method here in the app.component. In this method, we'll clear the secondary outlet by navigating to the outlet with a value of null. We'll call this.router.navigate passing in a link parameters array. The first array element is an object with the outlet's property set to a key and value pair. The key is the name of our secondary outlet, which is popup, and the value is null. Let's check it out. Clear the URL in the address bar, then click Show Messages. Log in to add a message to the Message Log. Then click Hide Messages, and the secondary outlet is cleared. Click Show Messages to show them again. Notice this X here. The user should be able to clear this outlet clicking on the X as well. In the message.component template, we have a button containing an X that should clear the message log from the secondary outlet effectively closing it. We could use the routerLink directive here, but this code calls a method when the user clicks a button. In the close method, we call this.router.navigate and assign the popup secondary outlet to null. Then we set the messageService.isDisplayed property to false to specify that the Message Log is no longer displayed. Let's check it out in the browser. Clear the URL to start fresh. Then let's log in so we have a message to display. Click Show Messages, and the Message Log appears in the secondary outlet. Click Hide Messages, and the Message Log is cleared from the secondary outlet. Click Show Messages again. This time click the X to close the Message Log. Looks like we have our secondary route working. Note that if we were only going to show messages here in this secondary outlet, we should use a nested component instead. Secondary outlets only make sense if we are going to display multiple routed components within the outlet. For example, we could replace the messages with a product summary when editing the product. Now let's finish up this module with some checklists we can use when working with secondary routes.
Checklists and Summary
To define secondary routes, add another RouterOutlet directive within a template that contains a RouterOutlet. To distinguish the outlets for secondary routes from outlets from primary routes, each secondary RouterOutlet must be uniquely named. When configuring a secondary route, add the outlet property to the route definition and set that outlet property to the name of the associated RouterOutlet directive. The activated component's template will then appear in that outlet. Activate a secondary route using a link parameters array with an object as the first element in the array. Set the object's outlets property to a key and value pair. As the key, define the name of the outlet. As the value, define another link parameters array containing the URL segments for the secondary route. In a template, activate using the routerLink directive. Activate in code using the router's navigate method. In either case, the link parameters array syntax is the same. Clear a secondary outlet using a link parameters array with an object as the first element in the array. Set the object's outlets property to a key and value pair. As the key, define the name of the outlet. As the value, specify null. In the template, clear the secondary outlet using the routerLink directive. Clear in code using the router's navigate method. In either case, the link parameters array syntax is again the same. In this module, we began with an overview of secondary routes. We then defined a named RouterOutlet for the secondary route, configured this secondary route, and activated the secondary route to display it in its named outlet. We then look at how to clear a secondary outlet. We now have our secondary route in place. Yay! Up next, let's talk about how to protect our routes with route guards.
Route Guards
Introduction
We may need to check whether route navigation is permitted for security, authorization, or monitoring purposes, or prevent the user from leaving a route without confirmation. Welcome back to Angular Routing from Pluralsight. My name is Deborah Kurata, and in this module, we guard our routes. Yeah, I had to do it. I had to go literal here with a route guard. Our users aren't getting past this guy unless they meet the specified criteria. Did they have appropriate authority to access this route? Have they completed their task before leaving this route? We achieve all of this with route guards. The Angular router provides several types of guards including canActivate to guard navigation to a route, canActivateChild to guard activation to a child route, canDeactivate to guard navigation away from the current route, canLoad to prevent asynchronous routing, and resolve to prefetch data before activating a route. We covered resolve earlier in this course in the Prefetching Using Route Resolvers module. And we build a canLoad guard in the next module when we cover asynchronous routing. In this module, we begin with an overview of route guards, what they are, and how to build them. We then build a canActivate guard and look at how to share data with a guard. We discuss the canActivateChild guard and build a canDeactivate guard. In our sample application, we guard access to the product routes ensuring that the user is logged in before viewing or updating product information. And now that we've grouped our product routes, defining the guard on the parent guards each of the child routes. And we guard leaving the Product Edit route warning the user if they try to leave before saving their changes. Let's get started.
Using Route 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 require that the user log in before accessing a specific route. We may want to warn the user before leaving a route, such as asking whether to save before navigating away from a edit page with unsaved changes. We may want to retrieve data before accessing a route. Earlier in this course, we prefetched the data for the Product Detail and Product Edit routes. For each of these, we use a route guard. The router first executes the canDeactivate guards for the current route to determine whether the user can leave that route. If a feature module is loaded asynchronously as we discuss in the next module, the canLoad route guard is checked before the module is loaded. The router then checks the canActivateChild guards and then the canActivate guards. After all other route guards are checked, then the resolvers are executed. It makes sense that this is last because we would not want to execute a resolver and retrieve the data for a route until we are certain that the user can access that route. If any guard returns false, all pending guards are cancelled, and the requested navigation is cancelled. Now that we know how guards are processed, let's look at how to build one. A route guard is often built as an Angular service similar to how we create any other Angular service. But we can also create it as a function as we saw with the resolver earlier in this course. To build a guard as a service, we export a class, decorate it with the injectable decorate, and import what we need. To make this service behave as a route guard, we implement the interface appropriate for the guard type. We then need to import that guard type from the @angular/router library. When we implement the interface, we must define the associated method. In this example, the method is canActivate. 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. If the returned value is a promise or an observable, the router will wait for that promise or observable to complete before proceeding with the navigation. Because the guard class is a service, we need to register the service provider with Angular's injector. Unlike other services, however, the guard service provider must be provided at the Angular module level, not in a component. By providing this service at the module level, the router can use these services during the navigation process. We register the service by adding it to the providers array. To use the guard on a route, add the guard to the route definition in the configuration. Simply add a property for the desired type of guard. In this example, it is the canActivate guard, so it is the canActivate property. Set that property to an array containing a list of references to the guard services. In this example, we define only one, but we can add multiple guards on any route at any level of the route hierarchy. We can also share guards across a set of routes. For example, say we want to guard each of our product routes. We could add the guard to each child route, or we could just add the guard to the parent route. Adding a guard to a parent route guards each of its children. So guarding a route involves three steps--building a guard class as an Angular service, registering the guard in an Angular module, and adding the guard to the desired route or routes. Let's try this out and build a canActivate guard.
CanActivate Guard
As its name suggests, we use the canActivate guard any time we want to check criteria before activating a route. It is commonly used to limit access to specific users. It is the primary mechanism for adding permissions to the application. We can also use the canActivate guard to ensure prerequisites are met such as requiring our user to log in before accessing a route. The canActivate guard is called any time the URL changes and matches the associated route, even if only the route parameters change. Let's give it a try. For our sample application, we want a canActivate guard that ensures the user is logged in before accessing any of the product routes. We begin by creating a new file for the guard service. Checking for a login is related to the user, so we'll add the guard to the user folder. Select New File and name it auth-guard.service.ts where auth is short for authorization. I'll paste the boilerplate code for the service, and we can talk through it. This class is called AuthGuard since this particular guard should be checked before activating a round. We implement canActivate and create a canActivate method. The canActivate method has two parameters. The ActivatedRouteSnapshot provides information about the current about-to-be activated route at a particular moment in time. We've used the ActivatedRouteSnapshot before in this course to get information on the current route, such as the route parameters and route data. The second parameter is the RouterStateSnapshot. It provides access to the entire router state. What do we want this method to do? Well, if the user is logged in, the route can be activated, so we should return true. If the user is not logged in, we should return false and instead route the user to the Log In page. So, first, we need to determine if the user is currently logged in. Luckily for us, the authorization service I provided with the starter files for this course has code to do just that. It provides a method called isLoggedIn that returns true if there is a current user. Otherwise, it returns false. The double bangs here effectively coerce the object to a Boolean value. Back in the guard class, we want to call the isLoggedIn method from the authorization service, so we need an instance of that authorization service here. If we want to route the user to the Log In page, we need the router as well. We define an import statement for the authorization service, add Router to the router import statement, and specify them both as dependencies in our constructor. Since there are several steps we want to perform here, we'll build a method. I'll paste the code, and we can walk through it. The method is called checkLoggedIn and returns a Boolean value. It calls the authorization service isLoggedIn method and returns true if the user is logged in. The requested route is then activated. Otherwise, it redirects the navigation to the Log In page and returns false cancelling the navigation to the requested route. We call this method in the canActivate method. Because this class is a service, we need to register it with Angular's injector. Since the auth-guard is part of the user feature, we'll add it to the user.module. We start by importing our guard service, then adding it to the providers array. Now the router can get an instance of this service during the navigation process. Our last step is to attach the guard to any route that needs it. We want it to guard each of our product routes in the product.module. We add the import statement for the guard, then we can attach canActivate to the desired routes. We could add the guard to each of the product routes, or we could just add the guard to the parent route. By adding the guard to the parent route, as we add more product routes over time, the children are automatically guarded. Let's try this out in the browser. Ensure we are not logged in, then click Product List. The Log In page appears for the user to log in. Click Home to navigate back to the Welcome page and click Add Product. We are again navigated to the Log In page. Our guard works! If we then log in using any Id and password, we are taken to the Product List page. But we were trying to get to the Add Product page. Regardless of where the user was trying to go, we always route them to the Product List page after logging in. That's because here in the login.component, we hard code in navigation to the products route, which takes us to the Product List page. Wouldn't it be better if we kept track of where the user was trying to go, share that information with the route guard, and returned the user there after logging in? Yes, yes it would be better. So how do we share data with the route guards?
Sharing Data with a Guard
There are several ways to share data across our routes and with our route guards. We can define route parameters to pass data from a route to its routed component. We can read route parameters from within our route guards using the ActivatedRouteSnapshot parameter. This is a great technique if the guard requires the route parameters, for example, if an edit route guard wants to confirm the user has access to a specific product based on the product Id provided on the URL. Our route can prefetch data required by the route component and share that data with its child routes using a resolver service. However, the resolver is executed after the other route guards. So the other route guards don't have access to that prefetch data. Attempting to access prefetch data will always return undefined. So using resolve won't work for sharing data with a guard. We could, however, use the route's data property as shown earlier in this course if we want to pass static data to a guard. A common way to share data in an Angular application is to define properties in a service. Since a service is a singleton, there is only one instance that is shared among all code that uses the service. If we define properties in that service to hold data, those properties and, hence, that data are shared everywhere including our guards. For our specific scenario, we have a route guard on the product routes that redirects to the Log In page if the user is not logged in. We want to retain the user's original requested URL so that after the login, we can route the user based on that URL. So let's try the Angular service approach. We already have an authorization service that was provided with the starter files. Let's add a property to that service to track our redirect URL. This property is now accessible to any class that injects this authorization service. In our route guard service, we are already injecting our authorization service, so we can set this redirectUrl before navigating to the login route. What do we set it to? Well, let's start by assuming we'll pass it into this method, add a URL string parameter, then set the redirectUrl to this passed-in URL. Now we, of course, see an error here where we are calling this function. You'd think that one of these two parameters here must have the URL, right? Let's try the ActivatedRouteSnapshot. Yes, it has a URL, but it is of type UrlSegment array. This snapshot has separated the URL into its individual segments. We really just want the URL string and don't want to try to build it from the URL segments. Let's try the RouterStateSnapshot. Its URL property is the string containing the entire URL. Great! Next, we change the login method in the login component. Instead of hardcoding it to route to the products route, we'll use the redirectUrl property from our authorization service. This class already injects the service, so we can simply access the property. I'll paste the code, and we can talk through it. First, we check whether there is a redirectUrl. There won't be if the user clicks the login menu option before accessing any product routes. If there is a redirectUrl, we use the navigateByUrl method to navigate to that URL. Otherwise, we navigate to the Product List page. Now let's check it out in the browser. Start on the Home page and ensure you are now logged in. Click Add Product, and the Log In page displays. Enter any Id and password and click Log In. The Add Product page appears. Yes! Use a service any time you need to share data across the application, and it's great for sharing data with our route guards. From this point onward, it's going to be more of a challenge to try out anything in our application. Every time the browser reloads, it will require that we log in. You may want to consider removing this guard as you add features or debug your application. And it's easier to comment out since it's added only once to the parent route. Just don't forget to put it back in before deployment. Next, let's look at the canActivateChild guard.
CanActivateChild Guard
The canActivateChild guard is similar to the canActivate guard except that it is called when a child of the route is activated and not the route itself. It is commonly used to limit access to child routes and ensure prerequisites for child routes are met. The canActivateChild guard is called any time the URL changes and matches the associated child route even if only the route parameters change. The key difference between the canActivate guard and the canActivateChild guard is that the canActivate guard will not re-execute if only the child route is changed. Let's look at some scenarios. If the user is on the Product List page and clicks the Add Product menu option, only the child route changes. The canActivate guards on the parent route are not executed again, but any canActivateChild guards are. Same thing for our tabs. Any canActivate guards on the parent Product Edit route are not re-executed when changing tabs. But any canActivateChild guards are. Building a canActivateChild guard is very similar to building a canActivate guard so we won't walk through an example here. Let's move on and look at the canDeactivate guard.
CanDeactivate Guard
The canDeactivate guard checks criteria before leaving a route. It is commonly used to check for unsaved changes before leaving an edit page and obtaining a confirmation from the user before leaving an incomplete operation and, therefore, potentially losing work. The canDeactivate guard is called any time the URL changes and matches a different route even if the only difference is the route parameters. Note that the canDeactivate only works when navigating within the Angular application. It does not check the guard is the user navigates to an entirely different site or closes the browser. Let's build a canDeactivate guard. Let's navigate to the edit page and make some changes. Currently if the user navigates away from this page, their changes are lost. Bummer! It would be more user friendly to notify them that they have unsaved changes and allow them to return to save their data before navigating away. Let's build a product edit guard to do just that. We begin by creating a new file for the guard service. This service is specifically for checking unsaved changes to product data, so we'll add the guard to the products folder. We select New File and name it product-guard.service.ts. I'll paste the start of the code, and we can talk through it. Here we define a ProductEditGuard. This particular guard should be checked before leaving the Product Edit route, so we implement CanDeactivate. The CanDeactivate interface is a bit different. It is a generic interface meaning it requires a generic parameter. The generic parameter identifies the type of component that will use this guard. We plan to use this guard with a ProductEditComponent's route, so we specify ProductEditComponent as the generic parameter. Next, we implement the canDeactivate method. The canDeactivate method passes the component in as its parameter. The parameter must be the same component type defined in the generic parameter here. For simple cases, this method can return a Boolean value--true to deactivate the route and continue to the new route, false to cancel the route deactivation and return to the current page. Now that we have the basic code in place, what do we need this guard to check? Our ProductEditGuard must determine if there are any unsaved changes to the product data. If so, it should display a warning and allow the user to decide whether to proceed or return to the edit page to save their changes. Since the ProductEditComponent is passed into the canDeactivate method, we can access any of its properties or methods. Let's take a look at the product-edit.component. How do we determine here whether the product data has changed. Recall that our edit feature is not just one form. We have a form for the Info tab and another form for the Tags tab, so we can't just ask the form for its dirty state. Instead, we need to implement something ourselves. There are several ways we can track the dirty state of the product data. For our example, we'll keep a copy of the original product data and check the updated product data against the copy to see if the user has made changes. Let's start by defining two properties--currentProduct of type IProduct and originalProduct also of type IProduct. We don't want any external code to use these properties, so we marked them as private. The product property here retains the product being edited. We could change this property everywhere to use our new currentProduct property. But let's instead just change the productProperty to be a getter and setter. The getter returns the currentProduct. The setter sets the currentProduct to the passed-in value. This is the product that the user edits. The setter then uses Object.assign to make a copy and retain the original values for comparison. Note that it is important that the currentProduct property be the value and the originalProduct property be the copy. That's because the parent ProductEditComponent and each child edit component shares the currentProduct object instance that we obtained from the resolver we created earlier in this course. Next, we need a property that returns whether or not the product data was changed. We'll define an isDirty property with a getter that returns true if the originalProduct does not match the currentProduct. There are numerous ways to compare the two products, but we selected a simple technique that works for our simple case. The getter here compares the stringified version of the originalProduct with a stringified version of the currentProduct. Be careful if you use this approach to ensure that the properties of the object are always defined in the same order. Otherwise, even if the data may match, the stringified objects won't match. A more versatile technique is to walk through each property of both products and compare their values. But this will work for our simple example. Now we can use this isDirty property in our guard. Going back to our guard, the component is passed into the canDeactivate method so code in the method can access the component properties. We'll access our new isDirty property. I'll paste the code, and we can walk through it. If the component isDirty property is true, the code first pulls the productName from the component products property. If the value is null, it sets the name to New Product. We then use a confirm to notify the user and request a confirmation before navigating away from the Product Edit page or navigating to a different product. In the confirmation message, we use a back tick to define a template literal and display the productName within the confirmation message. We return the user's response. If the user clicks OK, the current route is deactivated, and the requested route is activated. If the user selects to cancel, the navigation is cancelled. If the product data is not changed, this method returns true, and the route is deactivated. Before we can use this guard, we need to register the service provider in an Angular module. Since this guard checks product data, we register the service in the product feature module. We import the guard using an import statement, then register the provider in the list of providers. The last step is to tie the guard to the appropriate route. We'll add it to the parent Product Edit route. That way, it will guard both child routes. Let's check it out in the browser. We log in, navigate to the edit page, make a change, and select Product List. We see our confirmation dialog with the name of the product. The user can then click OK to continue to navigate away or click Cancel to return to the edit page. Cool! Let's click OK, and we lose our changes. Now let's try it again. Click Edit, make a change, click Save to save our changes, and we again see the dialog. That's not right. We see this dialog even if we save because we are not resetting our current product and original product after the save. Let's do that now to finish this implementation. In the product-edit.component, let's add a reset method that resets the data after completing an operation. Here we clear our validation structure, currentProduct, and originalProduct. We then call this method onSaveComplete. Let's try it one more time. Log in, select to edit a product, make an edit, and select Product List to ensure our confirmation message still works. Click Cancel, then save changes. We are then returned to the Product List page without a confirmation dialog. And our change was saved. It works! Let's finish up this module with some checklists we can use when working with route guards.
Checklists and Summary
To build a route guard, build a service, implement the guard type, CanActivate in this example, and create the associated method. Like every other type of Angular service, register the guard service provider. Guards must be registered in a module, not a component. Lastly, add the guard to the desired route in the route configuration. In this module, we began with an overview of route guards. We build a canActivate guard and saw how to share data with a guard using a service. We briefly covered canActivateChild guards and built a canDeactivate guard. We added an authorization guard to the parent product route to protect all of the children and ensure the user logged in before accessing any product data. We also added a canDeactivate guard to the Product Edit parent route to ask the user for confirmation before navigating away with unsaved changes. Our route hierarchy is now complete. Up next, lazy loading.
Lazy Loading
Introduction
First impressions matter. We want to minimize the time between the user's request for our application and the display of its first template. Welcome back to Angular Routing from Pluralsight. My name is Deborah Kurata, and in this module, we look at asynchronous routing, which loads feature modules on request, also known as lazy loading. When we think of lazy, we often think slow, but like this guy, our routes can be lazy and fast. We use lazy loading to speed up our application startup time, and we use preloading to quickly launch our lazy loaded routes. As with any web page, when a user accesses our application, a request is sent from the user's browser to the application's URL. That URL normally maps to a web server somewhere that serves up the application's index.html file in response. The index.html file provides details on the other files such as JavaScript and CSS files that our application needs. All of those required files are also downloaded. Angular then compiles the downloaded files and loads the root app component. After this process is complete, Angular displays our app.component's template. Depending on the size of the application and the user's download speed, the startup process could take a noticeable amount of time. Lazy loading speeds up our startup time by splitting the application into multiple bundles and loading some of them on demand. So when the index.html file comes down, only some of the required files are downloaded. That's less to download and less for Angular to compile before displaying that first template improving the startup time of our application. When the user navigates to a route in a lazy loaded module, then the module is downloaded and compiled. This is especially useful for features that are only accessed occasionally or only by specific users. The application is not weighed down with the code for these features until they are actually needed. Note that there are several other techniques we can use to improve the startup time for our application. We can minify the code and consider AOT, the ahead-of-time compiler. See the Angular 2: Fundamentals module on deployment for more information. Our focus in this module is lazy loading our routes. In this module, we begin with the steps required before we can use lazy loading. Then we lazy load a feature module and build a canLoad route guard. We examine how to preload feature areas in the background and then define our own custom module loading strategy. In our sample application, we'll set up our product routes to route asynchronously by lazy loading our product.module. Let's get started.
Preparing for Lazy Loading
Before we can lazy load a module and achieve asynchronous routing, the feature area to lazy load must meet a few requirements. The feature area must be defined in its own feature module. That's because lazy loading loads all of the components declared in one specific Angular module. Another requirement for lazy loading is that the routes are grouped as children of a single parent route. That's because lazy loading is configured on the parent route path. The module for that route path is then loaded asynchronously. Lastly, the feature module must not be imported in any other Angular module. That's because if a module is referenced by any other module within the application, Angular will download and compile it completely defeating the purpose of lazy loading. Let's start by viewing our application as it is now in the browser. Open the developer tools and click on the Network tab. Now let's see what happens when we refresh the application. All of the files required for our application and for Angular to compile and execute our application are all downloaded. Let's filter to just our components. Type component in the filter box, and we see only the component files that were downloaded for our application. Notice that all of our components are downloaded, including all of the product feature components. If we configure lazy loading of our product.module, we won't see any of our product feature components downloaded when we first access the application. Looking at the code, our product features are all declared within our product.module. So our first prerequisite is met. Our product paths are all children of a single products parent route. That meets our second prerequisite. However, our app.module contains a reference to our ProductModule. We need to remove that or the ProductModule will be downloaded with our other application files. We delete it here and delete the import statement for it here. Now we've met our third and final prerequisite. If we try to access a product feature at this point, we'll get our 404 Not Found page. That's because we are no longer loading the ProductModule so our application has no information about our product routes. Keep these requirements in mind as you lazy load feature areas of your application. With that, let's see how to lazy load a feature module.
Lazy Loading
To set up lazy loading, we configure a route to load a feature module asynchronously. If we follow the prerequisite requirements for lazy loading as defined in the last clip, then implementing lazy loading for a particular feature simply involves configuring this single parent route to asynchronously load the feature module. Here is the definition of the products parent route. We load the product feature module asynchronously using loadChildren. Note that this is different from setting up child routes with the children property. The loadChildren property is specifically for lazy loading. The value of this property is a bit different. We set the loadChildren property to a string containing the feature module file path, a hash symbol, and the name of the feature module class. The file path here is relative to the location of the index.html file. The loadChildren property tells the router to fetch the bundle containing this module only when the user navigates to a products route. It compiles this module, then merges the route configuration defined in this module with the application's route configuration. Lastly, it activates the requested component and displays that component's template. Believe it or not, this is all there is to configuring lazy loading. Let's give it a try. We'll configure the route in the app-routing.module. We'll put it after the Welcome route. It doesn't really matter where we put it as long as it is before the wildcard route path. Remember, route order matters. We set the path property to products, our root path name. Then we set loadChildren to the file path of the product feature module. Then we add a hash symbol and our product module class name, which is ProductModule. That's all we need to do to set up lazy loading. But if we bring it up in the browser, we see that it doesn't work. Any guesses why? Here's a hint. Look at our products route configuration. Notice that our root path name is here as well. So the configuration for our ProductListComponent is now products/products. That's not what we want. Now that we have this parent route defined in the app-routing.module, we can remove it from the product.module. Notice the route guard. We'll add that to the parent route in a moment. For now, let's just delete this parent path. And don't forget to delete its closing square brace and curly brace. I'll reformat to fix the indentation. And we can delete the AuthGuard import statement since we aren't referencing it anymore. Checking it out in the browser, we again have our list of products. Yay! Did the changes we make really change how our files are downloaded? Let's see. Open up the developer console again, click Home, then refresh the page. Ensure you still have the filter set to component. Notice that none of our product components are downloaded. Clear the display and click Product List. Now our product components are downloaded and compiled, and the Product List route is activated. With asynchronous routing, we can delay downloading features until they are requested by the user. This can speed up the initial startup time of the application. Lazy loading is especially useful for features that are not often accessed or are only accessed by specific users. Before we move on, let's fix our authorization guard. Recall that we just deleted it when we deleted the extra parent route. So the user is no longer required to log in before viewing the Product List. If we still want to guard all of our product routes, which we do in this case, we can add the guard back to the parent route here. In the app-routing.module, add an import statement for the authorization guard. And add canActivate to the parent route specifying the AuthGuard. Trying it out again in the browser, attempting to access the Product List now brings up the Log In page. We have the guard working again. But now that we are using lazy loading, we have another route guard we can use--canLoad. How does that one work?
CanLoad Guard
When working with lazy loaded components, we can use a canLoad guard any time we want to check criteria before loading an asynchronous route. So the module won't even be downloaded unless the guard requirements are met. This is great for security because no one except authorized users will even be able to see the source code because it won't be downloaded. It is commonly used to prevent loading a route if a user can't access it. Let's give it a try. Let's look again at the application in the browser. Click Home and open the developer tools. Click refresh, and we load all of the basic application components. Clear the log and click Product List. Notice that our ProductModule is immediately lazy loaded, and then the Log In page appears. If the user has no login for this part of the application, we've just loaded the product features unnecessarily. It would be better if we required the login before even loading the module. That's the purpose of the canLoad guard. Recall from earlier in this course that we create a guard by building a service. We already have the authGuard service, which you can find in the user folder. Let's add the canLoad guard to this existing service. We add CanLoad to the import statement, implement it, and add its appropriate method. I'll paste the code for that method. The canLoad method has one parameter, the Route, which we'll add to the import statement. Unlike the canActivate, the canLoad method cannot access the ActivatedRouteSnapshot or RouterStateSnapshot because the module defining the route is not yet loaded. As with the other guards, canLoad can return a Boolean, observable, or promise. We return a simple Boolean. The code in this guard is similar to the canActivate guard. We call the local checkLoggedIn method, but since we don't have a RouterStateSnapshot to get the URL, we'll instead obtain the URL from the route.path property. Now that we've defined our guard, we add it to the route. In the app-routing.module, we'll replace the canActivate guard with the canLoad guard. Let's see how that changed our application flow. Refresh the page, and our application components are downloaded. Clear the log and click Product List. The Log In page appears, and ProductModule components are not yet loaded. So if we cancel the login, the files will not be downloaded. If we do log in using any username and password, then our product feature components are downloaded, and the product-list.component template is displayed. This is all well and good and kind of cool, but lazy loading does have one drawback. Depending on the user's download speed, it may cause a noticeable delay when navigating to the first product route that kicks off the download of the product feature components. Can we do better?
Preloading Feature Modules
With lazy loading as we have it now, when the user launches our application, the user waits while the browser downloads the imported modules, their components, their services, and any other declared files such as pipes or custom directives. Any lazy loaded module should not be imported so they won't be downloaded at this time. After everything is downloaded and compiled, the router completes its initial navigation, and the root app component template appears. At some future point, the user navigates to a feature configured for lazy loading. The user again waits as the associated module is downloaded. After it is downloaded, the navigation completes, and the routed component's template appears. If we are loading feature modules asynchronously, why do we have to wait for the user to request it? Why not preload feature modules behind the scenes while the user is interacting with the application? With preloading, also known as eager lazy loading, there is a bit less wait time. When the user launches our application, the user still must wait for the browser to download the imported modules. After everything is downloaded and compiled, the router completes its initial navigation, and the root app component's template appears. After the initial navigation, the router checks its configuration for any modules that can be preloaded. It then preloads modules behind the scenes based on the defined preload strategy. By the time the user navigates to a product feature route, for example, the ProductModule is already downloaded and ready to go. And the routed component's template immediately appears. No additional waiting. Preloading makes sense if we know that the module features will most likely be used. It is less useful for modules that are seldom used or only accessible to specific users, such as an application admin feature. For these modules, no preloading may be best. How modules are preloaded is based on the preload strategy. What does that mean? The router offers three preloading strategies--no preloading, which is the default, lazy loaded feature areas are loaded on demand when the user navigates to a route configured for lazy loading, preload all, which preloads all of the lazy loaded feature modules, or custom, which allows us to define our own preload strategy for finer control over which modules preload when. We've already seen the no preloading strategy when we configured lazy loading for our product module. Let's look at preload all next. To enable preloading, we pass a preloadingStrategy into the forRoot method as a second argument after the route configuration array. We pass in an object with any extra options we want to set. For the preload all strategy, specify a type of PreloadAllModules, and we need to add that to the import statement. This tells the router after the initial application startup to immediately load all lazy loaded routes, which are any routes with a loadChildren property. Note that this is defined for the forRoot method, which means this strategy is set for all routes. Let's give this a try.
Preloading Feature Modules: Demo
In the app-routing.module, first, we'll add PreloadAllModules to the import statement. Then we'll add the ExtraOptions object as the second parameter to our RouterModule.forRoot method. If you still have enableTracing set here, just add it to the options object. We set preloadingStrategy to PreloadAllModules. Let's check it out in the browser. We have our developer tools open so we can view the network traffic, and we still have the filter set to component so we can focus on our component files. Click Home and clear the log. Then refresh the browser. We again see that all of our imported modules are downloaded. We see no product component routes. We're off to a good start. But shouldn't the router be preloading our ProductModule by now? Nope. Something is blocking it. That something is the canLoad route guard. Surprisingly, the canLoad route guard blocks any preloading. We just added the canLoad route guard to our products route. Bummer! But if we think about this more, it does make sense. The canLoad must execute before the module is downloaded. When we specify preloading, the module can be downloaded behind the scenes at any point in time. Imagine if the canLoad didn't block preloading. The user could launch the application, the Welcome page appears, and the router then decides to preload our ProductModule and execute our canLoad guard. They aren't logged in yet, so the canLoad guard routes to the Log In page. The user was simply enjoying the beautiful artwork on the Welcome page and suddenly out of nowhere, the Log In page displays. That could be a bit surprising. It is because of these types of issues that the canLoad guard simply blocks preloading. Preloading does work with any of the other route guards. Going back to the code, if we want preloading yet still want to limit access to our route, we can change the canLoad guard back to the canActivate guard. Let's see if we finally preload our module. Begin again by clearing the log and refreshing the page. We see the components and other files from our imported modules. And a moment later, our ProductModule files are downloaded. Yay! So without the preloading strategy, none of our lazy loaded modules are preloaded in the background. With the preloading strategy, all of our lazy loaded modules are preloaded in the background. But what if we want finer control of our preloading strategy? We can build our own. And it's actually easier than you might think.
Custom Preloading Strategy
So we've seen lazy loading on demand with no preloading, and preload all, which loads all of our lazy loaded modules behind the scenes. What if we want something more than an all or nothing approach? Let's examine how to define our own custom preload strategy. This allows us to only preload some of our lazy loaded modules and not others. For example, we may want to preload the most commonly used modules, then lazy load our admin features or less-commonly used features with no preloading. Like our route guards, we define a preload strategy by building an Angular service. We then register the service in an Angular module, and we set the preloading strategy property to our preloading strategy service. Our service is then called by the router any time it wants to preload some routes. We build a preload strategy service just like any other service. We define a class, specify the injectable decorator, and import what we need. Because we want this service to behave like a preload strategy, we implement PreloadingStrategy. Then we implement its required method, preload. The preload method has two parameters. The first parameter is the Route, which provides information about the current route. The second parameter is the Function that performs the preloading. In the preload method, we write the code that decides whether or not to call the load function and preload the lazy loaded module. Once our preloading strategy service is in place in an Angular module, we first add the preloading strategy service to the list of providers and then enable the strategy by setting the preload strategy routing option to our preload strategy service. Let's give this a try. Start by creating a new file. Since this is for an application-wide loading strategy, we'll add it to the app folder. We'll call the file selective-strategy.service.ts. I'll paste the code for this service, and we can talk through it. I named this class SelectiveStrategy. Since this is a preloading strategy, the class implements PreloadingStrategy. Here is the preload method. In the body of the function, we decide whether or not to preload a particular module. I'll paste the code, and we can talk through it. For our sample application, we use the route's data property to determine whether to preload a route. We discussed the route.data property earlier in this course but did not use it in our sample application until now. I called our new data element preload. We'll assume that the value is set to true if the route should be preloaded. So here in the method, we check for that element in the route's data property. If the route data property is set, and the route data property preload element is true, then we return the defined load function and the route is preloaded. Otherwise, we return null. But since this function returns an observable, we return an observable of null. That means that we need to import the observable of operator as well. Before we can use this service, we must register it with an Angular module. Since this service is for our application-wide loading strategy, we'll register it in our app-routing.module, import the service, and add it to the providers array. Now we are ready to use this strategy. Here is where we are using the PreloadAllModules strategy. We instead use our SelectiveStrategy. And since we are no longer using PreloadAllModules, we can remove its import statement. Now we just need to mark which modules we want to preload using the route's data property. We only have one lazy loaded route, so let's try out our new strategy there. I'll reformat the path for readability, we'll add our data property to the products route here, and set it to true. Our SelectiveStrategy reads this data element and preloads if the value is true. Now let's check it out in the browser. We set our preload property to true for our product routes so they should now preload. I have the developer tools open, the Network tab selected, and I'm filtering the log to file names containing component. Refresh the browser, and we see the main application routes load. Then our product routes. Now let's turn off preloading by setting the route's data property preload value to false and try again. Clear the log, refresh the page, and we see that our product routes are not preloaded. By building our own custom preload strategy, we control which routes are preloaded. In our sample application, we used a simple preload flag. Using a custom preload strategy is even more useful as we add more feature modules to our application. Let's finish up this module with some checklists you can use as you lazy load your own modules.
Checklists and Summary
Before we can lazy load a module and achieve asynchronous routing, the feature area must meet these requirements. The feature area must be defined in a feature module such as the ProductModule in our sample application. The feature area routes must be grouped under a single parent route such as the products route in our sample application. And the feature module must not be imported in any other Angular module. Configure lazy loading by adding the loadChildren property to the parent route. Be sure that this route is defined in a module that is loaded such as the app-routing.module. Set this property to the full path to the module, a hash, and the module's class name. By default, lazy loaded modules are loaded on demand when the user requests a lazy loaded route. To instead preload all lazy loaded modules, specify the PreloadAllModules loading strategy. For more fine control over which lazy loaded modules are preloaded, build a custom preloading strategy service. In this module, we looked at the prerequisites that our feature area must meet before we can lazy load it. We saw how quick and easy it is to lazy load the routes for a feature module. We build a canLoad guard and examined how to preload our lazy loaded routes. We then looked at how to build our own custom preloading strategy service. We've now set up our product routes to route asynchronously by lazy loading the product feature module. This improves the startup performance of our application because only the minimum required files are initially downloaded and compiled. Only one short module left.
Final Words
Introduction
As you have seen throughout this course, Angular routing can do so much more than just move the user between multiple views of an application. Welcome back to Angular Routing from Pluralsight. My name is Deborah Kurata, and the final words in this course include a recap of our journey and a few pointers to additional information. We've come a long way. Remember when we started?
Recapping Our Journey
We began at the beginning with routing basics. We configured our first set of routes, an empty default route, a welcome route, and wildcard route that displays a Page Not Found message. Then we examined how to build feature modules and routed to the Log In component in a user feature module and the Product List component in a product feature module. We discussed route path naming strategies and now see the benefits of using a common root path name such as products in our example. And we found that route order matters. We can swiftly pass data as we route from one component to the next using route parameters. We looked at required, optional, and query parameters. We used required parameters to define which product to display in the Product Detail and Product Edit component templates, and we used query parameters to retain the Product List selections when navigating to the Product Detail and back again. For a nicer visual experience, we can retrieve the data required for display in a component's template before routing to that component using route resolvers. We learned how to build a resolver service to prefetch the data for our Product Detail and Product Edit components. No more partial page display as our component waits for its data. Using child routes, we defined a route hierarchy to better organize, encapsulate, and navigate through our application. We replaced the form in the Product Edit component template with a tabbed Edit page and defined a router-outlet for display of the child tab components. As the user clicks a tab, the associated component's template is displayed in that router-outlet. And we saw how to share the single instance of the product data from the parent's route resolver. Then came the biggest change to our route hierarchy. We grouped our child routes under a component-less parent route. This helps us better organize our routes and share route guards without requiring the parent to provide a router-outlet. The child component templates are displayed in the next higher level of the hierarchy, in this example, the primary router-outlet. Then we switched gears a bit and spent some time polishing our routes. We learned how to style the selected route, animate our route transitions, watch our routes, and react to routing events to display a spinner. Secondary routes make it easy to display multiple panels or panes on the page, each containing different content and supporting independent navigation. We discussed a Messages component in our secondary router-outlet named popup. We could also display other content in this secondary outlet such as a product summary when editing product information. We used route guards to ensure navigation to a route is permitted for security, authorization, or monitoring purposes, or to prevent the user from leaving a route without a confirmation. We added a CanActivate guard to the product's parent route to ensure that the user is logged in before accessing any product data. And we added a CanDeactivate guard to the Product Edit parent route. This guard notifies the user of any unsaved changes before navigating away. Lastly, we lazy loaded our product routes to improve the startup time of our application. And we built a custom preloading strategy service so we can control which lazy loaded modules are preloaded when. Along the way, this course provided a set of checklists containing steps and tips. Feel free to revisit and reference these checklists as you start incorporating these routing features into your own applications.
Learning More
If you'd like more information about Angular components, services, dependency injection, or observables, check out one of these courses. Angular 2: Getting Started, Angular 2: First Look, or Angular 2: Fundamentals. For more information on routing, this book is a comprehensive guide to the Angular router written by its designer.
Closing
Congratulations! You now know how to leverage more sophisticated routing features to support more real-world routing scenarios. Yay! Thanks for listening, and I'd love to hear about your experiences with routing.
Course author
Deborah Kurata
Deborah Kurata is a software developer, consultant, Pluralsight author, Google Developer Expert (GDE) and Microsoft Most Valuable Professional (MVP). Follow her on twitter: @deborahkurata
Course info
LevelIntermediate
Rating
(310)
My rating
Duration4h 47m
Released30 Mar 2017
Share course