Показать экран загрузки при навигации между маршрутами в Angular 2
Как показать экран загрузки при изменении маршрута в Angular 2?
5 ответов
Текущий угловой маршрутизатор обеспечивает навигационные события. Вы можете подписаться на них и внести соответствующие изменения в пользовательский интерфейс. Не забудьте сосчитать в других событиях, таких как NavigationCancel
а также NavigationError
остановить ваш счетчик в случае сбоя при переходе через маршрутизатор.
app.component.ts - ваш корневой компонент
...
import {
Router,
// import as RouterEvent to avoid confusion with the DOM Event
Event as RouterEvent,
NavigationStart,
NavigationEnd,
NavigationCancel,
NavigationError
} from '@angular/router'
@Component({})
export class AppComponent {
// Sets initial value to true to show loading spinner on first load
loading = true
constructor(private router: Router) {
router.events.subscribe((event: RouterEvent) => {
this.navigationInterceptor(event)
})
}
// Shows and hides the loading spinner during RouterEvent changes
navigationInterceptor(event: RouterEvent): void {
if (event instanceof NavigationStart) {
this.loading = true
}
if (event instanceof NavigationEnd) {
this.loading = false
}
// Set loading state to false in both of the below events to hide the spinner in case a request fails
if (event instanceof NavigationCancel) {
this.loading = false
}
if (event instanceof NavigationError) {
this.loading = false
}
}
}
app.component.html - ваш корневой вид
<div class="loading-overlay" *ngIf="loading">
<!-- show something fancy here, here with Angular 2 Material's loading bar or circle -->
<md-progress-bar mode="indeterminate"></md-progress-bar>
</div>
Ответ с улучшенной производительностью: если вы заботитесь о производительности, есть лучший метод, который немного сложнее реализовать, но повышение производительности будет стоить дополнительной работы. Вместо того, чтобы использовать *ngIf
чтобы условно показать счетчик, мы могли бы использовать Angular's NgZone
а также Renderer
включить / выключить счетчик, который будет обходить обнаружение изменений Angular при изменении состояния счетчика. Я нашел это, чтобы сделать анимацию более плавной по сравнению с использованием *ngIf
или async
труба.
Это похоже на мой предыдущий ответ с некоторыми изменениями:
app.component.ts - ваш корневой компонент
...
import {
Router,
// import as RouterEvent to avoid confusion with the DOM Event
Event as RouterEvent,
NavigationStart,
NavigationEnd,
NavigationCancel,
NavigationError
} from '@angular/router'
import {NgZone, Renderer, ElementRef, ViewChild} from '@angular/core'
@Component({})
export class AppComponent {
// Instead of holding a boolean value for whether the spinner
// should show or not, we store a reference to the spinner element,
// see template snippet below this script
@ViewChild('spinnerElement')
spinnerElement: ElementRef
constructor(private router: Router,
private ngZone: NgZone,
private renderer: Renderer) {
router.events.subscribe((event: RouterEvent) => {
this._navigationInterceptor(event)
})
}
// Shows and hides the loading spinner during RouterEvent changes
private _navigationInterceptor(event: RouterEvent): void {
if (event instanceof NavigationStart) {
// We wanna run this function outside of Angular's zone to
// bypass change detection
this.ngZone.runOutsideAngular(() => {
// For simplicity we are going to turn opacity on / off
// you could add/remove a class for more advanced styling
// and enter/leave animation of the spinner
this.renderer.setElementStyle(
this.spinnerElement.nativeElement,
'opacity',
'1'
)
})
}
if (event instanceof NavigationEnd) {
this._hideSpinner()
}
// Set loading state to false in both of the below events to
// hide the spinner in case a request fails
if (event instanceof NavigationCancel) {
this._hideSpinner()
}
if (event instanceof NavigationError) {
this._hideSpinner()
}
}
private _hideSpinner(): void {
// We wanna run this function outside of Angular's zone to
// bypass change detection,
this.ngZone.runOutsideAngular(() => {
// For simplicity we are going to turn opacity on / off
// you could add/remove a class for more advanced styling
// and enter/leave animation of the spinner
this.renderer.setElementStyle(
this.spinnerElement.nativeElement,
'opacity',
'0'
)
})
}
}
app.component.html - ваш корневой вид
<div class="loading-overlay" #spinnerElement style="opacity: 0;">
<!-- md-spinner is short for <md-progress-circle mode="indeterminate"></md-progress-circle> -->
<md-spinner></md-spinner>
</div>
ОБНОВЛЕНИЕ:3 Теперь, когда я обновился до нового маршрутизатора, подход @borislemke не будет работать, если вы используете CanDeactivate
охранник. Я унижаю свой старый метод, ie:
этот ответ
ОБНОВЛЕНИЕ 2: События маршрутизатора в новом маршрутизаторе выглядят многообещающе, и ответ @borislemke, кажется, покрывает основной аспект реализации счетчика, я не проверял это, но я рекомендую это.
ОБНОВЛЕНИЕ1: я написал этот ответ в эпоху Old-Router
когда раньше было только одно событие route-changed
уведомлено через router.subscribe()
, Я также почувствовал перегрузку нижеприведенного подхода и попытался сделать это, используя только router.subscribe()
и это имело обратный эффект, потому что не было никакого способа обнаружить canceled navigation
, Поэтому мне пришлось вернуться к длительному подходу (двойная работа).
Если вы знаете свой путь в Angular2, это то, что вам нужно
Boot.ts
import {bootstrap} from '@angular/platform-browser-dynamic';
import {MyApp} from 'path/to/MyApp-Component';
import { SpinnerService} from 'path/to/spinner-service';
bootstrap(MyApp, [SpinnerService]);
Корневой компонент- (MyApp)
import { Component } from '@angular/core';
import { SpinnerComponent} from 'path/to/spinner-component';
@Component({
selector: 'my-app',
directives: [SpinnerComponent],
template: `
<spinner-component></spinner-component>
<router-outlet></router-outlet>
`
})
export class MyApp { }
Spinner-Component (подпишется на Spinner-сервис, чтобы соответственно изменить значение active)
import {Component} from '@angular/core';
import { SpinnerService} from 'path/to/spinner-service';
@Component({
selector: 'spinner-component',
'template': '<div *ngIf="active" class="spinner loading"></div>'
})
export class SpinnerComponent {
public active: boolean;
public constructor(spinner: SpinnerService) {
spinner.status.subscribe((status: boolean) => {
this.active = status;
});
}
}
Spinner-Service (начальная загрузка этого сервиса)
Определите наблюдаемое, которое будет подписано спиннер-компонентом, чтобы изменить статус при изменении, и функцию, чтобы узнать и установить спиннер активным / неактивным.
import {Injectable} from '@angular/core';
import {Subject} from 'rxjs/Subject';
import 'rxjs/add/operator/share';
@Injectable()
export class SpinnerService {
public status: Subject<boolean> = new Subject();
private _active: boolean = false;
public get active(): boolean {
return this._active;
}
public set active(v: boolean) {
this._active = v;
this.status.next(v);
}
public start(): void {
this.active = true;
}
public stop(): void {
this.active = false;
}
}
Компоненты всех других маршрутов
(образец):
import { Component} from '@angular/core';
import { SpinnerService} from 'path/to/spinner-service';
@Component({
template: `<div *ngIf="!spinner.active" id="container">Nothing is Loading Now</div>`
})
export class SampleComponent {
constructor(public spinner: SpinnerService){}
ngOnInit(){
this.spinner.stop(); // or do it on some other event eg: when xmlhttp request completes loading data for the component
}
ngOnDestroy(){
this.spinner.start();
}
}
Почему бы просто не использовать простой CSS:
<router-outlet></router-outlet>
<div class="loading"></div>
И в ваших стилях:
div.loading{
height: 100px;
background-color: red;
display: none;
}
router-outlet + div.loading{
display: block;
}
Или даже мы можем сделать это для первого ответа:
<router-outlet></router-outlet>
<spinner-component></spinner-component>
А потом просто
spinner-component{
display:none;
}
router-outlet + spinner-component{
display: block;
}
Хитрость в том, что новые маршруты и компоненты всегда будут появляться после выхода из маршрутизатора, поэтому с помощью простого селектора CSS мы можем показать и скрыть загрузку.
Если у вас есть специальная логика, необходимая только для первого маршрута, вы можете сделать следующее:
AppComponent
loaded = false;
constructor(private router: Router....) {
router.events.pipe(filter(e => e instanceof NavigationEnd), take(1))
.subscribe((e) => {
this.loaded = true;
alert('loaded - this fires only once');
});
Мне нужно было скрыть нижний колонтитул, который в противном случае появлялся вверху страницы. Также, если вам нужен только загрузчик для первой страницы, вы можете использовать это.
Вы также можете использовать это существующее решение. Демо здесь. Это похоже на панель загрузки YouTube. Я просто нашел его и добавил в свой собственный проект.