Child & Relative Routes (DRAFT) 6.0


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

Milestone

In this milestone you’ll create a routing component, independent from the app root, that will manage routing for the crisis center. The app root will play the role of parent rooting component.

Crisis center organization

You’ll organize the crisis center to conform to the recommended pattern for Angular apps:

  • All crisis center files will be contained in a separate folder (crisis).
  • The crisis center will have its own root component (CrisisListComponent).
  • This root component will have its own router outlet and child routes.

If your app had many feature groups, the app component trees might look like this:

Component Tree

It’s time to add real functionality to the app’s current placeholder crisis list component. As a quick way to upgrade the basic functionality of the crisis list component:

  • Delete the placeholder crisis component file.
  • Create a lib/src/crisis folder.
  • Copy the files from lib/src/hero into the new crisis folder, but rename files, and inside each file, change every mention of “hero” to “crisis”, and “heroes” to “crises”.
  • Add the CrisisService to the providers list of CrisisListComponent.
  • Define the following mock crises:

lib/src/crisis/mock_crises.dart

import 'crisis.dart';

final List<Crisis> mockCrises = [
  Crisis(1, 'Dragon Burning Cities'),
  Crisis(2, 'Sky Rains Great White Sharks'),
  Crisis(3, 'Giant Asteroid Heading For Earth'),
  Crisis(4, 'Procrastinators Meeting Delayed Again')
];

open_in_browser Refresh the browser and ensure that the app’s hero features work as they did before, and that the crisis lists appears. You can’t select a crisis until you’ve added the appropriate crisis routes.

Service isolation note: CrisisService is declared in the CrisisListComponent providers list. This step limits the scope of the service to the app component subtree rooted at the crisis list component. No component outside of the crisis center needs access to the CrisisService.

Routing component

The CrisisListComponent is a routing component like the AppComponent. It has its own router outlet and its own routes. In contrast to the hero list, the CrisisListComponent will display crisis details in a child view of the crisis list. View changes will be triggered by navigation.

Make the following changes to the crisis list component.

Define routes

Create a route path and a route defintion for the child route to use to access crisis details:

lib/src/crisis/route_paths.dart

import 'package:angular_router/angular_router.dart';

import '../route_paths.dart' as _parent;

export '../route_paths.dart' show idParam, getId;

class RoutePaths {
  static final crisis = RoutePath(
    path: ':${_parent.idParam}',
    parent: _parent.RoutePaths.crises,
  );
}

lib/src/crisis/routes.dart

import 'package:angular_router/angular_router.dart';

import 'crisis_component.template.dart' as crisis_template;
import 'route_paths.dart';

export 'route_paths.dart';

class Routes {
  static final crisis = RouteDefinition(
    routePath: RoutePaths.crisis,
    component: crisis_template.CrisisComponentNgFactory,
  );

  static final all = <RouteDefinition>[
    crisis,
  ];
}

Add a router outlet

Make the following changes to the crisis list component:

  • Drop the app route paths import.
  • Import the crisis routes.
  • Export RoutePaths and Routes using the exports argument of the @Component annotation.
  • Add RouterOutlet to the crisis list component directives metadata.

lib/src/crisis/crisis_list_component.dart (routes)

import 'routes.dart';

@Component(
  selector: 'my-crises',
  // ···
  directives: [coreDirectives, RouterOutlet],
  // ···
  exports: [RoutePaths, Routes],
)
class CrisisListComponent implements OnActivate {
  final CrisisService _crisisService;
  final Router _router;
  List<Crisis> crises;
  Crisis selected;

  CrisisListComponent(this._crisisService, this._router);
  // ···
}

Add a <router-outlet> element to the end of the component template, binding the routes input to Routes.all:

lib/src/crisis/crisis_list_component.html

<h2>Crisis Center</h2>
<ul class="items">
  <li *ngFor="let crisis of crises"
    [class.selected]="crisis === selected"
    (click)="onSelect(crisis)">
    <span class="badge">{{crisis.id}}</span> {{crisis.name}}
  </li>
</ul>
<router-outlet [routes]="Routes.all"></router-outlet>

