Mostrar pantalla de carga al navegar entre rutas en Angular 2

Respuestas:

196

El enrutador angular actual proporciona eventos de navegación. Puede suscribirse a estos y hacer cambios en la interfaz de usuario en consecuencia. Recuerde contar en otros eventos como NavigationCancely NavigationErrorpara detener su ruleta en caso de que falle la transición del enrutador.

app.component.ts : su componente raíz

...
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 : su vista raíz

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

Respuesta de rendimiento mejorado : si le importa el rendimiento, existe un método mejor, es un poco más tedioso implementarlo, pero la mejora del rendimiento valdrá la pena el trabajo extra. En lugar de usar *ngIfpara mostrar condicionalmente la ruleta, podríamos aprovechar Angular's NgZoney Rendererencender / apagar la ruleta que evitará la detección de cambios de Angular cuando cambiemos el estado de la ruleta. Encontré esto para hacer que la animación sea más suave en comparación con el uso *ngIfo una asynctubería.

Esto es similar a mi respuesta anterior con algunos ajustes:

app.component.ts : su componente raíz

...
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 : su vista raíz

<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
fuente
1
Genial, aprecio tu enfoque. Se me ha golpeado antes y reemplazado mi método muy largo con esta idea atractiva, de lo que tenía que volver becuase he perdido el control sobre router navigationy it triggering the spinnery luego no ser capaz de detenerlo. navigationInterceptorParece una solución, pero no estoy seguro de si hay algo más esperando para romperse. Si se mezcla con el async requests, volverá a presentar el problema, creo.
Ankit Singh
1
Tal vez esto funcionó en algún momento? No funciona ahora con Angular 2.4.8. La página es sincrónica. El Spinner no se procesa hasta que se procese toda la página / componente principal, y eso en NavigationEnd. Eso derrota el punto de la ruleta
techguy2000
1
Usar opacidad no es una gran opción. Visualmente, está bien, pero en Chrome, si la imagen / icono de carga está encima de un botón o texto, no puede acceder al botón. Lo cambié para usar displaycualquiera noneo inline.
im1dermike
2
Me gusta este enfoque, buen trabajo hombre! Pero tengo un problema y no entiendo por qué, si no cambio la animación en NavigationEnd, puedo ver la carga del spinner, pero si cambio a falso, las rutas cambian tan rápido que ni siquiera puedo ver ninguna animación :( He intentado incluso con la red acelerada para ralentizar la conexión, pero sigue siendo la misma :( sin carga en absoluto. ¿Podría darme alguna sugerencia sobre esto? Controle las animaciones agregando y eliminando clases en el elemento de carga. Gracias
d123546
1
Recibí este error cuando copié su código 'md-spinner' is not a known element:. Soy bastante nuevo en Angular. ¿Podría decirme cuál podría ser el error?
Manu Chadha
39

ACTUALIZACIÓN: 3 Ahora que he actualizado al nuevo enrutador, el enfoque de @borislemke no funcionará si usa CanDeactivateguardia. Estoy degradando a mi antiguo método, ie:esta respuesta

ACTUALIZACIÓN2 : Los eventos del enrutador en el nuevo enrutador parecen prometedores y la respuesta de @borislemke parece cubrir el aspecto principal de la implementación del spinner, no lo he probado pero lo recomiendo.

ACTUALIZACIÓN1: Escribí esta respuesta en la era de Old-Router, cuando solía haber solo un evento route-changednotificado a través de router.subscribe(). También sentí una sobrecarga del siguiente enfoque e intenté hacerlo solo router.subscribe(), y resultó contraproducente porque no había forma de detectarlo canceled navigation. Así que tuve que volver a un enfoque largo (doble trabajo).


Si conoce bien Angular2, esto es lo que necesitará.


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

Componente raíz: (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 (se suscribirá a Spinner-service para cambiar el valor de active en consecuencia)

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 (arranque este servicio)

Defina un observable para ser suscrito por el componente spinner para cambiar el estado del cambio, y funcione para conocer y configurar el spinner activo / inactivo.

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

Todos los componentes de otras rutas

(muestra):

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
fuente
Mira esto también. No sé cuánta animación admite angular de forma predeterminada.
Ankit Singh
¿Me pueden ayudar a escribir el servicio para la ruleta? Yo mismo puedo hacer las animaciones y otras cosas en CSS, pero sería bueno que me ayudaras con el servicio.
Lucas
vea esta implementación también, igual pero no exactamente
Ankit Singh
1
He agregado un código por spinner-serviceahora, solo necesita otras partes para que funcione. Y recuerda que es paraangular2-rc-1
Ankit Singh
1
en realidad, esto es increíble y funciona, mi consejo para fines de prueba puede retrasar la parada de la ruleta con setTimeout (() => this.spinner.stop (), 5000) en ngOnInit
Jhonatas Kleinkauff
10

¿Por qué no simplemente usando CSS simple:

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

Y en tus estilos:

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

O incluso podemos hacer esto para la primera respuesta:

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

Y luego simplemente

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

El truco aquí es que las nuevas rutas y componentes siempre aparecerán después de la salida del enrutador , por lo que con un simple selector css podemos mostrar y ocultar la carga.

Milad
fuente
<router-outlet> no nos permite pasar el valor del componente al componente padre, por lo que será un poco complejo ocultar el div de carga.
Praveen Rana
1
También puede ser súper molesto si su aplicación tiene muchos cambios de ruta para ver la ruleta cada vez al instante. Es mejor usar RxJs y configurar un temporizador sin rebote para que solo aparezca después de un breve retraso.
Simon_Weaver
2

Si solo necesita una lógica especial para la primera ruta, puede hacer lo siguiente:

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

Tenía la necesidad de que esto ocultara el pie de página, que aparecía en la parte superior de la página. Además, si solo desea un cargador para la primera página, puede usarlo.

Simon_Weaver
fuente
0

También podría usar esta solución existente . La demo está aquí . Parece la barra de carga de youtube. Lo encontré y lo agregué a mi propio proyecto.

Jonatan Dragon
fuente
¿Ayuda con los solucionadores? ¡Mi problema es que, aunque los resolutores recuperan los datos, no puedo mostrar la ruleta en ningún lado porque todavía no se ha llamado al componente objetivo real ngOninit! Mi idea era mostrar el spinner en ngOnInit y ocultarlo una vez que los datos resueltos se devuelven de la suscripción de ruta
kuldeep