Angular Routing: Understanding Child Routes

Most applications, even single page apps, have routing. Just like most things when it comes to code, there are several ways that routes can be implemented in Angular. In this post, we will look at ways to implement child routes and the pros and cons of each technique.

When creating navigation for an application we often need a route to a list of items, and then the ability to navigate to the item itself. Let's use a list of drink recipes for a coffee shop. Our routes could look something as follows:

Angular Routes
const routes: Routes = [
  { path: 'home': component: HomeComponent },
  { path: 'drinks': component: ListComponent },

  { path: '', redirectTo: '/home', pathMatch: 'full' }
]

In the above example (Listing 1) we have declared 2 routes, one to the home page with a path of / and one to the recipes pages with a path of /list. From there we have 2 options in how we structure our individual recipe routes.

Flat Routes

The first option is to stay at the same level of routing and add some slashes.

Flat Routes
const routes: Routes = [
  { path: 'home': component: HomeComponent },
  { path: 'drinks': component: ListComponent },
  { path: 'drinks/:drinkId': component: DrinkComponent },

  { path: '', redirectTo: '/home', pathMatch: 'full' }
]

Notice the last route declared in Listing 2:

{ path: 'drinks/:drinkId': component: RecipeComponent },

We declare a parameter :drinkId for each page that is going to be a recipe. However, our route is at the same level as the recipe list. The routes aren't nested in any way.

This architecture if diagrammed looks as follows (figure 1).

diagram showing the routes as a three with all the routes in the same column.

Flat Route Diagram

The downside to this technique is that is breaks relative paths inside of the application. Let's say we add 3rd level to our routing, which includes nutrition information for our drink (Listing 3).

Flat Routes, routes 3 deep
const routes: Routes = [
  { path: 'home': component: HomeComponent },
  { path: 'drinks': component: ListComponent },
  { path: 'drinks/:drinkId': component: DrinkComponent },
  { path: 'drinks/:drinkId/nutrition': component: NutritionComponent },

  { path: '', redirectTo: '/home', pathMatch: 'full' }
]

To add a link back to the recipe from the nutrition page, we must be aware of which recipe we came from in order to jump back up a level.

Flat Routes Link Back Up a Level
<!--
    We Cannot use.
  This would take use back to root,and therefore home 
-->
<a [routerLink]="['../']">Back to Recipe</a>

<!-- We must use -->
<a [routerLink]="['/drinks', drinkId]">Back to Recipe</a>

The advantage of this technique however is in the ease of getting all of the parameters for the route. By subscribing to ActivatedRoute params or by looking at the params in the ActivatedRoute snapshot, all of the parameters will be listed. Note that unique names for parameters becomes important here, let's look at how this plays out below in Listing 5.

Flat Routes With Multiple `Params`
//  Assuming the following route structure
const routes: Routes = [
  { path: 'home': component: HomeComponent },
  { path: 'shops': component: ListComponent },
  { path: 'shops/:shopId': component: ShopComponent },
  { path: 'shops/:shopId/drinks': component: ListComponent },
  { path: 'shops/:shopId/drinks/:drinkId': component: DrinkComponent },

  { path: '', redirectTo: '/home', pathMatch: 'full' }
]

//  When on a particular drink page
@Component({
  selector: 'app-drink',
  template: `...`
})
export class DrinkComponent implements OnInit {

  constructor(private route: ActivatedRoute)

  ngOnInit() {
    this.route.params.subscribe(params => {
      console.log(params)
      // { shopId: '123', drinkId: '789' }
    })
  // OR
    console.log(this.route.snapshot.params)
    // { shopId: '123', drinkId: '789' }
  }
}

Whether we are subscribing to the ActivatedRoute params or looking at them from the snapshot, both the parameters are here and easily available.

Let's look a different way of creating our routes however, using child routes.

Child Routes

We will start again with our coffee shop example but this time instead of all of the routes being at the same hierarchical level, we will create child routes so that our hierarchy can not just be visual in the URL, such as the first example, but also programmatic (Listing 6).

Child Routes
const routes: Routes = [
  { path: 'home': component: HomeComponent },
  { path: 'shops', children: [
    { path: '': component: ListComponent },
    { path: ':shopId', children: [
      { path: '': component: ShopComponent },
      { path: 'drinks': children: [
        { path: '': component: ListComponent },
        { path: ':drinkId': component: DrinkComponent },
      ]},
    ]},
  ]},

  { path: '', redirectTo: '/home', pathMatch: 'full' }
]

The above code (Listing 6), diagrammed out produces the following tree (Figure 2). diagram showing the routes nested accross multiple columns.

Nested Routes Diagram

We now have true hierarchy in our routes, so if we need to have a link back to the drink list from a particular drink recipe, we can now use relative paths without being sent directly back to root (Listing 7).

Link Back Up A Level
<a [routerLink]="['../']">Back to Drinks</a>

We can also use relative paths in our typescript using relativeTo in our navigation method (Listing 8).

Navigation Method
@Component({
  selector: 'app-drink',
  template: `...`
})
export class DrinkComponent {

