angular
February 28

Пример реализации прелоадера в Angular

Рассмотрим, как можно реализовать прелоадер в Angular и доступные варианты реализации.

Для чего это нужно?

Для начала стоит определить основные функции, которые хочется видеть в компоненте загрузки:

  • Должна быть возможность в любой момент и из любой части приложения реализовать переключение состояния загрузки
  • Прелоадер должен отрабатывать при переходе между маршрутами.
  • Прелоадер будет показан автоматически во время выполнение запросов на бекенд. Для определенных HTTP-запросов мы должны иметь возможность не включать индикатор загрузки, например, если мы хотим незаметно запустить запрос в фоне без уведомления пользователя
  • Прелоадер реализуется с использованием UI-библиотеки Angular Material.

Из чего состоит Прелоадер:

  • Компонент. Рисуется на форме
  • Сервис. Управляет состоянием компонента

Приступим.

1. Сервис

Создадим сервис, который будет использоваться для смены состояния индикатора загрузки. Основная цель - позволить контролировать поведение индикатора загрузки даже из компонента, который не имеет прямого доступа к компоненту загрузки.

@Injectable({
  providedIn: "root",
})
export class LoadingService {
  private loadingSubject = 
    new BehaviorSubject<boolean>(false);

  loading$ = this.loadingSubject.asObservable();

  loadingOn() {
    this.loadingSubject.next(true);
  }

  loadingOff() {
    this.loadingSubject.next(false);
  }
}

Разберем код по шагам:

  • BehaviorSubject loadingSubject используется для хранения текущего состояния индикатора загрузки.
  • Из вне Subject недоступен, соблюдаем принцип открытости-закрытости. Для чтения создаем обычный Observable.
  • Созданы два метода для изменения состояния прелоадера.


Этот сервис объявлен глобально как синглтон, поэтому его можно внедрить в любое место приложения и использовать для управления глобальным индикатором загрузки.

2. Компонент

Теперь займемся компонентом. Для создания индикатора используем прелоадер Angular Material.

Давайте сначала покажем полный код компонента, а затем разберем его по шагам.

Начнем со стилей:

.spinner-container {
  position: fixed;
  height: 100%;
  width: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  top: 0;
  left: 0;
  background: rgba(0, 0, 0, 0.32);
  z-index: 2000;
}

Класс CSS spinner-container будет использоваться для стилизации фона прелоадера.

Теперь давайте посмотрим на шаблон компонента загрузки:

@if(loading$ | async) {

<div class="spinner-container">
  @if(customLoadingIndicator) {
  <ng-container 
    *ngTemplateOutlet="customLoadingIndicator" />
  } @else {
    <mat-spinner />
  }
</div>
}
Код совместим только с версиями Angular 17+

Разберем код шаблона по шагам:

  • Индикатор загрузки будет отображаться только в том случае, если loading$ выдает значение true
  • Интерфейс пользователя для индикатора загрузки - это компонент <mat-spinner />, предоставленный Angular Material
  • Есть возможность предоставить пользовательский интерфейс для компонента, предоставив шаблон с именем customLoadingIndicator компоненту через проекцию контента


Далее посмотрим на класс компонента индикатора загрузки:

@Component({
  selector: "loading-indicator",
  templateUrl: "./loading-indicator.component.html",
  styleUrls: ["./loading-indicator.component.scss"],
  imports: [MatProgressSpinnerModule, AsyncPipe, NgIf, NgTemplateOutlet],
  standalone: true,
})
export class LoadingIndicatorComponent implements OnInit {

  loading$: Observable<boolean>;

  @Input()
  detectRouteTransitions = false;

  @ContentChild("loading")
  customLoadingIndicator: TemplateRef<any> | null = null;

  constructor(
  private loadingService: LoadingService, 
  private router: Router) {
    this.loading$ = this.loadingService.loading$;
  }

  ngOnInit() {
    if (this.detectRouteTransitions) {
      this.router.events
        .pipe(
          tap((event) => {
            if (event instanceof RouteConfigLoadStart) {
              this.loadingService.loadingOn();
            } else if (event instanceof RouteConfigLoadEnd) {
              this.loadingService.loadingOff();
            }
          })
        )
        .subscribe();
    }
  }
}

Глобальное использование индикатора загрузки

Благодаря сервису мы можем использовать прелоадер из любой части приложения. Первое, что нам нужно сделать, это настроить прелоадер в корневом компоненте приложения.

Представьте, что это ваш корневой компонент app.component.html:

<ul>
  <li><a routerLink="/contact">Contact</a></li>
  <li><a routerLink="/help">Help</a></li>
  <li><a routerLink="/about">About</a></li>
