Child & Relative Routes (DRAFT) 6.0
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:
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 newcrisis
folder, but rename files, and inside each file, change every mention of “hero” to “crisis”, and “heroes” to “crises”. - Add the
CrisisService
to theproviders
list ofCrisisListComponent
. - 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
andRoutes
using theexports
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 theAppComponent
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 withid=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.
Navigating back to home
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
- crisis
- web
- index.html
- main.dart
- styles.css
- lib
Here are key files for this version of the sample app: