Search

#### Docs

- Guide
  - Overview
  - Setup
  - Learning Angular
  - Architecture
  - Displaying Data
  - User Input
  - Forms
  - Dependency Injection
  - Template Syntax
- Tutorial
  - Introduction
  - 0. The Starter App
  - 1. The Hero Editor
  - 2. Master/Detail
  - 3. Multiple Components
  - 4. Services
  - 5. Routing
  - 6. HTTP
- Advanced
  - Attribute Directives
  - Component Styles
  - Deployment
  - Hierarchical Injectors
  - HTTP Client
  - Lifecycle Hooks
  - Pipes
  - Router
  - Security
  - Structural Directives
  - Testing

#### Components

- Components Gallery

#### Resources

- Cheat Sheet
- Glossary
- FAQ
- Tools
- Community and Support

#### Reference

- Videos
- API Documentation

new_releases We're currently in the progress of migrating this site from Angular version 5.3.1 to 7.0.0. Package versions might be out-of-date or inconsistent! Visit the repo for more info.

# Router Lifecycle Hooks (DRAFT) 6.0

If you notice any issues with this page, please report them.

<?code-excerpt path-base=”examples/ng/doc/router”?>

Milestone

At the moment, any user can navigate anywhere in the app anytime. That’s not always the right thing to do.

- Perhaps the user is not authorized to navigate to the target component.
- Maybe the user must login (authenticate) first.
- Maybe you should fetch some data before you display the target component.
- You might want to save pending changes before leaving a component.
- You might ask the user if it’s OK to discard pending changes rather than save them.

You can provide router lifecycle hooks to handle these scenarios.

A router lifecycle hook is a boolean function. The returned boolean value affects the router’s navigation behavior either canceling navigation (and staying on the current view) or allowing navigation to proceed.

Lifecycle hooks can also tell the router to navigate to a different component.

The router lifecycle hooks supplement, and are distinct from, component lifecycle hooks.

You’re already familiar with the OnActivate hook that was introduced in an earlier milestone. You’ll learn about other hooks below.

## Handling unsaved crisis name changes

The app currently accepts every change to a hero name immediately without delay or validation. More often, you’d accumulate a user’s changes so that the app can, for example:

- Validate across fields
- Validate on the server
- Hold changes in a pending state until the user confirms them as a group or cancels and reverts all changes

What should be done with unsaved changes when the user navigates away? Just ignoring changes offers a bad user experience.

Let the user decide what to do. If the user cancels navigation, then the app can stay put and allow more changes. If the user approves, then the app can save.

The app might still delay navigation until the save succeeds. If you let the user move to the next screen immediately and the save failed, you would have lost the context of the error.

The app can’t block while waiting for the server — that’s not possible in a browser. The app needs to stop the navigation while waiting, asynchronously, for the server to return with its answer.

For this, you need the CanNavigate hook.

### Add Save and Cancel buttons

The sample app doesn’t talk to a server. Fortunately, you have another way to demonstrate an asynchronous router hook.

Before defining a hook, you’ll need to make the following edits to the crisis component so that user changes to the crisis name are temporary until saved (in contrast, `HeroComponent` name changes will remain immediate).

Update `CrisisComponent`:

- Add a string `name` field to hold the crisis name while it is being edited.

- Initialize `name` in the `onActivate()` hook.

  <?code-excerpt “lib/src/crisis/crisis_component.dart (onActivate)” remove=”/·|log/” replace=”/name = .*?name;/[!$&!]/g” title?>

        void onActivate(_, RouterState current) async {
          final id = getId(current.parameters);
          if (id == null) return null;
          crisis = await (_crisisService.get(id));
          [!name = crisis?.name;!]
        }

- Add a `save()` method which assigns `name` to the selected crisis before navigating back to the crisis list.

  <?code-excerpt “lib/src/crisis/crisis_component.dart (save)” remove=”log” title?>

        Future<void> save() async {
          crisis?.name = name;
          goBack();
        }

