Imperative Navigation and Route Parameters (DRAFT) 5.3


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

Milestone

You’ve seen how to navigate using the RouterLink directive, now you’ll learn how to

  • Organize the app into features, or feature groups
  • Navigate imperatively from one component to another
  • Pass required and optional information in route parameters

This example has capabilities very similar to the Tour of Heroes Tutorial Part 5, and you’ll be copying much of the code from there.

Here’s how the user will experience this version of the app:

App in action

A typical app has multiple features or feature groups, each dedicated to a particular business purpose.

While you could continue to add files to the lib/src folder, developers generally prefer to organize their apps so that most, if not all, files implementing a feature are grouped into a separate folder.

You are about to break up the app into different feature groups, each with its own concerns.

Heroes functionality

Follow these steps:

  1. Create a hero folder under lib/src — you’ll be adding files implementing hero management there.
  2. Copy the following files from toh-5 lib/src into the new hero folder, adjusting import paths as necessary:
    • hero.dart
    • hero_component.*, that is, the CSS, Dart and HTML files
    • hero_service.dart
    • hero_list_component.*, the CSS, Dart and HTML files
    • mock_heroes.dart
  3. In hero_list_component.html, rename the <ul> element class to items (from heroes).
  4. In app_component.dart, import and then add ClassProvider(HeroService) to the providers list so that the service is available everywhere in the app.
  5. In src/routes.dart, adjust the hero component import path because the new file is under heroes.
  6. Delete the old lib/src/hero_list_component.dart.

open_in_browser Refresh the browser and you should see heroes in the heroes list. You can select heroes, but not yet view hero details. You’ll address that next.

Hero routing requirements

The heroes feature has two interacting components, the hero list and the hero detail. The list view is self-sufficient; you navigate to it, it gets a list of heroes and displays them.

The detail view is different. It displays a particular hero. It can’t know which hero to show on its own. That information must come from outside.

When the user selects a hero from the list, the app should navigate to the detail view and show that hero. You tell the detail view which hero to display by including the selected hero’s ID in the route URL.

Hero detail route

Create a hero route path and route definition like you did in the Tour of Heroes Tutorial Part 5. Adding the following route path and getId() helper function:

lib/src/route_paths.dart (hero)

const idParam = 'id';

class RoutePaths {
  // ···
  static final hero = RoutePath(path: '${heroes.path}/:$idParam');
}

int getId(Map<String, String> parameters) {
  final id = parameters[idParam];
  return id == null ? null : int.tryParse(id);
}

After including an appropriate import for HeroComponent, add the following route definition:

lib/src/routes.dart (hero)

import 'hero/hero_component.template.dart' as hero_template;
// ···
class Routes {
  // ···
  static final hero = RouteDefinition(
    routePath: RoutePaths.hero,
    component: hero_template.HeroComponentNgFactory,
  );

  static final all = <RouteDefinition>[
    // ···
    hero,
    // ···
  ];
}

Route definition with a parameter

Notice that the hero path ends with :id (the result of interpolating idParam in the path '${heroes.path}/:$idParam'). That creates a slot in the path for a route parameter. In this case, the router will insert the ID of a hero into that slot.

If you tell the router to navigate to the detail component and display “Magneta” (having ID 15), you’d expect a hero ID to appear in the browser URL like this: localhost:8080/#/heroes/15.

If a user enters that URL into the browser address bar, the router should recognize the pattern and go to the same “Magneta” detail view.

Users won’t navigate to the hero component by clicking a link so you won’t be adding a new RouterLink anchor tag to the shell. Instead, when the user clicks a hero in the list, you’ll ask the router to navigate to the hero view for the selected hero.

Make the following changes to the hero list component template:

  • Drop “My” from the <h2> element in the template so it reads “Heroes”.
  • Drop the <div *ngIf="selectedHero != null">...</div> element.

lib/src/hero/hero_list_component.html

<h2>Heroes</h2>
<ul class="items">
  <li *ngFor="let hero of heroes"
    [class.selected]="hero === selected"
    (click)="onSelect(hero)">
    <span class="badge">{{hero.id}}</span> {{hero.name}}
  </li>
</ul>

The template has an *ngFor element that you’ve seen before. There’s a (click) event binding to the component’s onSelect() method to which you’ll add a call to gotoDetail().

But first, make gotoDetail() private by prefixing the name with an underscore, since it won’t be used in the template anymore:

lib/src/hero/hero_list_component.dart (_gotoDetail)

Future<NavigationResult> _gotoDetail() =>
    _router.navigate(_heroUrl(id));

The onSelect() method is currently defined as follows:

void onSelect(Hero hero) => selected = hero;

By selecting a hero in the hero list, the router will navigate away from the hero list view to a hero view. Because of this, you don’t need to record the selected hero in the hero list component. Just navigate to the selected hero’s view.

First parameterize _gotoDetail() with a hero ID, then update the onSelect() method as follows:

lib/src/hero/hero_list_component.dart (onSelect & _gotoDetail)

void onSelect(Hero hero) => _gotoDetail(hero.id);
// ···
Future<NavigationResult> _gotoDetail(int id) =>
    _router.navigate(_heroUrl(id));

open_in_browser Refresh the browser and select a hero. The app navigates to the hero view.

Use a route parameter to specify a required parameter within the route path, for example, when navigating to the detail for the hero with ID 15: /heroes/15.

You sometimes want to add optional information to a route request. For example, when returning to the heroes list from the hero detail view, it would be nice if the viewed hero was preselected in the list:

