Mind is Software

Ying’s thoughts about software and business

Angular Routing

This blog is an attemp to understand the design and implementation of Angular routing.

1 Basic Concepts

The core routing function is to navigate among components and sync a route state with the url. There are two tree structures used in routing: 1) the Route[] array of routes forms a configuration tree. 2) a url represents a router state consisting of the current page’s activated routes. A url is represented as a UrlTree state tree.

There are two ways to navigate from one router state to another one: imperatively by calling router.navigate and declaratively by using the RouterLink directive.

An activated route can be associated with a component. There is always an activated route associated with the root component of the application.

There is a global router service responsible for the management of route states and URL. The global router service and main configurations are imported by routerModule.forRoot() method. Lazy loaded module routers are imported by routerModule.forChild() method.

The navigation cycle starts with redirecting (if ther is one) and recognizing a new url to a router state. Then guards and resolvers are processed to generate a new router state, views are activated and location is updated. The router sevice emits events during the lifecycle.

The router service also loads lazy modules and merge definted routes with the main configuration.

2 Route Configuration and URL

The navigation tree are defined by an array of router configuration objects, abbreviated as router objects or routes. A route maps a segement of a url to a routable state such as a component, a redirect, etc.

A route has two parts: a path and an optional action.

The path can be one of the following:

  • a constant segment: a string such as 'message'.
  • a variable segment: a string with a ':' prefix as ':folder'.
  • an empty semgent of '': any path starts and ends with an empty segment. It consumes nothing but can instantiate a component. It inherits matrix parameters of its parent. Use pathMatch: 'full' to force a full matching strategy.
  • a wildcard: ** matches anything.

The action part can be redirectTo, component or empty for a componentless route.

An url is a serialized router state. A router state is represented by a UrlTree object that, in addition to the fragement and the queryParams properties, has a parent property and a children property. The children consists of one or more UrlSegmentGroup, one for each outlet. A UrlSegmentGroup has an array of UrlSegment that is a string between two slashes in the url. Each UrlSegment is to be matched to a path property in the router configuraiton. UrlSegment can contain matrix parameters such as /users;name=nate;type=admin/.

There is one UrlSegmentGroup for each outlet. The default anounymous outlet is the primary outlet. Secondary outlet are defined within parentheses in a url, such as (secondary_outlet_name:secondary_path_name). There could be more than one secondary children separated by a ‘//’. For example: /inbox/33(popup:message/44//help:overview) means the root has two secondary outlets: popup and help. In the url /inbox/33/(messages/44//side:help), there is one secondary outlet side under the /inbox/33/. Outlets are routed independently of each other.

Angular uses depth-first search to match a url to the configuration tree to create a RouterState and a state snapshot.

4 Router States

After matching a url to the configuration tree, we have a set of components and other routing data such as url segments, query parameters, router parameters (also called matrix parameters) and the fragment – together called the current router state. The routerState is a property of the global router service.

The routerState has two properties: snapshot, and root. The snapshot is a tree of ActivatedRouteSnapshot objects. These are static objects that are used immediately. The root is a tree of ActivatedRoute objects. They are dynamic and are observalbe to listen for state changes. A route state can have multiple trees of ActivatedRoute, one for each outlet.

4.1 The Interfaces

The three interfaces have the following defintion:

interface RouterState {
  snapshot: RouterStateSnapshot //returns current snapshot
  root: ActivatedRoute

interface ActivatedRouteSnapshot {
  url: UrlSegment[]
  paramMap: ParamMap
  data: { [name: string]: any }
  queryParamMap: ParamMap
  fragment: string
  root: ActivatedRouteSnapshot
  parent: ActivatedRouteSnapshot
  firstchild: ActivatedRouteSnapshot
  children: ActivatedRouteSnapshot[]

interface ActivatedRoute {
  snapshot: ActivatedRouteSnapshot //returns current snapshot
  url: Observable<UrlSegment[]>
  paramMap: Observable<ParamMap>
  data: Observable<{ [name: string]: any }>
  queryParamMap: Observable<ParamMap>
  fragment: Observable<string>
  root: ActivatedRoute
  parent: ActivatedRoute
  firstchild: ActivatedRoute
  children: ActivatedRoute[]

The ActivatedRoute exposes its values as observables thus changes can be handled. It can be injected to a component’s constructor. Its snapshot property points to the current ActivatedRouteSnapshot object that might be changed when the url changes.

4.2 Data

You can config a route with a data property that is a static object that doesn’t change. The resolve is used for dynamic data. It takes a name and a resovler type that is provided by an injector. The router combines the resolved and static data into a single proeprty.

Unlike query params, router params, and the segment, the data is not serialized in a url.