Home » Software Development Resources » Angular Children Routing with Deep Linking

Angular Children Routing with Deep Linking

 

On a recent Angular migration project, I had to prototype a solution that would display the object details and relationship of a complex object graph where any object in the graph could be the root. This display had a set of tabs where the template in the tabs could be reused by any of the other objects in the graph. The legacy code had a significant amount of abstraction and a custom router to preserve deep linking. The goal was to use as much core Angular as possible and to minimize the amount of code to develop and preserve deep linking.

The solution detailed in this article utilizes a simple interface design, Angular routing, child routes, and the activate binding on the Angular router-output. This solution meets all the above criteria and allows the parent component to control the state of its children. It also has the added benefit of not needing a session store or any additional bloated node packages.

In this example, we will use a movie object an example. The tabs to display are Details, Actor, and Theaters. The movie is the parent component and has three tabs. Each tab is a child of the movie.component and has its own components. This allows us to reuse the tabs for other pages. For example, this app could also display data about Broadway plays. Actors and theater could be reused. The details could be reused, but a list of fields could be different and passed in by the parent component. However, in this example, we will be just setting the state of the child components from the parent in the Movie example.

Each child component implements the ChildRouterComponent interface and implements the updateData method. The Movie (Parent) component defines a method that takes in the ChildRouterComponent, sets the state, and calls updateData, which will rerender the child component.

export class MovieComponent implements OnInit {
    movie: Movie | undefined;
    movieId = 1;
    navLinks: Array<NavLink> | undefined;
    activeLinkIndex = -1;
    childComponent: ChildRouterComponent | undefined;

    constructor(private service: MovieService,
                private route: ActivatedRoute,
                private router: Router) {
    this.navLinks = [
        {
            label: 'Details',
            link: './details',
            index: 0
        }, {
            label: 'Actors',
            link: './actors',
            index: 1
        }, {
            label: 'Theater',
            link: './theaters',
            index: 2
        },
    ] satisfies Array<NavLink>;
}

ngOnInit(): void {
    console.log('Parent ngOnInit');
    this.movieId = Number(this.route.snapshot.paramMap.get('id'));
    this.service.getMovie(this.movieId).subscribe(data => this.movie = data);
    ...
}

  onRouterActivate(component: ChildRouterComponent): void {
    console.log('Parent onRouterActivate');
    this.childComponent.id = this.movieId;
    this.childComponent.movie = this.movie;
    this.childComponent.updateData();
  }
}

export interface ChildRouterComponent {
    id: number;
    movie: Movie | undefined;

    updateData(): void;
}

In the onRouterActivate method, we see the parent component casts the child component, sets the movie id, and the movie object that was retrieved from the service when the MovieComponent was created. In this example, the data sets are hard coded in the service class for ease of use, but in a real-world scenario, the service might call out to a REST or GraphQL endpoint.

export class DetailsComponent implements OnInit, ChildRouterComponent  {
    id = 0;
    movie: Movie | undefined;

    constructor() { }

    ngOnInit(): void {
      console.log('DetailsComponent ngOnInit');
    }

   updateData(): void {
     console.log('DetailsComponent updateDate');
     this.ngOnInit();
   }
}

The child component implements the ChildRouterComponent interface defined in the parent component and implements the updateData method which calls the init method. This will cause the child component to process the data rerender and view. It’s important that the child component is null-safe and that the view can handle undefined data. This is because the router-output with render the child component before the activate binding is triggered.

The key to this pattern is the activate binding on the router-outlet. The code below is from the movie.component.html.

<h2>{{movie.title}}</h2>
<div>
  <nav mat-tab-nav-bar [tabPanel]="tabPanel">
   <a 
     mat-tab-link
     *ngFor="let link of navLinks"
     [routerLink]="link.link"
     routerLinkActive 
     #rla="routerLinkActive"
     [active]="rla.isActive"
     >{{link.label}}</a>
  </nav>

  <mat-tab-nav-panel #tabPanel>
    <router-outlet (activate)="onRouterActivate($event)"></router-outlet>
  </mat-tab-nav-panel>
</div>

When the router loads the child component the activate method is called and passes in the event (the child component).

{ path: "", redirectTo: "movie/0", pathMatch: "full" },
    {
        path: "movie/:id",
        component: MovieComponent,
        children: [
            { path: "", redirectTo: "details", pathMatch: "full" },
            { path: "details", component: DetailsComponent },
            { path: "actors", component: ActorsComponent },
            { path: "theaters", component: TheatersComponent },
        ],
    },
];

The router is set up with the movie path and an array of child routes (details, actors, theaters). The children attirbute tell the router that these are subroutes to movie/:id path and allows Angular routing to preserve deep linking.

If you watch the location bar you can see the URL change with the tabs, preserving deep linking.

Using a little Object Oriented Design you can allow a parent component to set the state of its children with a low code approach and without installing any additional node packages. This allows the application to use the Angular router to serve the correct component and control deep linking.

If your looking for help designing, architecting, and developing a performant UX that will serve your customers reach out to us.

You can find this entire project on Github.
Special thanks to Rob Giseburt for helping with editing this project. 

Scroll to Top