Update the crisis component template by doing the following:

- Renaming the Back button to Cancel.
- Adding a Save button with a click event binding to the `save()` method.
- Changing the `ngModel` expression from `crisis.name` to `name`.

<?code-excerpt “lib/src/crisis/crisis_component.html (save and cancel)” title?>

      <div>
        <label>name: </label>
        <input [(ngModel)]="name" placeholder="name" />
      </div>
      <button (click)="goBack()">Cancel</button>
      <button (click)="save()">Save</button>

open_in_browser Refresh the browser and try out the new crisis details save and cancel features.

## CanNavigate hook to handle unsaved changes

What if the user tries to navigate away without saving or canceling? The user could push the browser back button or click the heroes link. Both actions trigger a navigation. Should the app save or cancel automatically?

It currently does neither. Instead you’ll ask the user to make that choice explicitly in a confirmation dialog. To implement this functionality you’ll need a dialog service; the following simple implementation will do.

<?code-excerpt “lib/src/crisis/dialog_service.dart” title?>

      import 'dart:async';
      import 'dart:html';

      class DialogService {
        Future<bool> confirm(String message) async =>
            window.confirm(message ?? 'Ok?');
      }

Add `DialogService` to the `CrisisListComponent` providers list so that the service is available to all components in the crises component subtree:

<?code-excerpt “lib/src/crisis/crisis_list_component.dart (providers)” replace=”/\S+DialogService.*/[!$&!]/g” title?>

      providers: [
        ClassProvider(CrisisService),
        [!ClassProvider(DialogService),!]
      ],

Next, implement a router CanNavigate lifecycle hook in `CrisisComponent`:

- Add the router `CanNavigate` interface to the class’s list of implemented interfaces.
- Add a private field and constructor argument to hold an injected instance of a `DialogService` — remember to import it.
- Add the following `canNavigate()` lifecycle hook:

<?code-excerpt “lib/src/crisis/crisis_component.dart (canNavigate)” remove=”/\blog\b/” title?>

      Future<bool> canNavigate() async {
        return crisis?.name == name ||
            await _dialogService.confirm('Discard changes?');
      }

Since the router calls the `canNavigate()` method when necessary, so you don’t need to worry about the different ways that the user can navigate away.

## CanDeactivate hook

If you can decide whether it’s ok to navigate away from a component solely based on the component’s state, then use `CanNavigate`. Sometimes you need to know the next router state: for example, if the current and next route haven’t changed, but navigation was triggered for another reason such as an added query parameter. In such cases, use the CanDeactivate hook to selectively prevent deactivation.

The crisis component doesn’t need a `canDeactivate()` lifecycle method, but if it had one, the method signature would be like this:

