Пример реализации прелоадера в 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 /> }
Это вольный перевод оригинального материала