Selected hero

You can achieve this by passing an optional ID as a query parameter when navigating back to the hero list. You’ll address that next.

Location service back()

The hero detail’s Back button has an event binding to the goBack() method, which currently navigates backward one step in the browser’s history stack using the Location service:

void goBack() => _location.back();

Router navigation

You’ll be implementing goBack() using the router rather than the location service, so you can replace the Location field by Router _router, initialized in the constructor:

lib/src/hero/hero_component.dart (router)

final Router _router;

HeroComponent(this._heroService, this._router);

Use the router’s navigate() method like you did previously in HeroListComponent, but encode the hero ID as a query parameter instead:

lib/src/hero/hero_component.dart (goBack)

Future<NavigationResult> goBack() => _router.navigate(
    RoutePaths.heroes.toUrl(),
    NavigationParams(queryParameters: {idParam: '${hero.id}'}));

open_in_browser Refresh the browser, select a hero and then click the Back button to return to the heroes list. Notice that the URL now ends with a query parameter like this: /#/heroes?id=15.

The router can also encode parameters using the matrix URL notation, such as /heroes;id=15;foo=bar. You’ll see this later, once the crises feature is fully fleshed out.

Extracting query parameters

Despite the URL query parameter, the hero isn’t selected. Using the hero component as a model, make the following changes to the hero list component:

  • Implement OnActivate rather than OnNgInit.
  • Replace ngOnInit() by onActivate(), a router lifecycle hook. Read about other router lifecycle hooks in Milestone 5.
  • Fetch the hero ID from the router state queryParameters.

lib/src/hero/hero_list_component.dart (onActivate)

@override
void onActivate(_, RouterState current) async {
  await _getHeroes();
  selected = _select(current);
}

Hero _select(RouterState routerState) {
  final id = getId(routerState.queryParameters);
  return id == null
      ? null
      : heroes.firstWhere((e) => e.id == id, orElse: () => null);
}

open_in_browser Refresh the browser, select a hero and then click the Back button to return to the heroes list. The previously selected hero will be selected again. Try deep linking to another selected hero by visiting this URL: localhost:8080/#/hero?id=15.

App code

After these changes, the folder structure looks like this:

  • router_example
    • lib
      • app_component.dart
      • src
        • crisis_list_component.dart
        • hero
          • hero.dart
          • hero_component.{css,dart,html}
          • hero_service.dart
          • hero_list_component.{css,dart,html}
          • mock_heroes.dart
        • route_paths.dart
        • routes.dart
    • web
      • index.html
      • main.dart
      • styles.css

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

import 'package:angular/angular.dart'; import 'package:angular_router/angular_router.dart'; import 'src/routes.dart'; import 'src/hero/hero_service.dart'; @Component( selector: 'my-app', template: ''' <h1>Angular Router</h1> <nav> <a [routerLink]="RoutePaths.crises.toUrl()" [routerLinkActive]="'active-route'">Crisis Center</a> <a [routerLink]="RoutePaths.heroes.toUrl()" [routerLinkActive]="'active-route'">Heroes</a> </nav> <router-outlet [routes]="Routes.all"></router-outlet> ''', styles: ['.active-route {color: #039be5}'], directives: [routerDirectives], providers: [ClassProvider(HeroService)], exports: [RoutePaths, Routes], ) class AppComponent {} |import 'dart:async'; | |import 'package:angular/angular.dart'; |import 'package:angular_forms/angular_forms.dart'; |import 'package:angular_router/angular_router.dart'; | |import '../route_paths.dart'; |import 'hero.dart'; |import 'hero_service.dart'; | |@Component( | selector: 'my-hero', | templateUrl: 'hero_component.html', | styleUrls: ['hero_component.css'], | directives: [coreDirectives, formDirectives], |) |class HeroComponent implements OnActivate { | Hero hero; | final HeroService _heroService; | final Router _router; | | HeroComponent(this._heroService, this._router); | | @override | void onActivate(_, RouterState current) async { | final id = getId(current.parameters); | if (id != null) hero = await (_heroService.get(id)); | } | | Future<NavigationResult> goBack() => _router.navigate( | RoutePaths.heroes.toUrl(), | NavigationParams(queryParameters: {idParam: '${hero.id}'})); |} |import 'dart:async'; | |import 'package:angular/angular.dart'; |import 'package:angular_router/angular_router.dart'; | |import '../route_paths.dart'; |import 'hero.dart'; |import 'hero_service.dart'; | |@Component( | selector: 'my-heroes', | templateUrl: 'hero_list_component.html', | styleUrls: ['hero_list_component.css'], | directives: [coreDirectives], |) |class HeroListComponent implements OnActivate { | final HeroService _heroService; | final Router _router; | List<Hero> heroes; | Hero selected; | | HeroListComponent(this._heroService, this._router); | | Future<void> _getHeroes() async { | heroes = await _heroService.getAll(); | } | | @override | void onActivate(_, RouterState current) async { | await _getHeroes(); | selected = _select(current); | } | | Hero _select(RouterState routerState) { | final id = getId(routerState.queryParameters); | return id == null | ? null | : heroes.firstWhere((e) => e.id == id, orElse: () => null); | } | | void onSelect(Hero hero) => _gotoDetail(hero.id); | | String _heroUrl(int id) => | RoutePaths.hero.toUrl(parameters: {idParam: '$id'}); | | Future<NavigationResult> _gotoDetail(int id) => | _router.navigate(_heroUrl(id)); |}