</ul>

<router-outlet />

<loading-indicator />

Виджет загрузки на месте и по умолчанию отключен. Теперь представим, что нам нужно включить и выключить loading из дочернего компонента, который не имеет прямого доступа к компоненту прелоадер.

Асинхронное использование

Все, что нам нужно сделать, это внедрить сервис загрузки в компонент и вызвать методы loadingOn и loadingOff:

@Component({
  selector: "child-component",
  standalone: true,
  imports: [CommonModule],
  template: ` 
  <button (click)="onLoadCourses()">
  Load Courses
  </button> `,
})
export class ChildComponentComponent {
  constructor(private loadingService: LoadingService) {}

  onLoadCourses() {
    try {
      this.loadingService.loadingOn();

      // load courses from backend
    } catch (error) {
      // handle error message
    } finally {
      this.loadingService.loadingOff();
    }
  }
}


В методе onLoadCourses используется шаблон обработки индикаторов загрузки во время запросов к бэкэнду. Сначала включается loading, затем выполняется запрос, и после этого loading отключается. Отключение выполнено в блоке finally, чтобы гарантировать его отключение даже в случае ошибки.

Создаем Interceptor

Чтобы избежать повторения этого кода везде, можно использовать Http Interceptor для автоматического включения и отключения индикатора загрузки для каждого HTTP-запроса.

export const SkipLoading = 
  new HttpContextToken<boolean>(() => false);

@Injectable()
export class LoadingInterceptor 
    implements HttpInterceptor {
  constructor(private loadingService: LoadingService) {
  }

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    // Check for a custom attribute 
    // to avoid showing loading spinner
    if (req.context.get(SkipLoading)) {
      // Pass the request directly to the next handler
      return next.handle(req);
    }

    // Turn on the loading spinner
    this.loadingService.loadingOn();

    return next.handle(req).pipe(
      finalize(() => {
        // Turn off the loading spinner
        this.loadingService.loadingOff();
      })
    );
  }
}

А так же зарегистрируем Interceptor:

bootstrapApplication(AppComponent, {
  providers: [
    importProvidersFrom(
      BrowserModule,
      AppRoutingModule,
      RouterModule,
      LoadingService
    ),
    {
      provide: HTTP_INTERCEPTORS,
      useClass: LoadingInterceptor,
      multi: true,
    },
  ],
});

Для регистрации этого перехватчика в приложении необходимо использовать { multi: true }, чтобы инжектируемый токен возвращал массив Interceptors, а не только один.

Interceptor просто создает клон исходного запроса, включает индикатор загрузки и отключает его с помощью оператора finalize, чтобы индикатор загрузки скрывался только после завершения или ошибки HTTP-запроса.

Отключение прелоадера для отдельных запросов

Мы можем отключить индикатор загрузки для отдельного HTTP-запроса, установив HttpContextToken SkipLoading в true. Это может быть полезно в сценариях, таких как периодическое фоновое опрос сервера, когда мы не хотим показывать индикатор каждый раз. Например, при обновлении данных диаграммы каждые 10 секунд мы можем пропустить отображение индикатора. Вот как это реализовать:

this.http.get("/api/courses", {
  context: new HttpContext().set(SkipLoading, true),
});

Интеграция индикатора загрузки с маршрутизатором

Другая распространенная функция, которую мы часто хотим внедрить, - это показ индикатора загрузки во время переходов между маршрутами. Для этого используется свойство detectRouteTransitions. Если установить его в true, индикатор загрузки будет переключен автоматически при изменении маршрута.

Вот как это реализуется в корневом компоненте приложения:

<loading-indicator 
    [detectRouteTransitions]="true" />

Альтернативный UI

Компонент загрузки также поддерживает возможность предоставления альтернативного интерфейса. Если Angular Material не подходит для наших целей, то можно передать любой шаблон с компонентом который нужен:

<loading-indicator>
  <ng-template #loading>
    <div class="custom-spinner">
      <img src="custom-spinner.gif" />
    </div>
  </ng-template>
</loading-indicator>

В случае предоставления шаблона, он будет отображаться взамен Angular Material. Шаблон должен иметь имя loading для корректной работы.

Компонент индикатора загрузки сначала проверяет наличие пользовательского шаблона с помощью декоратора @ContentChild.

@ContentChild("loading")
customLoadingIndicator: TemplateRef<any> | null = null


Если пользователь предоставил шаблон с именем loading, то он будет выведен взамен компонента Angular Material.

@if(customLoadingIndicator) {

<ng-container *ngTemplateOutlet="customLoadingIndicator" />

} @else {

<mat-spinner />

}

Это вольный перевод оригинального материала