Just like components go through a set of different phases during their lifetime, a routing operation goes through different lifecycle stages. Each one is accessible from a different lifecycle hook which, just like components, can be handled by implementing a specific interface in the component subject of the routing action. The only exception for this is the earliest hook in the routing lifecycle, the CanActivate
hook, which takes the shape of a decorator annotation, since it is meant to be called before the component is even instantiated.
The CanActivate
hook, presented as a decorator annotating the component, is checked by the Router
right before it is instantiated. It will need its setup to be configured with a function that is intended to return a Boolean value (or a Promise
-typed Boolean value) indicating whether the component should be finally activated or not:
@CanActivate((next, prev) => boolean | Promise<boolean>)
The @CanActivate
decorator is therefore a function that expects another function as an argument, expecting the latter two ComponentInstruction
objects as parameters in its signature: the first argument represents the route we want to navigate to and the second argument represents the route we are coming from. These objects expose useful properties about the route we come from and the component we aim to instantiate: path, parameters, component type, and so on.
This hook represents a good point in the overall component's lifecycle to implement behaviors such as session validation, allowing us to protect areas of our application. Unfortunately the CanActivate
hook does not natively support dependency injection, which makes harder to introduce advanced business logic prior to activate a route. The next chapters will describe workarounds for scenarios such as user authentication.
In the following example, we password-protect the form so that it won't be instantiated should the user enters the wrong passphrase. First, open the TaskEditorComponent
module file and import all that we will need for our first experiment, along with all the symbols required for implementing the interfaces for the routing lifecycle hooks we will see throughout this chapter. Then, proceed to apply the CanActivate
decorator to the component class:
app/tasks/task-editor.component.ts
import { Component } from '@angular/core'; import { ROUTER_DIRECTIVES, CanActivate, ComponentInstruction, OnActivate, CanDeactivate, OnDeactivate } from '@angular/router-deprecated'; @Component({ selector: 'pomodoro-tasks-editor', directives: [ROUTER_DIRECTIVES], templateUrl: 'app/tasks/task-editor.component.html' }) @CanActivate(( next: ComponentInstruction, prev: ComponentInstruction): boolean => { let passPhrase = prompt('Say the magic words'); return (passPhrase === 'open sesame'); } ) export default class TaskEditorComponent { ...
As you can see, we are populating the @CanActivate
decorator with an arrow function declaring two ComponentInstruction
typed arguments (which are not actually required for our example, although they have been included here for instructional purposes). The arrow function returns a Boolean value depending on whether the user types the correct case-sensitive passphrase. We would advise you to inspect the next and previous parameters in the console to get yourself more acquainted with the information these two useful objects provide.
By the way, did you notice that we declared the ROUTER_DIRECTIVES
token in the directives property? The routing directives are not required for our overview of the different routing lifecycle hooks, but now we are tweaking this component and will keep updating it to test drive the different lifecycle hooks. Let's introduce a convenient back button, leveraging the Cancel button already present in the component template:
app/tasks/task-editor.component.html
<form class="container"> ... <p> <input type="submit" class="btn btn-success" value="Save"> <a [routerLink]="['TasksComponent']" class="btn btn-danger"> Cancel </a> </p> </form>
The OnActivate
hook allows us to perform custom actions once the route navigation to the component has been successfully accomplished. We can easily handle it by implementing a simple interface. These custom actions can even encompass asynchronous operations, in which case, we just need to return a Promise
from the interface function. If so, the route will only change once the promised has been resolved.
Let's see an actual example where we will introduce a new functionality by changing the title of our form page. To do so, we will keep working on the TaskEditorComponent
module to bring support for the OnActivate
hook interface and the Title
class whose API exposes utility methods (https://angular.io/docs/ts/latest/api/platform/browser/Title-class.html) to set or get the page title while executing applications in web browsers. Let's import the Title
symbol and declare it as a provider in the component to make it available for the injector (you can also inject it earlier at the top root component should you wish to interact with this object in other components):
app/tasks/task-editor.component.ts
import { Component } from '@angular/core'; import { ROUTER_DIRECTIVES, CanActivate, ComponentInstruction, OnActivate, CanDeactivate, OnDeactivate } from '@angular/router-deprecated'; import { Title } from '@angular/platform-browser'; @Component({ selector: 'pomodoro-tasks-editor', directives: [ROUTER_DIRECTIVES], providers: [Title], templateUrl: 'app/tasks/task-editor.component.html' })
Now, let's implement the interface with its required routerOnActivate
method. As a rule of thumb, all router lifecycle hooks are named after the hook name prefixed by router in lowercase:
app/tasks/task-editor.component.ts
export default class TaskEditorComponent implements OnActivate { constructor(private title: Title) {} routerOnActivate( next: ComponentInstruction, prev: ComponentInstruction): void { this.title.setTitle('Welcome to the Task Form!'); } }
Please note how we inject the Title
type in the class through its constructor and how we later on execute it when the router activates the component once the navigation has finished. Save your changes and reload the application. You will notice how the browser title changes once we successfully access the component after passing the CanActivate
and OnActivate
stages.
Just like we can filter if the component we are navigating to can be activated, we can apply the same logic when the user is about to leave the current component towards another one located elsewhere in our application. As we saw in case of the CanActivate
hook, we must return a Boolean or a Promise
resolving to a Boolean in order to allow the navigation to proceed or not. When the CanDeactivate
hook returns or is resolved to true, the OnDeactivate
hook is executed just like the OnActivate
hook after the navigation is accomplished.
In the following example, we will intercept the deactivation stages of the routing lifecycle to first interrogate the user whether he wants to leave the component page or not, and then we will restore the page title if so. For both operations, we will need to implement the CanDeactivate
and OnDeactivate
interfaces in our component. The code is as follows:
export default class TaskEditorComponent implements OnActivate, CanDeactivate, OnDeactivate { constructor(private title: Title) {} routerOnActivate(): void { this.title.setTitle('Welcome to the Task Form!'); } routerCanDeactivate(): Promise<boolean> | boolean { return confirm('Are you sure you want to leave?'); } routerOnDeactivate(): void { this.title.setTitle('My Angular 2 Pomodoro Timer'); } }
Please note that we have removed the (next: ComponentInstruction, prev: ComponentInstruction)
arguments from our hook implementations because they were of no use for these examples, but you can access a lot of interesting information through them in your own custom implementations.
Same as the CanActivate
hook, the CanDeactivate
hook must return a Boolean value or a Promise
resolved to a Boolean value in order to allow the routing flow to continue.
Last but not least, we can reuse the same instance of a component while browsing from one component to another component of the same type. This way, we can skip the process of destroying and instantiating a new component, saving resources on the go.
This requires us to ensure that the information contained in the parameters and stuff is properly handled to refresh the component UI or logic if required in the new incarnation of the same component.
The CanReuse
hook is responsible for all this, and it tells the Router
whether the component should be freshly instantiated or whether we should reuse the component in the future calls of the same route. The CanReuse
interface method should return a Boolean value or a Promise
resolving to a Boolean value (just like the CanActivate
or CanDeactivate
hooks do), which informs the Router
if it should reuse this component in the next call. If the CanReuse
implementation throws an error or is rejected from within the Promise
, the navigation will be cancelled.
On the other hand, if the CanReuse
interface returns or resolves to true, the OnReuse
hook will be executed instead of the OnActivate
hook should the latter exist already in the component. Therefore, use only one of these two whenever you implement this functionality.
Let's see all these in an actual example. When we schedule a task in the task list table and proceed to its timer, we can jump at any time to the generic timer accessible from the top nav bar, thereby loading another timer that is not bound to any task whatsoever. By doing so, we are actually jumping from one instance of the TimerWidgetComponent
component to another TimerWidgetComponent
component and the Angular router will destroy and instantiate the same component again. We can save the Router
from doing so by configuring the component to be reused. Open the TimerWidgetComponent
module and import the interfaces we will need for this, along with the symbols we were importing already from the Router
library:
app/timer/timer-widget.component.ts
import { Component, OnInit } from '@angular/core';
import { SettingsService, TaskService } from '../shared/shared';
import { RouteParams, CanReuse, OnReuse } from '@angular/router-deprecated';
Now, implement the CanReuse
and OnReuse
interfaces in the class by adding them to the implements declaration and then proceed to attach the following required interface methods to the class body:
routerCanReuse(): boolean { return true; } routerOnReuse(next: ComponentInstruction): void { // No implementation yet }
Now go to the tasks table, schedule any task, and go to its timer. Click on the Timer link in the top nav bar. You will see how the URL changes in the browser but nothing happens. We are reusing the same component as it is. While this saves memory resources, we need a fresh timer when performing this action. So, let's update the OnReuse
method accordingly, resetting the taskName
value and the Pomodoro itself:
routerOnReuse(): void { this.taskName = null; this.isPaused = false; this.resetPomodoro(); }
Reproduce now the same navigation journey and see what happens. Voila! New behavior but same old component.
Besides the route definition types we have seen already, there is another RouteDefinition
type named Redirect
that is not bound to any named Route
or component
, but will rather redirect to another existing Route
.
So far, we were serving the task list table from the root path, but what if we want to deliver this table from a path named /tasks
while ensuring that all the links pointing to the root are properly handled? Let's create a redirect route then. We will update the top root router configuration with a new path for the existing home path and a redirect path to it from the new home URL. The code is as follows:
app/app.component.ts
... @RouteConfig([{ path: '', name: 'Home', redirectTo: ['TasksComponent'] }, { path: 'tasks', name: 'TasksComponent', component: TasksComponent, useAsDefault: true }, { path: 'tasks/editor', name: 'TaskEditorComponent', component: TaskEditorComponent }, { path: 'timer/...', name: 'TimerComponent', component: TimerComponent }]) export default class AppComponent {}
The new redirecting route just needs a string path property and a redirectTo property declaring the array of named routes we want to redirect all the requests to.
When we began working on our application routing, we defined the base href
of our application at index.html
, so the Angular router is able to locate any resource to load apart from the components themselves. We obviously configured the root /
path, but what if, for some reason, we need to deploy our application with another base URL path while ensuring the views are still able to locate any required resource regardless of the URL they're being served under? Or perhaps we do not have access to the HEAD
tag in order to drop a <base href="/">
tag, because we are just building a redistributable component and do not know where this component will wind up later. Whatever the reason is, we can easily circumvent this issue by overriding the value of the APP_BASE_HREF
token, which represents the base href
to be used with our LocationStrategy
of choice.
Try it for yourself. Open the main.ts
file where we bootstrap the application, import the required tokens, and override the value of the aforementioned base href
application variable by a custom value:
app/main.ts
import 'rxjs/add/operator/map'; import { bootstrap } from '@angular/platform-browser-dynamic'; import AppComponent from './app.component'; import { provide } from '@angular/core'; import { APP_BASE_HREF } from '@angular/common'; bootstrap(AppComponent, [provide(APP_BASE_HREF, { useValue: '/my-apps/pomodoro-app' })]);
Reload the app and see the resulting URL in your browsers.
As you have seen, whenever the browser navigates to a path by command of a routerLink
or as a result of the execution of the navigate method of the Router
object, the URL showing up in the browser's location bar conforms to the standardized URLs we are used to seeing, but it is in fact a local URL. No call to the server is ever made. The fact that the URL shows off a natural structure is because of the pushState
method of the HTML5 history API that is executed under the folds and allows the navigation to add and modify the browser history in a transparent fashion.
There are two main providers, both inherited from the LocationStrategy
type, for representing and parsing state from the browser's URL:
PathLocationStrategy
: This is the strategy used by default by the location service, honoring the HTML5 pushState
mode, yielding clean URLs with no hash-banged fragments (example.com/foo/bar/baz
).HashLocationStrategy
: This strategy makes use of hash fragments to represent state in the browser URL (example.com/#foo/bar/baz
).Regardless of the strategy chosen by default by the Location
service, you can fallback to the old hashbang-based navigation by picking the HashLocationStrategy
as the LocationStrategy
type of choice.
In order to do so, go to main.ts
and tell the Angular global injector that, from now on, any time the injector requires binding the LocationStrategy
type for representing or parsing state (which internally picks PathLocationStrategy
), it should use not the default type, but use HashLocationStrategy
instead.
It just takes to override a default provider injection:
app/main.ts
import 'rxjs/add/operator/map'; import { bootstrap } from '@angular/platform-browser-dynamic'; import AppComponent from './app.component'; import { provide } from '@angular/core'; import { LocationStrategy, HashLocationStrategy } from '@angular/common'; bootstrap(AppComponent, [provide(LocationStrategy, { useClass: HashLocationStrategy })]);
Save your changes and reload the application, requesting a new route. You'll see the resulting URL in the browser.
As you have seen in this chapter, each route definition needs to be configured with a component
property that will inform the router about what to load into the router outlet when the browsers reach that URL. However, we might sometimes find ourselves in a scenario where this component needs to be fetched at runtime or is just the by-product of an asynchronous operation. In these cases, we need to apply a different strategy to pick up the component we need. Here's where a new type of router definition named AsyncRoute comes to the rescue. This specific kind of route exposes the same properties of the already familiar RouteDefinition
class we have been using along this chapter. It replaces the component
property with a loader
property that will be linked to a Promise
that resolves asynchronously to a component loaded on demand.
Let's see this with an actual example. In order to keep things simple, we will not be importing the component we want to load at runtime, rather we will return it from an asynchronous operation. Open the top root component module and replace the route pointing to TimerComponent
with this async route definition:
app/app.component.ts
... @RouteConfig([{ path: '', name: 'Home', redirectTo: ['TasksComponent'] }, { path: 'tasks', name: 'TasksComponent', component: TasksComponent, useAsDefault: true }, { path: 'tasks/editor', name: 'TaskEditorComponent', component: TaskEditorComponent }, { path: '/timer/...', name: 'TimerComponent', loader: () => { return new Promise(resolve => { setTimeout(() => resolve(TimerComponent), 1000); }); } } ]) export default class AppComponent {}
The next time we attempt to load any route belonging to the timer branch (either the generic timer accessible from the nav bar or any task-specific timer), we will have to wait until the Promise
resolves to the component we need. Obviously, the goal of this example is not to teach how to make things load slower, but to provide a simple example of loading a component asynchronously.