web-dev-qa-db-fra.com

Afficher l'écran de chargement lors de la navigation entre les itinéraires dans Angular 2

Comment afficher un écran de chargement lorsque je modifie un itinéraire dans Angular 2?

102
think123

Le routeur angulaire actuel fournit des événements de navigation. Vous pouvez vous y abonner et apporter des modifications à l'interface utilisateur en conséquence. N'oubliez pas de compter dans d'autres événements tels que NavigationCancel et NavigationError pour arrêter votre compteur en cas d'échec des transitions de routeur.

app.component.ts - votre composant racine

...
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 - votre vue racine

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

Performance Improved Answer : Si vous vous souciez de la performance, il existe une meilleure méthode, elle est légèrement plus fastidieuse à mettre en œuvre, mais l’amélioration de la performance vaudra le travail supplémentaire. Au lieu d'utiliser *ngIf pour afficher conditionnellement le compteur, nous pourrions utiliser les noms NgZone et Renderer d'Angular pour activer/désactiver le compteur, ce qui contournera la détection de changement par Angular lorsque nous changerons son statut. J'ai trouvé cela pour rendre l'animation plus fluide par rapport à l'utilisation de *ngIf ou d'un tube async.

Ceci est similaire à ma réponse précédente avec quelques ajustements:

app.component.ts - votre composant racine

...
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 - votre vue racine

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

UPDATE: 3 Maintenant que j'ai mis à niveau le nouveau routeur, l'approche de @borislemke ne fonctionnera pas si vous utilisez CanDeactivate guard. Je me dégrade à mon ancienne méthode, ie: cette réponse

UPDATE2: les événements de routeur dans new-router semblent prometteurs et la réponse by @borislemke semble couvrir l'aspect principal de la mise en œuvre de spinner, je ne l'ai pas encore testée, mais je le recommande.

UPDATE1: J'ai écrit cette réponse à l'ère de Old-Router, lorsqu'il n'y avait qu'un seul événement route-changed notifié via router.subscribe(). Je me sentais également surchargé par l’approche ci-dessous et j’essayais de le faire en utilisant uniquement router.subscribe(), et il retournait contre moi car il n’existait aucun moyen de détecter canceled navigation. J'ai donc dû revenir à une approche longue (double travail).


Si vous connaissez Angular2, c’est ce dont vous aurez besoin.


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

Composant racine - (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 (sera abonné à Spinner-service pour modifier la valeur de active en conséquence)

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 (amorcez ce service)

Définissez un observable à souscrire par composant spinner pour changer le statut lors du changement, et fonction pour connaître et définir le spinner comme actif/inactif.

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

Composants de tous les autres itinéraires 

(échantillon):

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();
  }
}
38
Ankit Singh

Pourquoi ne pas simplement utiliser des CSS simples: 

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

Et dans vos styles: 

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

Ou même nous pouvons le faire pour la première réponse: 

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

Et puis tout simplement 

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

Le truc ici est que les nouveaux itinéraires et composants apparaîtront toujours après router-outlet , donc avec un simple sélecteur CSS, nous pouvons afficher et masquer le chargement.

10
Milad

Si vous avez une logique spéciale requise pour la first route uniquement, vous pouvez effectuer les opérations suivantes:

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

J'avais besoin de cela pour cacher mon pied de page, qui par ailleurs était affiché en haut de la page. Aussi, si vous voulez seulement un chargeur pour la première page, vous pouvez l'utiliser.

1
Simon_Weaver

Vous pouvez également utiliser cette solution existante . La démo est ici . Cela ressemble à la barre de chargement youtube. Je viens de le trouver et de l'ajouter à mon propre projet.

0
walkerbox