Before we dive into the article, I would like to mention that the following article deviates a bit from the Angular best practices, however, the world is quite an opinionated place and requirements usually do not follow the newest best practices for various reasons.

The best practice to configure an Angular app is to define multiple environment files and use one of them at compilation time. This of course means that if you would like to configure your application for multiple stages, you will have to build it all over again and deploy newly build bundles. Unfortunately, with line-of-business apps, I mostly saw that the usual way is to build a single bundle which is then deployed through individual stages, and each starts it with a different configuration file.

Therefore, if you find yourself in a similar situation and need to deploy a single configurable Angular application, I would like to show you a quick tip on how to take advantage of guards. Why guards? Well, let’s imagine you are starting a business application that needs to get a configuration for OAuth from the backend. In that case, you need to wait for a display of the page until the config is loaded. You could do it with a simple *ngIf directive on the main template, however, a guard applied to the root route is in my opinion bit safer and more elegant choice.

First of all, we need a service that would load and expose environment variables. In this case, we will download info from Spring Boot’s actuator/info endpoint and set it to a typed variable that is exposed by this service. Since it is an environment service, I also decided to expose Angular’s environment files, but this is usually not needed.

@Injectable({
  providedIn: 'root',
})
export class EnvironmentServiceImpl extends EnvironmentService {
  readonly frontend = environment;
  backend?: BackendEnvironmentConfig;

  constructor(protected http: HttpClient) {
    super();
  }

  loadBackendConfig(): Observable<BackendEnvironmentConfig> {
    return this.http.get(`${location.origin}/actuator/info`).pipe(
      map((info) => info as BackendEnvironmentConfig),
      tap((config) => {
        this.backend = config;
      })
    );
  }
}

Once we have a service, we need a guard that uses it and implements CanActivate interface, that will enable route if there is a backend config available or it will download it in case it isn’t.

@Injectable({
  providedIn: 'root',
})
export class EnvironmentGuard implements CanActivate, CanActivateChild {
  constructor(protected environmentService: EnvironmentService) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ):
    | Observable<boolean | UrlTree>
    | Promise<boolean | UrlTree>
    | boolean
    | UrlTree {
    return this.updateEnvironmentConfig();
  }

  canActivateChild(
    childRoute: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ):
    | Observable<boolean | UrlTree>
    | Promise<boolean | UrlTree>
    | boolean
    | UrlTree {
    return this.updateEnvironmentConfig();
  }

  protected updateEnvironmentConfig(): boolean | Observable<boolean> {
    return this.environmentService.backend
      ? true
      : this.environmentService.loadBackendConfig().pipe(map(() => true));
  }
}

Now we are ready to apply the guard to the main route and we are ready to use backend config.

const routes: Routes = [
  {
    path: '',
    component: AuthViewComponent,
    canActivate: [EnvironmentGuard],
    children: [
      // TODO: Define you routes here
    ]
  }

And that’s it. The old, probably still more used way of configuring line-of-business applications that hurts initial load of the application, but saves you some build minutes or gives confidence that what you build and worked on TEST will work on PROD instance.

Hope it helps!

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published.