Angular Resolver

Цель Resolver - получить асинхронные данные перед отображением компонента. Важный момент в том, что не надо в резолвер помещать всё подряд, особенно если это некритичные долго загружаемые данные. В нём должно быть то, без чего нельзя отобразить компонент. Официальная документация даёт следующий пример:

@Injectable({ providedIn: 'root' })
export class HeroResolver implements Resolve<Hero> {
  constructor(private service: HeroService) {}

  resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<any>|Promise<any>|any {
    // функция получения данных определяется в отдельной сервисе
    return this.service.getHero(route.paramMap.get('id'));
  }
}

@NgModule({
  imports: [
    RouterModule.forRoot([
      {
        path: 'detail/:id',
        component: HeroDetailComponent,
        // использование сервиса в секции resolve
        resolve: {
          hero: HeroResolver
        }
      }
    ])
  ],
  exports: [RouterModule]
})
export class AppRoutingModule {}

@Component({
  selector: "app-hero-detail",
  templateUrl: "./hero-detail.component.html",
})
export class HeroDetailComponent implements OnInit {
  readonly hero$ = this.route.data.pipe(
    map((data: { hero: Hero }) => data.hero)
  );

  constructor(
    private route: ActivatedRoute,
  ) {}
}

Альтернативный и более простой способ - получать данные сразу в компоненте.

@Component({
  selector: "app-hero-detail",
  templateUrl: "./hero-detail.component.html",
})
export class HeroDetailComponent implements OnInit {
  readonly hero$ = this.route.paramMap.pipe(
    switchMap((params: ParamMap) => this.service.getHero(params.get("id")))
  );

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private service: HeroService
  ) {}
}

Однако привидённый пример весьма абстрактен. В реальном приложении UX требует показывать пользователю обратную связь, обычно это spinner во время загрузки. В альтернативном варианте опуская нюансы spinner показать несложно:

<div *ngIf="hero$ | async as hero; else loading">
  <h3>"{{ hero.name }}"</h3>
</div>
<ng-template #loading><spinner></spinner></ng-template>

В случае с resolver простой вариант показать загрузку я вижу в универсальном индикаторе

На днях вышла статья от ребят из жёлтого банка, где предложен механизм, позволяющий показывать spinner во время загрузки данных, используя резолверы.

Ещё одна частная плюшка

В случае использования hash навигации решается проблема смены id "руками" в адресной строке браузера.

type RunGuardsAndResolvers = 'pathParamsChange' | 'pathParamsOrQueryParamsChange' | 'paramsChange' | 'paramsOrQueryParamsChange' | 'always' | ((from: ActivatedRouteSnapshot, to: ActivatedRouteSnapshot) => boolean);

Сталкивался с кодом, когда в OnInit компонента была инициализация, которая не срабатывала т.к. Angular переиспользовал существющий компонент. В итоге получился костылик с *ngIf="!needReload" в родительском компоненте на <router-outlet>, который в нужный момент для пересоздания компонента делал:

  private reloadChildComponent(): void {
    this.needReload = true;
    this.cdr.detectChanges();
    this.needReload = false;
    this.cdr.markForCheck();
  }

Похожие записи

RxJS Pipeable Operators

Начиная с версии rxjs 5.5 операторы вместо цепочки вызовов применяются как параметры функции pipe.