  constructor(
    private route: ActivatedRoute,
    private router: Router
  )

  back() {
    this.router.navigate([../], { relativeTo: this.route })
  }
}

By adding { relativeTo: this.route } in our navigate method we set what route the method needs to be relative to. In this case we are telling the method that the path should be relative to our current route.

The disadvantage to using child routes is that we don't get all the parameters right when we call the subscribe to the route params on our current route (Listing 9).

Nested Routes Params
// Assuming our previously established nested routes structure
const routes: Routes = [
  { path: 'home': component: HomeComponent },
  { path: 'shops', children: [
    { path: '': component: ListComponent },
    { path: ':shopId', children: [
      { path: '': component: ShopComponent },
      { path: 'drinks': children: [
        { path: '': component: ListComponent },
        { path: ':drinkId': component: DrinkComponent },
      ]},
    ]},
  ]},

  { path: '', redirectTo: '/home', pathMatch: 'full' }
]

//  When on a particular drink page
@Component({
  selector: 'app-drink',
  template: `...`
})
export class DrinkComponent implements OnInit {

  constructor(private route: ActivatedRoute)

  ngOnInit() {
    this.route.params.subscribe(params => {
      console.log(params)
      // { drinkId: '789' }
    })
  // OR
    console.log(this.route.snapshot.params)
    // { drinkId: '789' }
  }
}

We when subscribing to the routes parameters, we only get the drinkId, we do not get the shopId. We have 2 options go also get the shopId.

  1. We can go up the route tree and by subscribing to the route's ancestor that includes the ID, look for the parameter value such as in Listing 10.
  2. Or we can set router configurations to include all parameters.

Version 1, going up the route tree to get the parameter from the ancestor is not ideal because it requires the component to understand where it lies in the route tree, and needs to be done for every parameter needed. Figure 10 shows how we would go about getting the shop ID.

Getting `Params` From an Ancestor
// Assuming our previously established nested routes structure
const routes: Routes = [
  { path: 'home': component: HomeComponent },
  { path: 'shops', children: [
    { path: '': component: ListComponent },
    { path: ':shopId', children: [
      { path: '': component: ShopComponent },
      { path: 'drinks': children: [
        { path: '': component: ListComponent },
        { path: ':drinkId': component: DrinkComponent },
      ]},
    ]},
  ]},

  { path: '', redirectTo: '/home', pathMatch: 'full' }
]

//  When on a particular drink page
@Component({
  selector: 'app-drink',
  template: `...`
})
export class DrinkComponent implements OnInit {

  constructor(private route: ActivatedRoute)

  ngOnInit() {
    this.route.params.subscribe(params => {
      console.log(params)
      // { drinkId: '789' }
    })
    this.route.parent.parent.params.subscribe(params => {
      console.log(params)
      // { shopId: '123' }
    })
  // OR
    console.log(this.route.snapshot.params)
    // { drinkId: '789' }
    console.log(this.route.parent.parent.snapshot.params)
    // { shopId: '123'}
  }
}

For every parameter we want to get, we must go up the three and individually get the parameter value. Not ideal. This is where option 2 comes into play. In our module we can set our router configurations to include all params in the route data as if we had flat routes. Listing 11 shows how it's done.

Routing Configurations

const routes = [ ... ]
const routingConfiguration: ExtraOptions = {
  paramsInheritanceStrategy: 'always'
};

@NgModule ({
  imports: [
    BrowserModule,
    RouterModule.forRoot(routes, routingConfiguration)
  ],
  declarations: [ ... ],
  ...
})

Where ever the RouterModule.forRoot() is being called, usually in app.module, we add a configuration object that contains paramsInheritanceStrategy: 'always' in the forRoot method. This makes ActivatedRoute show current params as well as ancestor route params.

Routing Configurations
//  When on a particular drink page
@Component({
  selector: 'app-drink',
  template: `...`
})
export class DrinkComponent implements OnInit {

  constructor(private route: ActivatedRoute)

  ngOnInit() {
    this.route.params.subscribe(params => {
      console.log(params)
      // { shopId: '123', drinkId: '789' }
    })
  // OR
    console.log(this.route.snapshot.params)
    // { shopId: '123', drinkId: '789' }
  }
}

When on our drink recipe page, we now also get the shopId by looking at the params. We no longer need to go hunt through the ancestors to go find it.

Wrapping it up

We have reviewed two options when it comes to setting up our routes.

We can choose to have all of our routes at the same level. This is very easy to set up and does not require any extra configuration in order to get all of the parameters on the route. The downside to this approach is the lack of programmatic hierarchy which leads to breaking the ability to use relative paths.

With child routes, we gain the programmatic hierarchy which allow us to use relative paths both in the HTML and when using the navigate method. The downside is that it requires more setup, both in the routes, and in the configurations if we want to easily get access to all of the params including those on ancestor routes.

And of course, although I would not recommend, we can always mix the two strategies. Happy Coding!

Solutions

Looking for a partner to support your core business practices? Our solutions have you covered.

  • Training
  • Product Management
  • Product Development
Read more about Solutions with Andromeda