Angualr - resolver, canActivate and passing data to component

Usually, I use a resolver for the route path to make sure that data from the provider (like API) are available before the component is shown to the user. I like this approach rather than handling data retrieval under the OnInit method and later ensuring data availability under the template.

The resolver may be implemented in the following way:

@Injectable({ providedIn: "root" })
export class MetaDataResolver implements Resolve<MetaDataDto> {
  constructor(private metaDataService: MetaDataService) {}
  resolve(route: ActivatedRouteSnapshot): Observable<MetaDataDto> {
    return this.metaDataService.getMetaData();
}

Nothing fancy here. We have to implement an interface, inject our service for API communication and use it to retrieve the data.

Now, from the component level, all we need to do is subscribe to the route data.

export class MetaDataComponent {
  constructor(private route: ActivatedRoute) {
    this.route.data.subscribe((data: { metaData: MetaDataDto }) => {
      this.metaData = data.metaData;
    });
  }
  private metaData: MetaDataDto;
}

Recently I had to make sure that the route path will be activated only when API can manage to provide the data. To do that we can use a canActivate guard that either allows or denies access to the specific route. Below is the basic implementation for the described scenario.

@Injectable()
export class MetaDataGuard implements CanActivate {
  constructor(private metaDataService: MetaDataService) {}

  canActivate(next: ActivatedRouteSnapshot): Observable<boolean> {
    return this.metaDataService.getMetaData().pipe(
      mergeMap((result) => {
        if(result.dataAvailable === false) {
          return of(false);
        }
        return of(true);
      }),
    );
  }

Having implementation of canActivate and resolver we can configure the routes under the module.

const routes: Routes = [
  {
    path: CommonRoutes.MetaData,
    component: MetaDataComponent,
    canActivate: [MetaDataGuard],
    resolve: {
      metaData: MetaDataResolver,
    },
  },
];

@NgModule({
  imports: [
    RouterModule.forChild(routes),
  ],
  providers: [metaDataService],
  declarations: [MetaDataComponent],
  exports: [MetaDataComponent],
})

That's it. First, the canActivate method will be executed for the route path. If data are available from the API, the route path will be activated and then the resolver executed. Finally, we can consume data under the constructor of the component.

The problem is that we have to call the API twice for the same data. Sure, we could extend our API to return the status of the data availability, but I wanted to try something else. Apart from that the API itself might be closed for extensions. I was wondering whether it is possible to call only one of the methods: canActivate or resolver, to handle both: route activation and data retrieval.

This can be easily done with a few small modifications. After reading the documentation it looks like canActivate is called before the resolver. This helped me to make a decision - I had to get rid of the resolver. Route activation and fetching the data need to be done under the canActivate.

@Injectable()
export class MetaDataGuard implements CanActivate {
  constructor(private metaDataService: MetaDataService) {}

  canActivate(next: ActivatedRouteSnapshot): Observable<boolean> {
    return this.metaDataService.getMetaData().pipe(
      mergeMap((result) => {
        if (this.isMetaDataRoutePath(next) === false) {
          console.log("Cannot activate for route path other than metadata.");
          return of(false);
        }

        if (nextRoutePath === undefined && nextRoutePath === null) {
          return of(false);
        }

        if(result.dataAvailable === false) {
          return of(false);
        }

        next.data = { ...next.data, metaData: result };
        return of(true);
      }),
    );
  }

  private isMetaDataRoutePath(next: ActivatedRouteSnapshot): boolean {
    const nextRoutePath = next.routeConfig.path;
    if (nextRoutePath !== undefined && nextRoutePath !== null 
        && nextRoutePath === CommonRoutes.MetaData) {
      return true;
    }
    return false;
  }
}

Now, a few words of explanation.

if (this.isMetaDataRoutePath(next) === false)

I wanted to make sure that my implementation of canActivate guard for metadata can be used only for the specific route - to not allow someone to use it with a different route.

if(result.dataAvailable === false) {
    return of(false);
}

Still checking whether the API can deliver data before we activate the route.

next.data = { ...next.data, metaData: result };

Here I'm extending the route data by additional data retrieved from the API - similar to the operation done under the resolver.

From the component level, nothing changed:

export class MetaDataComponent {
  constructor(private route: ActivatedRoute) {
    this.route.data.subscribe((data: { metaData: MetaDataDto }) => {
      this.metaData = data.metaData;
    });
  }
  private metaData: MetaDataDto;
}

The last step would be the removal of the resolver from the route configuration.

const routes: Routes = [
  {
    path: CommonRoutes.MetaData,
    component: MetaDataComponent,
    canActivate: [MetaDataGuard],
  },
];

That's it. It is not my goal to state that we don't need resolver and we can rely only on canActivate implementation. The described steps just seemed to be a good solution for particular use cases that I had to deal with.