Pokaż ekran ładowania podczas nawigacji między trasami w Angular 2

Odpowiedzi:

196

Obecny router Angular zapewnia zdarzenia nawigacji. Możesz je zasubskrybować i odpowiednio zmienić interfejs użytkownika. Pamiętaj, aby liczyć w innych wydarzeniach, takich jak NavigationCanceliNavigationError zatrzymać swój spinner w przypadku niepowodzenia przejść między routerami.

app.component.ts - Twój główny komponent

...
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) {
    this.router.events.subscribe((e : RouterEvent) => {
       this.navigationInterceptor(e);
     })
  }

  // 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 - Twój widok główny

<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>

Poprawiona wydajność Odpowiedź : Jeśli zależy Ci na wydajności, istnieje lepsza metoda, która jest nieco bardziej żmudna do wdrożenia, ale poprawa wydajności będzie warta dodatkowej pracy. Zamiast używać *ngIfdo warunkowego pokazywania tarczy, moglibyśmy wykorzystać Angular's NgZonei Rendererwłączyć / wyłączyć tarczę, która obejdzie wykrywanie zmiany Angulara, gdy zmienimy stan tarczy. Okazało się, że to sprawia, że ​​animacja jest płynniejsza w porównaniu z użyciem *ngIflubasync rury.

Jest to podobne do mojej poprzedniej odpowiedzi z kilkoma poprawkami:

app.component.ts - Twój główny komponent

...
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(this._navigationInterceptor)
  }

  // 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 - Twój widok główny

<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>
borislemke
źródło
1
Świetnie, doceniam twoje podejście. To uderzyło mnie przed i wymieniłem moje długie metody z tej atrakcyjnej idei, niż miałem powrócić becuase straciłem kontrolę nad router navigationi it triggering the spinner, a następnie nie jest w stanie go powstrzymać. navigationInterceptorwydaje się rozwiązaniem, ale nie jestem pewien, czy coś innego czeka, aby się zepsuć. async requestsMyślę, że zmieszanie z tym ponownie spowoduje problem.
Ankit Singh
1
Może to zadziałało w pewnym momencie? Teraz nie działa z Angular 2.4.8. Strona jest synchroniczna. Spinner nie jest renderowany, dopóki nie zostanie wyrenderowany cały składnik strony / nadrzędny, a at NavigationEnd. To pokonuje
cel spinnera
1
Używanie krycia nie jest dobrym wyborem. Wizualnie jest dobrze, ale w przeglądarce Chrome, jeśli obraz / ikona ładowania znajduje się nad przyciskiem lub tekstem, nie możesz uzyskać dostępu do przycisku. Zmieniłem to na użycie displayalbo nonelub inline.
im1dermike
2
Podoba mi się to podejście, dobra robota stary! Ale mam problem i nie rozumiem dlaczego, jeśli nie włączę animacji na NavigationEnd, widzę ładowanie tarczy, ale jeśli przełączę na fałsz, trasy zmieniają się tak szybko, że nie mogę nawet zobaczyć żadnych animacji :( Próbowałem nawet sieć trotelingowa w celu spowolnienia połączenia, ale nadal pozostaje taka sama :( brak ładowania w ogóle. Czy możesz podać mi jakieś sugestie na ten temat. Kontroluję animacje, dodając i usuwając klasę w elemencie ładującym. Dzięki
d123546
1
Otrzymałem ten błąd podczas kopiowania twojego kodu 'md-spinner' is not a known element:. Jestem całkiem nowy w Angular. Czy możesz mi powiedzieć, jaki może być błąd?
Manu Chadha
40

AKTUALIZACJA: 3 Teraz, gdy zaktualizowałem router do nowego, podejście @borislemke nie będzie działać, jeśli używasz CanDeactivateguard. Poniżam moją starą metodęie: ta odpowiedź

Update2 wydarzenia router nowego routera i wyglądają obiecująco: odpowiedź przez @borislemke wydaje się obejmować główny aspekt realizacji wirowania, ja havent't przetestowane ale polecam go.

UPDATE1: Napisałem tę odpowiedź w erze Old-Router, kiedy było tylko jedno zdarzenie route-changedpowiadamiane przez router.subscribe(). Czułem również przeciążenie poniższego podejścia i próbowałem to zrobić używając tylko router.subscribe(), ale przyniosło odwrotny skutek, ponieważ nie było sposobu na wykrycie canceled navigation. Musiałem więc wrócić do długiego podejścia (podwójna praca).


Jeśli znasz się na Angular2, to jest to, czego potrzebujesz


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]);