<?code-excerpt “lib/src/crisis/crisis_component.dart (canDeactivate)” remove=”log(“ replace=”/return .*/…/g” title?>

      Future<bool> canDeactivate(RouterState current, RouterState next) async {
        ...
      }

The `canNavigate()` method is more efficient to use because the router doesn’t need to precompute the (potential) next router state. Favor using `CanNavigate` over `CanDeactivate` when possible.

## OnActivate and OnDeactivate interfaces

Each route is handled by a component instance. Generally, when the router processes a route change request, it performs the following actions:

- It deactivates and then destroys the component instance handling the current route, if any.
- It instantiates the component class registered to handle the new route instruction, and then activates the new instance.

Lifecycle hooks exist for both component activation and deactivation. You’ve been using the OnActivate hook in the hero list and the crisis list components.

Add OnDeactivate to the list of classes implemented by `CrisisComponent`. Add the following `onDeactivate()` hook, and add print statements to the contructor and activate hook as shown:

<?code-excerpt “lib/src/crisis/crisis_component.dart (excerpt)” region=”OnActivate-and-OnDeactivate” replace=”/log(‘/print(‘CrisisComponent: /g; /(canNavigate):.*(?=’)/$1/g” title?>

      CrisisComponent(this._crisisService, this._router, this._dialogService) {
        print('CrisisComponent: created');
      }
      // ···
      void onActivate(_, RouterState current) async {
        print('CrisisComponent: onActivate: ${_?.toUrl()} -> ${current?.toUrl()}');
        // ···
      }
      // ···
      void onDeactivate(RouterState current, _) {
        print('CrisisComponent: onDeactivate: ${current?.toUrl()} -> ${_?.toUrl()}');
      }
      // ···
      Future<bool> canNavigate() async {
        print('CrisisComponent: canNavigate');
        return crisis?.name == name ||
            await _dialogService.confirm('Discard changes?');
      }

open_in_browser Refresh the browser and open the JavaScript console. Click the crises tab and select each of the first three crises, one at a time. Finally, select Cancel in the crisis detail view. You should see the following sequence of messages:

    CrisisComponent: created
    CrisisComponent: onActivate: crises -> crises/1
    CrisisComponent: canNavigate
    CrisisComponent: onDeactivate: crises/1 -> crises/2
    CrisisComponent: created
    CrisisComponent: onActivate: crises/1 -> crises/2
    CrisisComponent: canNavigate
    CrisisComponent: onDeactivate: crises/2 -> crises/3
    CrisisComponent: created
    CrisisComponent: onActivate: crises/2 -> crises/3

When a component implements the OnDeactivate interface, the router calls the component’s `onDeactivate()` method before the instance is destroyed. This gives component instances an opportunity to perform tasks like cleanup and resource deallocation before being deactivated.

Given the nature of a crisis detail component’s responsibilities, it seems wasteful to create a new instance each time. A single instance could handle all crisis detail route instructions.

To tell the router that a component instance might be reusable, use the CanReuse lifecycle hook.

## CanReuse interface

Add `CanReuse` as a mixin so that it implements the CanReuse interface, and uses the `canReuse()` hook implementation that always returns true.

<?code-excerpt “lib/src/crisis/crisis_component.dart (CanReuse)” replace=”/extends \w+|with \w+/[!$&!]/g; /, InstanceLogger//g; /CanDeactivate, //g” title?>

      class CrisisComponent [!extends Object!]
          [!with CanReuse!]
          implements CanNavigate, OnActivate, OnDeactivate {
        // ···
      }

open_in_browser Refresh the browser and click the crises tab, then select a few crises. You’ll notice that crisis components still get created after each crisis selection. This is because a component is reusable only if its parent is reusable.

Add `CanReuse` as a mixin to the crisis list component:

<?code-excerpt “lib/src/crisis/crisis_list_component.dart (CanReuse)” replace=”/extends \w+|with \w+/[!$&!]/g; /, InstanceLogger//g” title?>

      class CrisisListComponent [!extends Object!]
          [!with CanReuse!]
          implements OnActivate, OnDeactivate {
        // ···
      }

open_in_browser Refresh the browser and select a few crises. The view details should now reflect the selected crisis. Also notice how the `OnDeactivate` hook is called only if you navigate away from a crisis detail view — such as by clicking crisis detail view Cancel button, or clicking the Heroes tab.

## App code

After these changes, the folder structure looks like this:

- router_example
  - lib
    - app_component.dart
    - src
      - crisis
        - crises_component.{css,dart,html}
        - crisis.dart
        - crisis_list_component.dart
        - crisis_component.{css,dart,html}
        - crisis_service.dart
        - dialog_service.dart (new)
        - mock_crises.dart
      - hero
        - hero.dart
        - hero_component.{css,dart,html}
        - hero_service.dart
        - hero_list_component.{css,dart,html}
        - mock_heroes.dart
  - web
    - index.html
    - main.dart
    - styles.css

Here are key files for this version of the sample app: