Search code examples
angularangular-routingangular10

Angular: How to route to different components, depending on API response?


I am wondering whether / how it is possible to implement a specific routing strategy in Angular 10.

To explain, take this simplified example:

I have an Angular application with 4 components:

  • StudentListComponent: Displays list of all students
  • CourseListComponent: Displays list of all courses
  • StudentComponent: Displays details about one specific student
  • CourseComponent: Displays details about one specific course

I would like to achieve the following routing strategy

  • myapp.com/students routes to "StudentListComponent"
  • myapp.com/courses routes to "CourseListComponent"
  • myapp.com/millerh routes to "StudentComponent" and displays details about student with username "millerh"
  • myapp.com/math1 routes to "CourseComponent" and displays details about course with short name "math1"

The names "millerh" and "math1" are unique, and there is no id that maps to both a student and a course. (EDIT: There will also be no student or course called "students" or "courses")

Background: The links will be displayed in another application where the links will not be clickable and can not be copy/pasted, thus I want the paths to be as short as possible. Also power users will prefer to directly type out the URL rather than going to the list pages and searching for the student / course they want to see details of.

To achieve this, I want to make a backend/API call during routing. The API response will indicate, whether i.e. "millerh" is a student, a course or neither of the two. Depending on that I want to navigate to the respective component, and pass the name of the student/course along to the component. I would like to avoid a redirect, i.e. from myapp.com/millerh to myapp.com/students/millerh, to avoid having multiple valid paths for the same resource.

How could I achieve this, starting from the out-of-the-box angular routing module?

Thanks very much for any hints and suggestions!


Solution

  • Alright, I have found a working solution that I like quite much: It seems to work well, and does not feel very hacky. It's not really matching exactly any of the posted suggestions - but thanks for all the replies and comments as they really helped me finding the final solution!

    The final solution uses in its core a guard implementing CanActivate, combined with router.resetConfig() and router.navigate() inside the guard.

    As an entry point to my solution I use the standard routing module. It's still useful to my use case as there are components that have a single static route.

    file: app-routing.module.ts

    import { NgModule } from '@angular/core';
    import { Routes, RouterModule } from '@angular/router';
    import { HomeComponent } from './features/home/home.component';
    import { StudentListComponent} from './features/student-list/student-list.component';
    import { CourseListComponent} from './features/course-list/course-list.component';
    import { LoadingComponent } from './features/loading/loading.component';
    import { NotFoundComponent } from './features/not-found/not-found.component';
    import { DynamicRouteGuard } from './guards/dynamic-route.guard';
    
    const routes: Routes = [
      { 
        path: '/', 
        component: HomeComponent
      },
      {
        path: 'students',
        component: StudentListComponent
      },
      {
        path: 'courses',
        component: CourseListComponent
      },
      {
        path: 'not-found',
        component: NotFoundComponent
      },
      {
        path: '**',
        canActivate: [ DynamicRouteGuard ],
        component: LoadingComponent,
      }
    ];
    
    @NgModule({
      imports: [RouterModule.forRoot(routes)],
      exports: [RouterModule]
    })
    export class AppRoutingModule { }
    

    The paths /, students, courses, and not-found are normal static routes like in most Angular apps. Any route which does not match these will be treated by the wildcard route at the bottom. That route loads a component which may hold a loading spinner. It will be visible while the Guard makes the API call to the backend asynchronously, to determine which component to load.

    file: dynamic-route.guard.ts

    import { Injectable } from '@angular/core';
    import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';
    import { Observable } from 'rxjs';
    import { EntityTypeService } from '../../services/entity-type.service';
    import { IEntityTypeModel } from '../../models/entity-type.model';
    import { EntityType } from '../../models/entity-type.enum';
    import { StudentDetailComponent } from '../features/student-detail/student-detail.component';
    import { CourseDetailComponent } from '../features/course-detail/course-detail.component';
    
    @Injectable({
      providedIn: 'root'
    })
    export class DynamicRouteGuard implements CanActivate {
      constructor(
        private entityTypeService : EntityTypeService,
        private router : Router) { }
    
      canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): 
        Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    
        // At this point, only routes with a single path element are considered valid routes.
        if (route.url.length != 1) {
          this.router.navigate(['not-found']);
          return false;
        }
    
        let path = route.url[0].toString();
    
        // Ask backend, what kind of entity the requested path matches to
        this.entityTypeService.getEntityTypeForPath(path).subscribe(response => {
          let entityTypeModel = response as IEntityTypeModel;
    
          // If backend did not recognize path --> redirect to page not found component.
          if (entityTypeModel.entityType === EntityType.Unknown) {
            this.router.navigate(['not-found']);
          }
    
          // Build a new routes array. Slicing is required as wildcard route
          // should be omitted from the new routes array (risk of endless loop)
          let routes = this.router.config;
          let newRoutes = routes.slice(0, routes.length - 1);
    
          // Add a new route for the requested path to the correct component.
          switch(entityTypeModel.entityType) {
            case EntityType.Student:
              newRoutes.push({ path: path, component: StudentDetailComponent, data: { resourceName: path } });
              break;
            case EntityType.Course:
              newRoutes.push({ path: path, component: CourseDetailComponent, data: { resourceName: path } });
              break;
            default:
              this.router.navigate(['not-found']);
              return;
          }
    
          // Reload routes and navigate.
          this.router.resetConfig(newRoutes);
          this.router.navigate([path]);
        });
    
        // Guard always returns true and loads LoadingComponent while API 
        // request is being executed.
        return true;
      }
    }
    

    In the guard, the requested route is accessible and an API service can easily be injected. Using that service, the backend looks up the path in the database, and returns an enum value indicating whether its a Student, a Course or neither. Depending on that, a new route is added to the Routes array, for that specific student/course name, linking to the matching component. After that, the routes are reloaded, and the Router can directly navigate to the correct component.