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


Réponses:


196

Le routeur angulaire actuel fournit des événements de navigation. Vous pouvez vous y abonner et apporter des modifications à l'interface en conséquence. N'oubliez pas de compter dans d'autres événements tels que NavigationCancelet NavigationErrord'arrêter votre spinner 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) {
    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 - 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>

Réponse améliorée : Si vous vous souciez des performances, il existe une meilleure méthode, elle est légèrement plus fastidieuse à mettre en œuvre, mais l'amélioration des performances vaudra le travail supplémentaire. Au lieu d'utiliser *ngIfpour afficher conditionnellement le spinner, nous pourrions tirer parti de Angular NgZoneet Rendereractiver / désactiver le spinner, ce qui contournera la détection de changement d'Angular lorsque nous changerons l'état du spinner. J'ai trouvé cela pour rendre l'animation plus fluide par rapport à l'utilisation *ngIfou à unasync tuyau.

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

1
Super, j'apprécie votre approche. Cela m'a déjà frappé et j'ai remplacé ma longue méthode par cette idée séduisante, que j'ai dû revenir parce que j'avais perdu le contrôle router navigationet que je it triggering the spinnerne pouvais pas l'arrêter. navigationInterceptorsemble être une solution, mais je ne sais pas si quelque chose d'autre attend de se casser. S'il est mélangé avec le, async requestsil introduira à nouveau le problème, je pense.
Ankit Singh

1
Peut-être que cela a fonctionné à un moment donné? Cela ne fonctionne pas maintenant avec Angular 2.4.8. La page est synchrone. Le Spinner n'est pas rendu tant que la page entière / le composant parent n'est pas rendu, et que le at NavigationEnd. Cela bat le point du spinner
Techguy2000

1
Utiliser l'opacité n'est pas un excellent choix. Visuellement, c'est bien, mais dans Chrome, si l'image / l'icône de chargement se trouve au-dessus d'un bouton ou d'un texte, vous ne pouvez pas accéder au bouton. Je l'ai changé pour utiliser displaysoit noneou inline.
im1dermike

2
J'aime cette approche, bon travail homme! Mais j'ai un problème et je ne comprends pas pourquoi, si je ne change pas d'animation sur NavigationEnd, je peux voir le chargement du spinner, mais si je passe à false, les routes changent si vite que je ne peux même pas voir d'animations: (J'ai essayé avec même le throteling du réseau pour ralentir la connexion mais cela reste le même: (pas de chargement du tout. Pourriez-vous me donner des suggestions à ce sujet s'il vous plaît. Je contrôle les animations en ajoutant et en supprimant une classe à l'élément de chargement. Merci
d123546

1
J'ai eu cette erreur lorsque j'ai copié votre code 'md-spinner' is not a known element:. Je suis assez nouveau sur Angular. Pourriez-vous s'il vous plaît me dire quelle pourrait être l'erreur?
Manu Chadha

40

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

UPDATE2 : Les événements de routeur dans le nouveau routeur semblent prometteurs et la réponse de @borislemke semble couvrir l'aspect principal de l'implémentation de spinner, je ne l'ai pas testé mais je le recommande.

UPDATE1: J'ai écrit cette réponse à l'époque de Old-Router, quand il n'y avait qu'un seul événement route-changednotifié via router.subscribe(). J'ai également ressenti une surcharge de l'approche ci-dessous et j'ai essayé de le faire en utilisant uniquement router.subscribe(), et cela s'est retourné contre lui parce qu'il n'y avait aucun moyen de détecter canceled navigation. J'ai donc dû revenir à une approche longue (double travail).


Si vous connaissez votre chemin dans 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 (s'abonnera au service Spinner pour changer la valeur d'actif 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 une observable à souscrire par composant spinner pour changer le statut en cas de changement, et fonction pour connaître et définir le spinner 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();
  }
}

voyez ceci aussi. Je ne sais pas dans quelle mesure l'animation prend en charge angulaire par défaut.
Ankit Singh

pouvez-vous m'aider à rédiger le service pour le spinner? Je peux faire moi-même les animations et tout le reste en CSS, mais ce serait bien si vous pouviez m'aider avec le service.
Lucas

voir cette implémentation aussi, même mais pas exactement
Ankit Singh

1
J'ai ajouté du code pour l' spinner-serviceinstant, vous avez juste besoin d'autres parties pour le faire fonctionner. Et rappelez-vous que c'est pourangular2-rc-1
Ankit Singh

1
en fait, c'est génial et fonctionne, mon conseil à des fins de test vous permet de retarder l'arrêt du spinner avec setTimeout (() => this.spinner.stop (), 5000) dans ngOnInit
Jhonatas Kleinkauff

10

Pourquoi ne pas simplement utiliser un simple CSS:

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

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

L'astuce ici est que les nouvelles routes et composants apparaîtront toujours après la sortie du routeur , donc avec un simple sélecteur css, nous pouvons afficher et masquer le chargement.


<router-outlet> ne nous permet pas de passer la valeur du composant au composant parent, il sera donc un peu complexe de masquer le div de chargement.
Praveen Rana

1
De plus, il peut être très ennuyeux si votre application a beaucoup de changements d'itinéraire pour voir le spinner à chaque fois instantanément. Il est préférable d'utiliser RxJs et de définir une minuterie anti-rebond pour qu'elle n'apparaisse qu'après un court délai.
Simon_Weaver

2

Si une logique spéciale est requise pour la première 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 masquer le pied de page de ma page, qui apparaissait autrement en haut de la page. De plus, si vous souhaitez uniquement un chargeur pour la première page, vous pouvez l'utiliser.


0

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


cela aide-t-il les résolveurs? Mon problème est que bien que les résolveurs ramènent les données, je ne peux pas afficher le spinner nulle part car le composant cible réel ngOninit n'a pas encore été appelé !! Mon idée était d'afficher le spinner dans ngOnInit et de le masquer une fois que les données résolues sont renvoyées de l'abonnement à l'itinéraire
kuldeep
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.