Główny składnik - (MojaAplikacja)

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 (subskrybuje usługę Spinner, aby odpowiednio zmienić wartość aktywnego)

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;
    });
  }
}

Usługa Spinner (uruchom tę usługę)

Zdefiniuj obserwowalny, który ma być subskrybowany przez składnik pokrętła, aby zmienić stan w przypadku zmiany, i użyj funkcji, aby poznać i ustawić pokrętło jako aktywne / nieaktywne.

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;
  }
}

Składniki wszystkich innych tras

(próba):

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();
  }
}
Ankit Singh
źródło
zobacz to również. Nie wiem, ile Animation obsługuje domyślnie kątowe.
Ankit Singh
czy możesz mi pomóc napisać usługę dla błystki? Mogę sam wykonać animacje i inne elementy w CSS, ale byłoby miło, gdybyś mógł mi pomóc z usługą.
Lucas
zobacz również tę implementację , to samo, ale nie dokładnie
Ankit Singh
1
Dodałem trochę kodu, na spinner-servicerazie potrzebujesz tylko innych części, aby działał. I pamiętaj, że to dlaangular2-rc-1
Ankit Singh
1
w rzeczywistości jest to niesamowite i działa, moja wskazówka do celów testowych możesz opóźnić zatrzymanie przędzarki za pomocą setTimeout (() => this.spinner.stop (), 5000) w ngOnInit
Jhonatas Kleinkauff
10

Dlaczego nie po prostu użyć prostego CSS:

<router-outlet></router-outlet>
<div class="loading"></div>

I w twoich stylach:

div.loading{
    height: 100px;
    background-color: red;
    display: none;
}
router-outlet + div.loading{
    display: block;
}

Lub nawet możemy to zrobić dla pierwszej odpowiedzi:

<router-outlet></router-outlet>
<spinner-component></spinner-component>

A potem po prostu

spinner-component{
   display:none;
}
router-outlet + spinner-component{
    display: block;
}

Sztuczka polega na tym, że nowe trasy i komponenty będą zawsze pojawiać się po wyjściu routera , więc za pomocą prostego selektora css możemy pokazać i ukryć ładowanie.

Milad
źródło
<router-outlet> nie pozwala nam na przekazywanie wartości z komponentu do komponentu nadrzędnego, więc ukrycie ładującego się elementu div będzie nieco skomplikowane.
Praveen Rana
1
Może to być również bardzo denerwujące, jeśli aplikacja ma wiele zmian trasy, aby natychmiast zobaczyć spinner za każdym razem. Lepiej jest użyć RxJs i ustawić zdemontowany licznik czasu, aby pojawił się dopiero po krótkim czasie.
Simon_Weaver,
2

Jeśli masz specjalną logikę wymaganą tylko dla pierwszej trasy, możesz wykonać następujące czynności:

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');
                   });

Musiałem to ukryć stopkę mojej strony, która w przeciwnym razie pojawiałaby się u góry strony. Również jeśli chcesz ładować tylko pierwszą stronę, możesz tego użyć.

Simon_Weaver
źródło
0

Możesz również skorzystać z tego istniejącego rozwiązania . Demo jest tutaj . Wygląda jak pasek ładowania youtube. Po prostu go znalazłem i dodałem do swojego projektu.

Jonatan Dragon
źródło
czy to pomaga w resolwerach? Mój problem polega na tym, że chociaż resolwery przywracają dane, nie mogę nigdzie pokazać przędzarki, ponieważ rzeczywisty komponent docelowy ngOninit nie został jeszcze wywołany !! Moim pomysłem było pokazanie spinnera w ngOnInit i ukrycie go, gdy rozwiązane dane zostaną zwrócone z subskrypcji trasy
kuldeep