Search code examples
angularangular2-routingnested-routes

Angular child route not updating when the subpath changes


Tinkering with my work-along from the Angular "Tour of Heroes" tutorial, I'm trying to see how child routes work. I have a HeroesComponent with an embedded <router-outlet></router-outlet>. HeroesComponent, reached via '/heroes', displays a list of links for individual heroes, each with routerLink set to '/heroes/[id]', with that hero's ID appearing instead of '[id]'--'/heroes/7', for example.

(I know how to add a child component directly to a parent component without using a child route. My purpose here is to learn how child routes work. With a direct child component, I know to use ngOnChanges, but in this case, it isn't an @Input component property that's changing, it's a router property.)

Routing

const routes: Routes = [
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  { path: 'heroes', component: HeroesComponent, runGuardsAndResolvers: 'always', children: [
    { path: ':id', component: HeroDetailComponent, runGuardsAndResolvers: 'always' }
  ] },
  { path: 'dashboard', component: DashboardComponent }
];

Child router outlet in parent HeroesComponent

<section>
    <h2>All Heroes</h2>
    <div><a routerLink="/detail">Create a hero</a></div>
    <table class="table">
        <thead>
            <tr><th>Name</th><th>Location</th></tr>
        </thead>
        <tbody>
            <tr *ngFor="let hero of heroes">
                <th><a routerLink="/heroes/{{hero.id}}">{{hero.name}}</a></th>
                <td>{{hero.location}}</td>
                <td><a (click)="delete(hero.id)">delete</a></td>
            </tr>
        </tbody>
    </table>
</section>

<router-outlet></router-outlet>

Key code in the child HeroDetailComponent

ngOnInit(): void {
  let id: string;
// I'll explain this comment later.
//    this.route.paramMap.subscribe((paramMap) => id = paramMap.get('id'));
  id = this.route.snapshot.paramMap.get('id');
  if (id) {
    this.mode = 'update';
    this.heroService.getHero(+id).subscribe((hero) => {
      this.hero = hero
    });
  } else {
    this.mode = 'add';
    this.hero = { id: 0, name: '', location: '', cuisine: 0 }
  }
}

Navigating to /heroes gives me the following. (I already had a restaurant API set up for a .NET CORE tutorial, so I'm repurposing that as my data source for this Angular tutorial.)

enter image description here

The browser's address field shows "localhost:4200/heroes". When I hover the cursor over "Masseria", the browser status bar (this is Chrome on Windows 10, if it matters) reads "http://localhost:4200/heroes/9". Clicking the Masseria link, I get:

enter image description here

So far, so good! But then, when I click, say, the Takumi link, though the contents of the browser address field change correctly to "http://localhost:4200/heroes/13", the display doesn't change: I'm still seeing the details for Masseria.

Now, if I click into the browser address field and press Enter, then the entire page refreshes, showing me both the list of restaurants and the details for Takumi.

I figure some necessary update notification isn't happening. I did a little research. Regarding the line I commented out in the last chunk of code above, I thought that maybe subscribing explicitly to the router parameter :id would help, using

this.route.paramMap.subscribe((paramMap) => id = paramMap.get('id'));

instead of taking the parameter from the router snapshot. This had no effect.

I also tried adding the attribute runGuardsAndResolvers: 'always' to both the parent and child paths in my routes array (first chunk of code, above), but that also had no effect.


Solution

  • Ack, I got it. I was halfway there with the subscription in OnInit to the router's paramMap:

    this.route.paramMap.subscribe((paramMap) => id = paramMap.get('id'));

    The part I was missing is that all the state computation needs to be inside the subscription callback, not just setting the ID value. Behold:

      ngOnInit(): void {
        let id: string;
        this.route.paramMap.subscribe((paramMap) => {
          id = paramMap.get('id');
          if (id) {
            this.mode = 'update';
            this.heroService.getHero(+id).subscribe((hero) => {
              this.hero = hero
            });
          } else {
            this.mode = 'add';
            this.hero = { id: 0, name: '', location: '', cuisine: 0 }
          }
        }
      );
    

    Now it's working as intended.