Imperative Navigation and Route Parameters (DRAFT) 6.0
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:
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:
- Create a
hero
folder underlib/src
— you’ll be adding files implementing hero management there. - Copy the following files from toh-5
lib/src
into the newhero
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
- In
hero_list_component.html
, rename the<ul>
element class toitems
(fromheroes
). - In
app_component.dart
, import and then addClassProvider(HeroService)
to theproviders
list so that the service is available everywhere in the app. - In
src/routes.dart
, adjust the hero component import path because the new file is underheroes
. - 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.
Navigating imperatively
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.
Navigating back and selecting a hero
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:
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 thanOnNgInit
. - Replace
ngOnInit()
byonActivate()
, 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
- lib
Here are the relevant files for this version of the sample app: