4

I'm working on a project converting an AngularJS app to Angular and I'm facing a blocker regarding routing.

TL/DR: I need routes to be defined based on an API response before the routing module is used.

Working scenario in AngularJS: (Sort of pseudo code further below)

There are several base routes that exist for everyone, these are defined in the standard AngularJS way:

/home
/settings
...etc

Then there are dynamic routes that are created based on an API response

/purchase-requests
/invoices
/godzilla
...etc. Content doesn’t matter, basically, a dynamic list of routes that an existing API gives as an array of strings

The basic workflow of the existing AngularJS app:

  1. The AngularJS app is NOT bound to an element immediately using ng-app, like is usually done.
  2. A raw (or jQuery) response is received from the API on page load.
  3. The AngularJS app is initialized using:
 angular.bootstrap(document.getElementById('mainElementId'),[‘appName']);

This works because of AngularJS's behavior of not calling .config() on load but on bootstrap of the angular app, which we postpone until after the API response.

Sample AngularJS that works today:

<script>

  let appList = [];
  const mainApp = angular.module('mainApp', ['ngRoute']);


  // Controllers
  mainApp.controller('mainController', mainController);
  mainApp.controller('homeController', homeController);
  mainApp.controller('appListController', appListController);
  mainApp.controller('appSingleController', appSingleController);
  mainApp.controller('errorController', errorController);

  // config will not be called until the app is bootstrapped
  mainApp.config(function($routeProvider) {

    // Default routes that everyone gets
    $routeProvider.when('/', { templateUrl: 'views/home.html', controller: 'homeController'});
    $routeProvider.when('/home', { templateUrl: 'views/home.html', controller: 'homeController'});

    // Add the dynamic routes that were retreived from the API
    for (let appName in appList) {
      $routeProvider.when(`/${appName}`, { templateUrl: 'views/app-list.html', controller: 'appListController'});
      $routeProvider.when(`/${appName}/new`, { templateUrl: 'views/app-single.html', controller: 'appSingleController'});
      $routeProvider.when(`/${appName}/view/:itemNumber`, { templateUrl: 'views/app-single.html', controller: 'appSingleController'});
    }

    $routeProvider.otherwise({ templateUrl: 'views/error.html', controller: 'errorController'});
  });



  $(document).ready(function() {
    const options = {
      type: 'GET',
      url: '/api/apps/getAvailableApps',
      success: onAppSuccess,
    };
    $.ajax(options);
  });

  function onAppSuccess(response) {
    appList = response.appList;
    angular.bootstrap(document.getElementById('mainApp'), ['mainApp']);
  }

</script>

<!-- Typically, you bind to the app using ng-app="mainApp" -->
<div id="mainApp" class="hidden" ng-controller="mainController">

  <!-- Route views -->
  <div ng-view></div>

</div>

In Angular 9 (or, seemingly any recent version of Angular), routes are defined in the routing module before initialization of the main component:

const routes: Routes = [
  { path: 'login', component: LoginComponent },
  { path: '', component: DashboardComponent },
  { path: 'home', component: DashboardComponent },
  { path: 'settings', component: SettingsComponent },
];

Using router.resetConfig does not work

Let's say I have the main module load the API config first, then use resetConfig based on the response. This works great if the first page a user loads is / or /home or one of the other predefined routes: The new dynamic routes are created and navigation to them works.

However, if a user navigates directly to a route that's not predefined, (let's say /godzilla) the router doesn't even allow the page to load (or) if the wildcard route is set, brings up the 404. The ngOnInit() in the main component (which I was trying to use to load the API response) never gets a chance to run.

Question is: How can I create routes based on the API response before the router navigation is executed or even initialized?

Jesse Williams
  • 436
  • 4
  • 17

3 Answers3

0

The way I add dynamic routes is to predefine the route url templates using parameters.

const routes: Routes = [
  { path: 'login', component: LoginComponent },
  { path: '', component: DashboardComponent },
  { path: 'home', component: DashboardComponent },
  { path: 'settings', component: SettingsComponent },
  { path: ':appName', canActivate: AppGuard, children: [
    { path: '', component: AppListComponent },
    { path: 'new', component: 'NewAppComponent' },
    { path: 'view/:itemNumber', component: AppSingleComponent }
  ] },
  { path: '**', component: ErrorComponent }
];

Routes are matched in order, so "known" routes should go first. Any URL with a single segment that isn't matched against the "known" routes will be matched against :appName. You can declare a guard to verify that the :appName parameter is valid. If it isn't, the '**' route will be matched against.

The guard would look something like this:

@Injectable({ providedIn: 'root' })
export class AppGuard implements CanActivate {
  constructor(private appService: AppService) {
  }

  canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
    const appName = route.params.appName;
    return this.appService.isValidAppName(appName);
  }
}

Where appService.isValidAppName is some function that validates the app name.

Kurt Hamilton
  • 12,490
  • 1
  • 24
  • 40
0

How can I create routes based on the API response before the router navigation is executed or even initialized?

There are two ways of doing this.

The first way is using Component to display all dynamic routes. All static routes are defined first and lastly the dynamic routes are routed to the DynamicComponent with the routing parameter id. In DynamicComponent, we use ActivatedRoute to get the routing parameter and use Router to navigate to 404 route on failure.

In app-routing.module.ts

const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: "prefix" },
  { path: 'login', component: LoginComponent },
  { path: 'home', component: DashboardComponent },
  { path: 'settings', component: SettingsComponent },
  { path: '404', component: PageNotFoundComponent },
  { path: ':id', component: DynamicComponent },
];

In DynamicComponent

constructor(private aroute: ActivatedRoute, private router: Router) { }

ngOnInit(): void {

  this.aroute.params.pipe(first()).subscribe((param) => {
    console.log(param.id)

    ...   // make any API call with param.id and get a response as promise

    .then( (response) => {

       ...    // do whatever you want to do on success

    })
    .catch( (error) => {

       console.error(error);
       this.router.navigate(['404']);    // route to 404 on failure

    })


  }
}

The second way is using Service to filter all unknown routes. All static routes are defined first and lastly the dynamic routes are routed to the DynamicComponent filtered by DynamicRouteService which implements CanActivate. In DynamicRouteService, we map the next.params to return an Observable<boolean> to the Router module and will hold the routing till the observable is fulfilled.

In app-routing.module.ts

const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: "prefix" },
  { path: 'login', component: LoginComponent },
  { path: 'home', component: DashboardComponent },
  { path: 'settings', component: SettingsComponent },
  { path: '404', component: PageNotFoundComponent },
  { path: ':id', component: DynamicComponent, canActivate: [DynamicRouteService] },
];

Note: make sure to add DynamicRouteService to providers in app.module.ts

In dynamic-route.service.ts

export class DynamicRouteService implements CanActivate {

  constructor(private router: Router) { }

  canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {

    return next.params.pipe(first()).pipe(map(param) => {
      console.log(param.id)

      return ...   // make any API call with param.id and get a response as promise

      .then( (response) => {

         ...    // do whatever you want to do on success
         return true;

      })
      .catch( (error) => {

         console.error(error);
         this.router.navigate(['404']);    // route to 404 on failure
         return false;

      }))

    }
  }
}
T. Sunil Rao
  • 1,167
  • 5
  • 14
-1

Thanks for the responses.

I ended up solving this a different way

I was going down the route of having a "DynamicRouter" component but found a much simpler solution using APP_INITIALIZER.

I've answered this in: Angular load routing from REST service before initialization

Jesse Williams
  • 436
  • 4
  • 17