Angular 2のルート間をナビゲートするときにロード画面を表示する


回答:


196

現在のAngular Routerはナビゲーションイベントを提供しています。これらをサブスクライブして、それに応じてUIを変更できます。以下のような他のイベントでカウントすることを忘れないでくださいNavigationCancelNavigationError遷移が失敗するルータの場合には、あなたのスピナーを停止します。

app.component.ts-ルートコンポーネント

...
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-ルートビュー

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

パフォーマンスの向上した回答パフォーマンスを重視する場合、より良い方法があります。実装するのは少し面倒ですが、パフォーマンスの向上は追加の作業に値します。代わりに使用する*ngIf条件付きスピナーを表示するために、我々は、角の活用可能性NgZoneとをRenderer、我々はスピナーの状態を変更する角度の変化を検出バイパス意志スピナーのオン/オフスイッチに。これを使用した*ngIf場合と比較してアニメーションがスムーズになることがわかりましたasyncパイプ。

これは、いくつかの調整を加えた私の以前の回答に似ています。

app.component.ts-ルートコンポーネント

...
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-ルートビュー

<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
すばらしい、あなたのアプローチに感謝します。それは以前に私に影響を与えたので、私は自分の長い方法をこの魅力的なアイデアに置き換えました。制御を失い、それを止めることができなかったのでrouter navigation、元に戻す必要があっit triggering the spinnerたのです。navigationInterceptor解決策のように見えますが、何かが壊れるのを待っているかどうかはわかりません。混ざるとasync requests再び問題が発生すると思います。
Ankit Singh 2016

1
多分これはある時点でうまくいきましたか?Angular 2.4.8では現在動作していません。ページは同期しています。Spinnerは、ページ全体または親コンポーネントがレンダリングされるまでレンダリングされず、そのNavigationEndでレンダリングされます。それはスピナーのポイントを打ち負かす
techguy2000

1
不透明度の使用は良い選択ではありません。視覚的には問題ありませんが、Chromeでは、ロードする画像/アイコンがボタンまたはテキストの上にある場合、ボタンにアクセスできません。私はそれを使用するように変更displayのいずれかnoneまたはinline
im1dermike

2
私はこのアプローチが好きです。しかし、私は問題を抱えており、理由がわかりません。NavigationEndでアニメーションを切り替えないと、スピナーの読み込みを確認できますが、falseに切り替えると、ルートが非常に速く変化するため、アニメーションも確認できません:(でも、ネットワークとの接続を遅くするthrotelingが、そのはまだ同じ残っていない:(全くロードあなたは私にこの上の任意の提案をしてください与えるでした私は負荷要素にクラスを追加し、除去することにより、アニメーションを制御感謝。。。
d123546

1
コードをコピーしたときにこのエラーが発生しました'md-spinner' is not a known element:。Angularは初めてです。何が間違いなのか教えて頂けませんか?
Manu Chadha

39

更新:3新しいルーターにアップグレードしたので、ガードを使用すると@borislemkeのアプローチは機能しませんCanDeactivate。私は私の古い方法に降格していie:ます、この答え

UPDATE2:新しいルーターのルーターイベントは有望に見え、@ borislemke の回答はスピナー実装の主要な側面をカバーしているようです。テストはしていませんが、お勧めします。

UPDATE1:で通知されたOld-Routerイベントが1つだけであったの時代に、私はこの回答を書きました。また、以下のアプローチの過負荷を感じて、それのみを使用してそれを実行しようとしましたが、検出する方法がなかったため、それは逆効果になりました。だから私は長いアプローチ(二重の仕事)に戻らなければなりませんでした。route-changedrouter.subscribe()router.subscribe()canceled navigation


あなたがAngular2であなたのやり方を知っているなら、これはあなたが必要とするものです


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

ルートコンポーネント-(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(Spinner-serviceにサブスクライブして、それに応じてactiveの値を変更します)

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(このサービスをブートストラップする)

変更時のステータスを変更するためにスピナーコンポーネントによってサブスクライブされるオブザーバブルを定義し、スピナーをアクティブ/非アクティブにして設定する機能。

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

他のすべてのルートのコンポーネント

(サンプル):

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 2016

スピナーのサービスを書くのを手伝ってくれませんか?CSSでアニメーションやその他のことは自分で行うことができますが、サービスを手伝っていただければ幸いです。
ルーカス

この実装も同じですが、正確ではありません
Ankit Singh

1
私は今のところいくつかのコードを追加しましたspinner-serviceが、それを機能させるために他の部分に必要なだけです。そして、それがそうであることを思い出してくださいangular2-rc-1
アンキット・シン

1
実際には、これは素晴らしく機能します。テスト目的での私のヒントは、ngOnInitのsetTimeout(()=> this.spinner.stop()、5000)でスピナーの停止を遅らせることができます
Jhonatas Kleinkauff

10

単純なcssを使用しないのはなぜですか?

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

そしてあなたのスタイルで:

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

または、最初の回答でこれを行うこともできます。

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

そして、単に

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

ここでのトリックは、新しいルートとコンポーネントは常にrouter-outletの後に表示されるため、単純なcssセレクターを使用して、ロードの表示と非表示を切り替えることができます。


<router-outlet>では、コンポーネントから親コンポーネントに値を渡すことができないため、読み込みdivを非表示にするのは少し複雑になります。
Praveen Rana

1
また、アプリケーションに多くのルート変更があり、スピナーを毎回瞬時に表示する場合は、非常に煩わしい場合があります。RxJを使用してデバウンスタイマーを設定し、少し遅れてのみ表示されるようにすることをお勧めします。
Simon_Weaver

2

最初に必要な特別なロジックがある場合ルートにのみは、次のことを実行できます。

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

これにより、ページの上部に表示されていたページフッターを非表示にする必要がありました。また、最初のページのローダーのみが必要な場合は、これを使用できます。


0

この既存のソリューションを使用することもできます。デモはこちらです。youtube読み込みバーのように見えます。見つけて自分のプロジェクトに追加しました。


それはリゾルバーに役立ちますか?私の問題は、リゾルバーがデータを返却している間、実際のターゲットコンポーネントngOninitがまだ呼び出されていないため、スピナーをどこにも表示できないことです!! 私のアイデアは、ngOnInitでスピナーを表示し、解決されたデータがルートサブスクリプションから返されるとそれを非表示にすること
でした
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.