open_in_browser Refresh the browser. Select a crisis and its details appear! But notice that the selected crisis isn’t shown as active in the list.

Show selected crisis as active

The crisis list selection code is still based on the hero list implementation: a crisis is selected when an ID is provided as an optional query parameter (try it: localhost:8080/#/crises?id=2):

lib/src/crisis/crisis_list_component.dart (_select)

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

This isn’t the behavior you want for the crisis center. Instead you want the following paths processing:

  • /crises results in a crisis list without any selected item.
  • /crises/id both:
    • Displays the details of the identified crisis
    • Shows this crisis as active in the list.

Crisis list highlighting can be fixed using the following simple change:

lib/src/crisis/crisis_list_component.dart (_select)

Crisis _selectHero(RouterState routerState) {
  final id = getId(routerState.parameters);
  return id == null
      ? null
      : crises.firstWhere((e) => e.id == id, orElse: () => null);
}

open_in_browser Refresh the browser and select a crisis. The selected crisis is shown as active in the list and crisis details appear.

Crisis center home

Through its router outlet, the crisis list component displays the details of a selected crisis. What should it display when no crisis is selected?

Crisis home component

For this purpose, create the following “home” component:

lib/src/crisis/crisis_list_home_component.dart

import 'package:angular/angular.dart';

@Component(
  selector: 'crises-home',
  template: '<p>Welcome to the Crisis Center</p>',
)
class CrisisListHomeComponent {}

Crisis home route

Add a path and route to this component:

lib/src/crisis/route_paths.dart (home)

static final home = RoutePath(
  path: '',
  parent: _parent.RoutePaths.crises,
  useAsDefault: true,
);

lib/src/crisis/routes.dart (home)

import 'crisis_list_home_component.template.dart' as crisis_list_home_template;
import 'route_paths.dart';

export 'route_paths.dart';

class Routes {
  // ···
  static final home = RouteDefinition(
    routePath: RoutePaths.home,
    component: crisis_list_home_template.CrisisListHomeComponentNgFactory,
    useAsDefault: true,
  );

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

Now two routes navigate to the crisis center child components, CrisisListHomeComponent and CrisisComponent, respectively.

Being child routes, there are some important differences in the way the router treats them:

  • The router displays the components of these routes in the router outlet of the CrisisListComponent, not in the router outlet of the AppComponent shell.
  • At the top level, paths that begin with / refer to the root of the app. But child route’s path extend the path of the parent route. With each step down the route tree, you add a slash followed by the route path, unless the path is empty.

Apply that logic to navigation within the crisis area for which the parent path is /crises.

  • To navigate to the CrisisListHomeComponent, the full URL is /crises (/crises + '').

  • To navigate to the CrisisComponent for a crisis with id=2, the full URL is /crises/2 (/crises + '/' + '2').

The absolute URL for the latter example, including the localhost origin, is localhost:8080/#/crises/2.

Looking at the CrisisListComponent alone, you can’t tell that it is a child routing component. You can’t tell that its routes are child routes, indistinguiable from top level app routes. This is intentional, and it makes it easier to reuse component routers. What makes a component a child router of our app is determined by the parent route configuration.

open_in_browser Refresh the browser and visit the crisis center to see the home component rendered. Select a crisis and its details appear. Click Cancel and the home welcome message reappears.

TODO: continue here. You should be able to select crises and see their details. Unfortunately, the crisis details Back button doesn’t work yet. You’ll fix that next.

The currently faulty crisis view Back button has an event binding to this method:

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

This is the version of the method copied from the hero component. It attempts to navigate back to the hero view, using paths defined for the app root.

To fix this, first adjust the route path import URI so that it refers to crisis route paths:

lib/src/crisis/crisis_component.dart (import)

import 'route_paths.dart';

Change the navigate() method argument to be the path to the crisis home:

lib/src/crisis/crisis_component.dart (goBack)

Future<NavigationResult> goBack() =>
    _router.navigate(RoutePaths.home.toUrl());

open_in_browser Refresh the browser, select a crisis and ensure that the Back button causes the crisis details view to be replaced by the crises home message.

App code

After these changes, the folder structure looks like this:

  • router_example
    • lib
      • app_component.dart
      • src
        • crisis
          • crises_component.{css,dart,html}
          • crises_home_component.dart
          • crisis.dart
          • crisis_component.{css,dart,html}
          • crisis_service.dart
          • mock_crises.dart
          • route_paths.dart
          • routes.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 key files for this version of the sample app:

|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 'crisis.dart'; |import 'crisis_service.dart'; | |@Component( | selector: 'my-crisis', | templateUrl: 'crisis_component.html', | styleUrls: ['crisis_component.css'], | directives: [coreDirectives, formDirectives], |) |class CrisisComponent implements OnActivate { | Crisis crisis; | final CrisisService _crisisService; | final Router _router; | | CrisisComponent(this._crisisService, this._router); | | @override | void onActivate(_, RouterState current) async { | final id = getId(current.parameters); | if (id != null) crisis = await (_crisisService.get(id)); | } | | Future<NavigationResult> goBack() => | _router.navigate(RoutePaths.home.toUrl()); |} |import 'dart:async'; | |import 'package:angular/angular.dart'; |import 'package:angular_router/angular_router.dart'; | |import 'crisis.dart'; |import 'crisis_service.dart'; |import 'dialog_service.dart'; |import 'routes.dart'; | |@Component( | selector: 'my-crises', | templateUrl: 'crisis_list_component.html', | styleUrls: ['crisis_list_component.css'], | directives: [coreDirectives, RouterOutlet], | providers: [ | ClassProvider(CrisisService), | ClassProvider(DialogService), | ], | exports: [RoutePaths, Routes], |) |class CrisisListComponent implements OnActivate { | final CrisisService _crisisService; | final Router _router; | List<Crisis> crises; | Crisis selected; | | CrisisListComponent(this._crisisService, this._router); | | Future<void> _getCrises() async { | crises = await _crisisService.getAll(); | } | | @override | void onActivate(_, RouterState current) async { | await _getCrises(); | selected = _select(current); | } | | Crisis _select(RouterState routerState) { | final id = getId(routerState.parameters); | return id == null | ? null | : crises.firstWhere((e) => e.id == id, orElse: () => null); | } | | void onSelect(Crisis crises) => _gotoDetail(crises.id); | | String _crisisUrl(int id) => | RoutePaths.crisis.toUrl(parameters: {idParam: '$id'}); | | Future<NavigationResult> _gotoDetail(int id) => | _router.navigate(_crisisUrl(id)); |} <h2>Crisis Center</h2> <ul class="items"> <li *ngFor="let crisis of crises" [class.selected]="crisis === selected" (click)="onSelect(crisis)"> <span class="badge">{{crisis.id}}</span> {{crisis.name}} </li> </ul> <router-outlet [routes]="Routes.all"></router-outlet> import 'crisis.dart'; final List<Crisis> mockCrises = [ Crisis(1, 'Dragon Burning Cities'), Crisis(2, 'Sky Rains Great White Sharks'), Crisis(3, 'Giant Asteroid Heading For Earth'), Crisis(4, 'Procrastinators Meeting Delayed Again') ]; |import 'package:angular_router/angular_router.dart'; | |import '../route_paths.dart' as _parent; | |export '../route_paths.dart' show idParam, getId; | |class RoutePaths { | static final crisis = RoutePath( | path: ':${_parent.idParam}', | parent: _parent.RoutePaths.crises, | ); | | static final home = RoutePath( | path: '', | parent: _parent.RoutePaths.crises, | useAsDefault: true, | ); |} |import 'package:angular_router/angular_router.dart'; | |import 'crisis_component.template.dart' as crisis_template; |import 'crisis_list_home_component.template.dart' as crisis_list_home_template; |import 'route_paths.dart'; | |export 'route_paths.dart'; | |class Routes { | static final crisis = RouteDefinition( | routePath: RoutePaths.crisis, | component: crisis_template.CrisisComponentNgFactory, | ); | static final home = RouteDefinition( | routePath: RoutePaths.home, | component: crisis_list_home_template.CrisisListHomeComponentNgFactory, | useAsDefault: true, | ); | | static final all = <RouteDefinition>[ | crisis, | home, | ]; |}