Finrod_Amandil
Finrod_Amandil

Reputation: 119

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:

I would like to achieve the following routing strategy

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!

Upvotes: 0

Views: 3791

Answers (2)

Finrod_Amandil
Finrod_Amandil

Reputation: 119

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.

Upvotes: 3

Elmehdi
Elmehdi

Reputation: 1430

I don't think Angular router has any means of distinguishing between millerh being a student and math1 being a course, I think you should go with something like:
myapp.com/students/millerh
myapp.com/courses/math1

EDIT

Imagine one of your users want to search of a student, he is gonna type myapp.com/students/<studentName>, then the app is gonna navigate to students component and the studentName is gonna be available as routing params, then you can use ngOnInit life cycle hook to make your API call to the students endpoint of you backend.

EDIT number 2

Solution without having to type students/courses in the URL but you will still have have them after the URL resolves

Let's say you don't wanna differentiate between the students route and courses route at first.
Image your user type myapp.com/<someValue>, for now you don't know what he is looking for, student or course, at first he is gonna land on your root component, in this component you can use ngOnInit to make your API calls.
Start by making an API call to your students endpoint, if there a student, make your app navigate programmatically to myapp.com/students/<someValue>.
If no student is found, make an API call to your course endpoint, if there a course, make your app navigate programmatically to myapp.com/courses/<someValue>.
Ultimatly there is gonna be a reference to either students of courses in the url, but it is gonna be easier for your users because they can directly type myapp.com/<someValue>.

Solution without having to type students/courses in the URL and you wont have them in the URL at the end either

Instead of having your students component and your courses in two different pages, you can put them both in one single page, maybe the root component in your case, and instead of redirecting either to students or course, you can show one or the other depending on which API call returned a value.

Upvotes: 3

Related Questions