Mostrar tela de carregamento ao navegar entre rotas no Angular 2

Respostas:

196

O atual roteador angular fornece eventos de navegação. Você pode assinar e fazer alterações na interface do usuário de acordo. Lembre-se de contar em outros eventos, como NavigationCancele NavigationErrorpara interromper seu controle giratório, caso as transições do roteador falhem.

app.component.ts - seu componente raiz

...
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 - sua visualização raiz

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

Resposta aprimorada do desempenho : Se você se importa com o desempenho, existe um método melhor, é um pouco mais tedioso de implementar, mas a melhoria do desempenho valerá o trabalho extra. Em vez de usar *ngIfpara mostrar condicionalmente o botão giratório, poderíamos alavancar o Angular NgZonee Rendererativar / desativar o botão giratório, que ignorará a detecção de alterações do Angular quando mudarmos o estado do botão giratório. Achei isso para tornar a animação mais suave em comparação com o uso de *ngIfum asynccachimbo.

Isso é semelhante à minha resposta anterior com alguns ajustes:

app.component.ts - seu componente raiz

...
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 - sua visualização raiz

<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
fonte
1
Ótimo, agradeço sua abordagem. Ele bateu em mim antes e eu substituído meu método demorado com essa idéia atraente, do que eu tinha para reverter becuase eu perdi o controle sobre router navigatione it triggering the spinnere depois não ser capaz de pará-lo. navigationInterceptorparece uma solução, mas não tenho certeza se algo está esperando para ser quebrado. Se misturado com async requestsele, apresentará o problema novamente, eu acho.
Ankit Singh
1
Talvez isso tenha funcionado em algum momento? Agora não está funcionando com o Angular 2.4.8. A página é síncrona. O Spinner não é renderizado até que toda a página / componente pai seja renderizada, e que o em NavigationEnd. Que derrotas a ponto do spinner
techguy2000
1
Usar opacidade não é uma ótima opção. Visualmente, tudo bem, mas no Chrome, se a imagem / ícone de carregamento estiver em cima de um botão ou texto, você não poderá acessar o botão. Eu mudei para usar displayum noneou outro inline.
im1dermike
2
Eu gosto dessa abordagem, bom trabalho, cara! Mas eu tenho um problema e não entendo por que, se não mudar a animação no NavigationEnd, posso ver o carregamento do girador, mas se mudar para falso, as rotas mudam tão rápido que não consigo ver nenhuma animação :( tentei com até mesmo a aceleração da rede para desacelerar a conexão, mas ainda permanece a mesma :( sem carregamento. Você poderia me dar alguma sugestão sobre isso, por favor. Eu controlo as animações adicionando e removendo a classe no elemento de carregamento. Obrigado
d123546
1
Eu recebi esse erro quando copiei seu código 'md-spinner' is not a known element:. Eu sou bastante novo no Angular. Poderia me dizer qual poderia ser o erro?
Manu Chadha
39

ATUALIZAÇÃO: 3 Agora que eu atualizei para o novo roteador, a abordagem do @borislemke não funcionará se você usar o CanDeactivateguarda. Estou degradando o meu método antigo, ie:esta resposta

ATUALIZAÇÃO2 : Os eventos do roteador no novo roteador parecem promissores e a resposta do @borislemke parece cobrir o principal aspecto da implementação do spinner. Não testei, mas recomendo.

UPDATE1: Eu escrevi esta resposta na era de Old-Router, quando costumava haver apenas um eventoroute-changed notificado via router.subscribe(). Também senti sobrecarga da abordagem abaixo e tentei fazê-la usando apenas router.subscribe(), e ela saiu pela culatra porque não havia como detectar canceled navigation. Então eu tive que voltar à abordagem demorada (trabalho duplo).


Se você conhece o Angular2, é disso que precisa


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 raiz (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 (assinará o serviço Spinner para alterar o valor de ativo de acordo)

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

Serviço Spinner (inicie este serviço)

Defina um observável a ser inscrito pelo componente giratório para alterar o status em mudança e função para conhecer e definir o girador ativo / inativo.

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 os outros componentes das rotas

(amostra):

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
fonte
veja isso também. Não sei quanta animação suporta angular por padrão.
Ankit Singh
você pode me ajudar a escrever o serviço para o spinner? Eu posso fazer as animações e outros enfeites em CSS, mas seria bom se você pudesse me ajudar com o serviço.
Lucas
ver este Implementação também, mesmo, mas não exatamente
Ankit Singh
1
Eu adicionei um código por spinner-serviceenquanto, você só precisa de outras partes para fazê-lo funcionar. E lembre-se de que é paraangular2-rc-1
Ankit Singh
1
na verdade, isso é incrível e obras, a minha dica para fins de teste você pode atrasar a paragem do girador com setTimeout (() => this.spinner.stop (), 5000) em ngOnInit
Jhonatas Kleinkauff
10

Por que não usar apenas css simples:

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

E nos seus estilos:

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

Ou ainda podemos fazer isso pela primeira resposta:

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

E então simplesmente

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

O truque aqui é que as novas rotas e componentes sempre aparecerão após a saída do roteador , portanto, com um simples seletor de css, podemos mostrar e ocultar o carregamento.

Milad
fonte
O <router-outlet> não nos permite passar valor do componente para o componente pai; portanto, será um pouco complexo ocultar a div de carregamento.
Praveen Rana
1
Também pode ser super irritante se o seu aplicativo sofrer muitas alterações de rota para ver o botão giratório todas as vezes instantaneamente. É melhor usar RxJs e definir um timer com atraso, para que ele apareça apenas após um pequeno atraso.
Simon_Weaver 17/02/19
2

Se você tiver uma lógica especial necessária apenas para a primeira rota, poderá fazer o seguinte:

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

Eu precisava disso para ocultar o rodapé da minha página, que de outra forma aparecia na parte superior da página. Além disso, se você quiser apenas um carregador para a primeira página, poderá usá-lo.

Simon_Weaver
fonte
0

Você também pode usar esta solução existente . A demo está aqui . Parece barra de carregamento do youtube. Acabei de encontrá-lo e adicioná-lo ao meu próprio projeto.

Jonatan Dragon
fonte
isso ajuda com resolvedores? Meu problema é que, embora os resolvedores retornem os dados, não consigo mostrar o spinner em nenhum lugar porque o componente de destino real ngOninit ainda não foi chamado! Minha idéia era mostrar o spinner no ngOnInit e escondê-lo dados uma vez resolvido é retornado de subscrição rota
kuldeep