Chapter Summary
Problem | Solution | Listing |
---|---|---|
Delaying navigation until a task is complete | Use a route resolver | 1–7 |
Preventing route activation | Use an activation guard | 8–14 |
Preventing the user from navigating away from the current conte`nt | Use a deactivation guard | 15–19 |
Deferring loading a feature module until it is required | Create a dynamically loaded module | 20–25 |
Controlling when a dynamically loaded module is used | Use a loading guard | 26–28 |
Using routing to manage multiple router outlets | Use named outlets in the same template | 29–34 |
Preparing the Example Project
For this chapter, I will continue using the exampleApp project that was created in Chapter 22 and has been modified in each subsequent chapter. To prepare for this chapter, I have simplified the routing configuration, as shown in Listing 27-1.
You can download the example project for this chapter—and for all the other chapters in this book—from https://github.com/Apress/pro-angular-9. See Chapter 1 for how to get help if you have problems running the examples.
Simplifying the Routes in the app.routing.ts File in the src/app Folder
Guarding Routes
The Routes Properties for Guards
Name | Description |
---|---|
resolve | This property is used to specify guards that will delay route activation until some operation has been completed, such as loading data from a server. |
canActivate | This property is used to specify the guards that will be used to determine whether a route can be activated. |
canActivateChild | This property is used to specify the guards that will be used to determine whether a child route can be activated. |
canDeactivate | This property is used to specify the guards that will be used to determine whether a route can be deactivated. |
canLoad | This property is used to guard routes that load feature modules dynamically, as described in the “Loading Feature Modules Dynamically” section. |
Delaying Navigation with a Resolver
A common reason for guarding routes is to ensure that the application has received the data that it requires before a route is activated. The example application loads data from the RESTful web service asynchronously, which means there can be a delay between the moment at which the browser is asked to send the HTTP request and the moment at which the response is received and the data is processed. You may not have noticed this delay as you followed the examples because the browser and the web service are running on the same machine. In a deployed application, there is a much greater prospect of there being a delay, caused by network congestion, a high server load, or a dozen other factors.
Adding a Delay in the rest.datasource.ts File in the src/app/model Folder
The delay is added using the Reactive Extensions delay method and is applied to create a five-second delay, which is long enough to create a noticeable pause without being too painful to wait for every time the application is reloaded. To change the delay, increase or decrease the argument for the delay method, which is expressed in milliseconds.
The delay is applied to all HTTP requests, which means that if you create, edit, or delete a product, the change you have made will not be reflected in the product table for five seconds.
Creating a Resolver Service
The Contents of the model.resolver.ts File in the src/app/model Folder
Resolvers are classes that define a resolve method that accepts two arguments. The first argument is an ActivatedRouteSnapshot object, which describes the route that is being navigated to using the properties described in Chapter 25. The second argument is a RouterStateSnapshot object, which describes the current route through a single property called url. These arguments can be used to adapt the resolver to the navigation that is about to be performed, although neither is required by the resolver in the listing, which uses the same behavior regardless of the routes that are being navigated to and from.
All of the guards described in this chapter can implement interfaces defined in the @angular/router module. For example, resolvers can implement an interface called Resolve. These interfaces are optional, and I have not used them in this chapter.
The Result Types Allowed by the resolve Method
Result Type | Description |
---|---|
Observable<any> | The browser will activate the new route when the Observer emits an event. |
Promise<any> | The browser will activate the new route when the Promise resolves. |
Any other result | The browser will activate the new route as soon as the method produces a result. |
The Observable and Promise results are useful when dealing with asynchronous operations, such as requesting data using an HTTP request. Angular waits until the asynchronous operation is complete before activating the new route. Any other result is interpreted as the result of a synchronous operation, and Angular will activate the new route immediately.
The resolver in Listing 27-3 uses its constructor to receive Model and RestDataSource objects via dependency injection. When the resolve method is called, it checks the number of objects in the data model to determine whether the HTTP request to the RESTful web service has completed. If there are no objects in the data model, the resolve method returns the Observable from the RestDataSource.getData method, which will emit an event when the HTTP request completes. Angular will subscribe to the Observable and delay activating the new route until it emits an event. The resolve method returns null if there are objects in the model, and since this is neither an Observable nor a Promise, Angular will activate the new route immediately.
Combining asynchronous and synchronous results means that the resolver will delay navigation only until the HTTP request completes and the data model has been populated. This is important because the resolve method will be called every time the application tries to navigate to a route to which the resolver has been applied.
Registering the Resolver Service
Registering the Resolver in the model.module.ts File in the src/app/model Folder
Applying the Resolver
Applying a Resolver in the app.routing.ts File in the src/app Folder
The resolve property accepts a map object whose property values are the resolver classes that will be applied to the route. (The property names do not matter.) I want to apply the resolver to all the views that display the product table, so to avoid duplication, I created a route with the resolve property and used it as the parent for the existing child routes.
Displaying Placeholder Content
Displaying a Message in the model.resolver.ts File in the src/app/model Folder
Using a Resolver to Prevent URL Entry Problems
As I explained in Chapter 25, the development HTTP server will return the contents of the index.html file when it receives a request for a URL for which there is no corresponding file. Combined with the automatic browser reload functionality, it is easy to make a change in the project and have the browser reload a URL that causes the application to jump to a specific URL without going through the navigation steps that the application expects and that sets up the required state data.
Applying the Resolver to Other Routes in the app.routing.ts File in the src/app Folder
Applying the ModelResolver class to the routes that target FormComponent prevents the problem shown in Figure 27-4. There are other ways to solve this problem, including the approach that I used in Chapter 8 for the SportsStore application, which uses the route guard feature described in the “Preventing Route Activation” section of this chapter.
Preventing Navigation with Guards
Resolvers are used to delay navigation while the application performs some prerequisite work, such as loading data. The other guards that Angular provides are used to control whether navigation can occur at all, which can be useful when you want to alert the user to prevent potentially unwanted operations (such as abandoning data edits) or limit access to parts of the application unless the application is in a specific state (such as when a user has been authenticated).
Adding Responses in the message.model.ts File in the src/app/messages Folder
Presenting Responses in the message.component.html File in the src/app/core Folder
Preventing Route Activation
Guards can be used to prevent a route from being activated, helping to protect the application from entering an unwanted state or warning the user about the impact of performing an operation. To demonstrate, I am going to guard the /form/create URL to prevent the user from starting the process of creating a new product unless the user agrees to the application’s terms and conditions.
The Result Types Allowed by the canActivate Method
Result Type | Description |
---|---|
boolean | This type of result is useful when performing synchronous checks to see whether the route can be activated. A true result will activate the route, and a result of false will not, effectively ignoring the navigation request. |
Observable<boolean> | This type of result is useful when performing asynchronous checks to see whether the route can be activated. Angular will wait until the Observable emits a value, which will be used to determine whether the route is activated. When using this kind of result, it is important to terminate the Observable by calling the complete method; otherwise, Angular will just keep waiting. |
Promise<boolean> | This type of result is useful when performing asynchronous checks to see whether the route can be activated. Angular will wait until the Promise is resolved and activate the route if it yields true. If the Promise yields false, then the route will not be activated, effectively ignoring the navigation request. |
The Contents of the terms.guard.ts File in the src/app Folder
The canActivate method can return two different types of results. The first type is a boolean, which allows the guard to respond immediately for routes that it doesn’t need to protect, which in this case is any that lacks a parameter called mode whose value is create. If the URL matched by the route doesn’t contain this parameter, the canActivate method returns true, which tells Angular to activate the route. This is important because the edit and create features both rely on the same routes, and the guard should not interfere with edit operations.
The other type of result is a Promise<boolean>, which I have used instead of Observable<true> for variety. The Promise uses the modifications to the message service to solicit a response from the user, confirming they accept the (unspecified) terms and conditions. There are two possible responses from the user. If the user clicks the Yes button, then the Promise will resolve and yield true, which tells Angular to activate the route, displaying the form that is used to create a new product. The Promise will resolve and yield false if the user clicks the No button, which tells Angular to ignore the navigation request.
Registering the Guard as a Service in the app.module.ts File in the src/app Folder
Applying the Guard to a Route in the app.routing.ts File in the src/app Folder
Consolidating Child Route Guards
If you have a set of child routes, you can guard against their activation using a child route guard, which is a class that defines a method called canActivateChild. The guard is applied to the parent route in the application’s configuration, and the canActivateChild method is called whenever any of the child routes are about to be activated. The method receives the same ActivatedRouteSnapshot and RouterStateSnapshot objects as the other guards and can return the set of result types described in Table 27-4.
Guarding Child Routes in the app.routing.ts File in the src/app Folder
Implementing Child Route Guards in the terms.guard.ts File in the src/app Folder
The guard only protects the categories child route and will return true immediately for any other route. The guard prompts the user using the message service but does something different if the user clicks the No button. In addition to rejecting the active route, the guard navigates to a different URL using the Router service, which is received as a constructor argument. This is a common pattern for authentication when the user is redirected to a component that will solicit security credentials if a restricted operation is attempted. The example is simpler in this case, and the guard navigates to a sibling route that shows a different component. (You can see an example of using route guards for navigation in the SportsStore application in Chapter 9.)
Guards are applied only when the active route changes. So, for example, if you click the Count Categories button when the /table URL is active, then you will see the prompt, and clicking Yes will change the active route. But nothing will happen if you click the Count Categories button again because Angular doesn’t trigger a route change when the target route and the active route are the same.
Preventing Route Deactivation
When you start working with routes, you will tend to focus on the way that routes are activated to respond to navigation and present new content to the user. But equally important is route deactivation, which occurs when the application navigates away from a route.
Preparing for the Guard in the form.component.ts File in the src/app/core Folder
When the component begins editing, it creates a copy of the Product object that it gets from the data model and assigns it to the originalProduct property. This property will be used by the deactivation guard to see whether there are unsaved edits. To prevent the guard from interrupting save operations, the originalProduct property is set to the editing product object in the submitForm method before the navigation request.
Disabling Form Reset in the form.component.html File in the src/app/core Folder
The Contents of the unsaved.guard.ts File in the src/app/core Folder
Deactivation guards define a class called canDeactivate that receives three arguments: the component that is about to be deactivated and the ActivatedRouteSnapshot and RouteStateSnapshot objects. This guard checks to see whether there are unsaved edits in the component and prompts the user if there are. For variety, this guard uses an Observable<true>, implemented as a Subject<true> instead of a Promise<true>, to tell Angular whether it should activate the route, based on the response selected by the user.
Notice that I call the complete method on the Subject after calling the next method. Angular will wait indefinitely for the complete method to be called, effectively freezing the application.
Registering the Guard as a Service in the core.module.ts File in the src/app/core Folder
Applying the Guard in the app.routing.ts File in the src/app Folder
Loading Feature Modules Dynamically
Angular supports loading feature modules only when they are required, known as dynamic loading or lazy loading. This can be useful for functionality that is unlikely to be required by all users. In the sections that follow, I create a simple feature module and demonstrate how to configure the application so that Angular will load the module only when the application navigates to a specific URL.
Loading modules dynamically is a trade-off. The application will be smaller and faster to download for most users, improving their overall experience. But users who require the dynamically loaded features will have wait while Angular gets the module and its dependencies. The effect can be jarring because the user has no idea that some features have been loaded and others have not. When you create dynamically loaded modules, you are balancing improving the experience for some users against making it worse for others. Consider how your users fall into these groups and be careful not to degrade the experience of your most valuable and important customers.
Creating a Simple Feature Module
Dynamically loaded modules must contain only functionality that not all users require. I can’t use the existing modules because they provide the core functionality for the application, which means that I need a new module for this part of the chapter. I started by creating a folder called ondemand in the src/app folder. To give the new module a component, I added a file called ondemand.component.ts in the example/app/ondemand folder and added the code shown in Listing 27-20.
It is important not to create dependencies between other parts of the application and the classes in the dynamically loaded module so that the JavaScript module loader doesn’t try to load the module before it is required.
The Contents of the ondemand.component.ts File in the src/app/ondemand Folder
The ondemand.component.html File in the src/app/ondemand Folder
The template contains a message that will make it obvious when the component is selected and that contains a button element that will navigate back to the application’s root URL when clicked.
The Contents of the ondemand.module.ts File in the src/app/ondemand Folder
The module imports the CommonModule functionality, which is used instead of the browser-specific BrowserModule to access the built-in directives in feature modules that are loaded on-demand.
Loading the Module Dynamically
Defining Routes in the ondemand.module.ts File in the src/app/ondemand Folder
Routes in dynamically loaded modules are defined using the same properties as in the main part of the application and can use all the same features, including child components, guards, and redirections. The route defined in the listing matches the empty path and selects the OndemandComponent for display.
When I created the application-wide routing configuration, I used the RouterModule.forRoot method. This is the method that is used to set up the routes in the root module of the application. When creating dynamically loaded modules, the RouterModule.forChild method must be used; this method creates a routing configuration that is merged into the overall routing system when the module is loaded.
Creating a Route to Dynamically Load a Module
Creating an On-Demand Route in the app.routing.ts File in the src/app Folder
The loadChildren property is used to provide Angular with details of how the module should be loaded. The property is assigned a function that invokes import, passing in the path to the module. The result is a Promise whose then method is used to select the module after it has been imported. The function in the listing tells Angular to load the OndemandModule class from the ondemand/ondemand.module file.
Using a Dynamically Loaded Module
Adding Navigation in the table.component.html File in the src/app/core Folder
No special measures are required to target a route that loads a module, and the Load Module button in the listing uses the standard routerLink attribute to navigate to the URL specified by the route added in Listing 27-24.
Guarding Dynamic Modules
You can guard against dynamically loading modules to ensure that they are loaded only when the application is in a specific state or when the user has explicitly agreed to wait while Angular does the loading (this latter option is typically used only for administration functions, where the user can be expected to have some understanding of how the application is structured).
The Contents of the load.guard.ts File in the src/app Folder
Dynamic loading guards are classes that implement a method called canLoad, which is invoked when Angular needs to activate the route it is applied to and which is provided with a Route object that describes the route.
Registering the Guard as a Service in the app.module.ts File in the src/app Folder
Applying a Dynamic Loading Guard
Guarding the Route in the app.routing.ts File in the src/app Folder
Targeting Named Outlets
A template can contain more than one router-outlet element, which allows a single URL to select multiple components to be displayed to the user.
The Contents of the first.component.ts File in the src/app/ondemand Folder
The Contents of the second.component.ts File in the src/app/ondemand Folder
This component is almost identical to the one in Listing 27-29, differing only in the message that it displays through its inline template.
Creating Additional Outlet Elements
Adding Outlets in the ondemand.component.html File in the src/app/ondemand Folder
The new elements create three new outlets. There can be at most one router-outlet element without a name element, which is known as the primary outlet. This is because omitting the name attribute has the same effect as applying it with a value of primary. All the routing examples so far in this book have relied on the primary outlet to display components to the user.
All other router-outlet elements must have a name element with a unique name. The names I have used in the listing are left and right because the classes applied to the div elements that contain the outlets use CSS to position these two outlets side by side.
Targeting Outlets in the ondemand.module.ts File in the src/app/ondemand Folder
If you omit the outlet property, then Angular assumes that the route targets the primary outlet. I tend to include the outlet property on all routes to emphasize which routes match an outlet element.
When Angular activates the route, it looks for matches for each outlet. All three of the new outlets have routes that match the empty path, which allows Angular to present the components shown in the figure.
Navigating When Using Multiple Outlets
Setting Routes for Outlets in the ondemand.module.ts File in the src/app/ondemand Folder
Navigating to Outlets in the ondemand.component.html File in the src/app/ondemand Folder
Summary
In this chapter, I finished describing the Angular URL routing features and explaining how to guard routes to control when a route is activated, how to load modules only when they are needed, and how to use multiple outlet elements to display components to the user. In the next chapter, I show you how to apply animations to Angular applications.