The Router lifecycle hooks

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

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.

Note

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

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.

The CanDeactivate and OnDeactivate hooks

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.

The CanReuse and OnReuse hooks

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.

Advanced tips and tricks

Although we have discussed all that you need to start building complex applications with routing functionalities, there is still a big collection of advanced techniques you can use to take our application to the next level. In the upcoming sections, we will highlight just a few.

Redirecting to other routes

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.

Note

At the time of this writing, the rotue definitions in the new Router still do not implement support for the redirectTo property. Please check the online documentation for a more up-to-date status on the subject.

Tweaking the base path

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.

Finetuning our generated URLs with location strategies

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.

Note

Please note that any location-related token in the example is not imported from '@angular/router-deprecated' but from '@angular/common' instead.

Loading components asynchronously with AsyncRoutes

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.220.252.227