web-dev-qa-db-ja.com

Angular 2ルート変更で一番上までスクロールします

ページを下にスクロールしてページ下部のリンクをクリックすると、Angular 2アプリでルートが変更されて次のページに移動しますが、ページの上部にスクロールしません。ページ。その結果、1ページ目が長くて2ページ目の内容が少ない場合は、2ページ目の内容が不足しているように見えます。ユーザーがページの一番上までスクロールした場合にのみコンテンツが表示されるので。

コンポーネントのngInitでウィンドウをページの一番上までスクロールできますが、アプリ内のすべてのルートを自動的に処理できるより良い解決策はありますか?

191
Naveed Ahmed

メインコンポーネントにルート変更リスナーを登録し、ルート変更時に一番上にスクロールすることができます。

import { Component, OnInit } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';

@Component({
    selector: 'my-app',
    template: '<ng-content></ng-content>',
})
export class MyAppComponent implements OnInit {
    constructor(private router: Router) { }

    ngOnInit() {
        this.router.events.subscribe((evt) => {
            if (!(evt instanceof NavigationEnd)) {
                return;
            }
            window.scrollTo(0, 0)
        });
    }
}
296

Angular 6.1以降

Angular 6.1(2018-07-25にリリース)は、 "Router Scroll Position Restoration"と呼ばれる機能を通して、この問題を処理するための組み込みサポートを追加しました。公式の Angularブログ で説明されているように、ルータ設定でこれを有効にする必要があります。

RouterModule.forRoot(routes, {scrollPositionRestoration: 'enabled'})

さらに、ブログは「これは将来のメジャーリリースでデフォルトになると予想される」と述べています。これまでのところ(Angular 7.xの時点では)これは発生していませんが、最終的にはコード内で何もする必要はなくなり、そのまま使用できます。

Angular 6.0以前

@ GuilhermeMeirelesの優れた答えは元の問題を解決しますが、前後に移動したときに予想される通常の動作を中断することによって(ブラウザボタンまたはコード内のLocationを使用して)新しい問題をもたらします。予想される動作は、ページに戻ってもリンクをクリックしたときと同じ場所までスクロールしたままにしておく必要があることですが、すべてのページに到達したときに上にスクロールするとこの予想は明らかに崩れます。

以下のコードは、LocationのPopStateEventシーケンスをサブスクライブし、新しく到着したページがそのようなイベントの結果である場合は上にスクロールするロジックをスキップすることによって、この種のナビゲーションを検出するロジックを拡張します。

戻るページがビューポート全体をカバーするのに十分な長さである場合は、スクロール位置は自動的に復元されますが、@ JordanNelsonが正しく指摘したように、ページが短い場合は元のyスクロール位置を追跡して復元する必要があります。ページに戻ったときに明示的にコードの更新版では、スクロール位置を常に明示的に復元することによって、この場合もカバーしています。

import { Component, OnInit } from '@angular/core';
import { Router, NavigationStart, NavigationEnd } from '@angular/router';
import { Location, PopStateEvent } from "@angular/common";

@Component({
    selector: 'my-app',
    template: '<ng-content></ng-content>',
})
export class MyAppComponent implements OnInit {

    private lastPoppedUrl: string;
    private yScrollStack: number[] = [];

    constructor(private router: Router, private location: Location) { }

    ngOnInit() {
        this.location.subscribe((ev:PopStateEvent) => {
            this.lastPoppedUrl = ev.url;
        });
        this.router.events.subscribe((ev:any) => {
            if (ev instanceof NavigationStart) {
                if (ev.url != this.lastPoppedUrl)
                    this.yScrollStack.Push(window.scrollY);
            } else if (ev instanceof NavigationEnd) {
                if (ev.url == this.lastPoppedUrl) {
                    this.lastPoppedUrl = undefined;
                    window.scrollTo(0, this.yScrollStack.pop());
                } else
                    window.scrollTo(0, 0);
            }
        });
    }
}
210

Angular 6.1から、煩雑さを避けてextraOptionsを2番目のパラメータとしてRouterModule.forRoot()に渡すことができ、scrollPositionRestoration: enabledを指定してAngularにルートが変わるたびに上にスクロールするように指示できます。

デフォルトでは、これはapp-routing.module.tsにあります。

const routes: Routes = [
  {
    path: '...'
    component: ...
  },
  ...
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, {
      scrollPositionRestoration: 'enabled', // Add options right here
    })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Angular公式ドキュメント

43
Abdul Rafay

観察可能なfilterメソッドを利用することで、これをもっと簡潔に書くことができます。

this.router.events.filter(event => event instanceof NavigationEnd).subscribe(() => {
      this.window.scrollTo(0, 0);
});

Angular Material 2サイドナビを使用しているときに上部にスクロールする問題がある場合は、これが役立ちます。ウィンドウやドキュメント本体にはスクロールバーがないので、sidenavコンテンツコンテナを取得してその要素をスクロールする必要があります。それ以外の場合は、デフォルトでウィンドウをスクロールしてみてください。

this.router.events.filter(event => event instanceof NavigationEnd)
  .subscribe(() => {
      const contentContainer = document.querySelector('.mat-sidenav-content') || this.window;
      contentContainer.scrollTo(0, 0);
});

また、Angular CDK v6.xには スクローリングパッケージ があります。これはスクロールの処理に役立ちます。

27
mtpultz

サーバーサイドのレンダリングがある場合は、その変数が存在しないサーバーでwindowsを使用してコードを実行しないように注意する必要があります。コードが壊れる可能性があります。

export class AppComponent implements OnInit {
  routerSubscription: Subscription;

  constructor(private router: Router,
              @Inject(PLATFORM_ID) private platformId: any) {}

  ngOnInit() {
    if (isPlatformBrowser(this.platformId)) {
      this.routerSubscription = this.router.events
        .filter(event => event instanceof NavigationEnd)
        .subscribe(event => {
          window.scrollTo(0, 0);
        });
    }
  }

  ngOnDestroy() {
    this.routerSubscription.unsubscribe();
  }
}

isPlatformBrowserは、アプリケーションがレンダリングされている現在のプラットフォームがブラウザかどうかを確認するために使用される関数です。注入されたplatformIdを渡します。

安全のために、変数windowsの存在をチェックすることも可能です。

if (typeof window != 'undefined')
15
Raptor

クリック操作で簡単に行えます

あなたのメインコンポーネントのhtmlに#scrollContainerを参照してください

<div class="main-container" #scrollContainer>
    <router-outlet (activate)="onActivate($event, scrollContainer)"></router-outlet>
</div>

主成分の.tsに

onActivate(e, scrollContainer) {
    scrollContainer.scrollTop = 0;
}
12
SAT

最良の答えはAngular GitHubのディスカッションにあります( 新しいページでルートを変更しても一番上にスクロールしません )。

たぶんあなたはルートルーターの変更だけでトップに行きたい(あなたはf.e.タブセットで遅延ロードでルートをロードすることができるので子ではなく)

app.component.html

<router-outlet (deactivate)="onDeactivate()"></router-outlet>

app.component.ts

onDeactivate() {
  document.body.scrollTop = 0;
  // Alternatively, you can scroll to top by using this other call:
  // window.scrollTo(0, 0)
}

JoniJnmオリジナルの投稿 )への完全なクレジット

10
zurfyx

あなたのコンポーネントに AfterViewInit lifecycleフックを追加することができます。

ngAfterViewInit() {
   window.scrollTo(0, 0);
}
6
stillatmylinux

これが私が思いついた解決策です。 LocationStrategyとRouterイベントを組み合わせました。 LocationStrategyを使用して、ユーザーがブラウザの履歴を現在移動している時期を知るためのブール値を設定します。このように、私はたくさんのURLとyスクロールデータを保存する必要はありません(それぞれのデータはURLに基​​づいて置き換えられるので、いずれにしてもうまくいきません)。これは、ユーザーがブラウザの戻るボタンまたは進むボタンを押したままにして、1ページだけではなく複数のページを前後に移動することにした場合のEdgeの問題も解決します。

P.S私はIE、Chrome、FireFox、Safari、そしてOperaの最新版でのみテストした(この記事の時点で)。

お役に立てれば。

export class AppComponent implements OnInit {
  isPopState = false;

  constructor(private router: Router, private locStrat: LocationStrategy) { }

  ngOnInit(): void {
    this.locStrat.onPopState(() => {
      this.isPopState = true;
    });

    this.router.events.subscribe(event => {
      // Scroll to top if accessing a page, not via browser history stack
      if (event instanceof NavigationEnd && !this.isPopState) {
        window.scrollTo(0, 0);
        this.isPopState = false;
      }

      // Ensures that isPopState is reset
      if (event instanceof NavigationEnd) {
        this.isPopState = false;
      }
    });
  }
}
4
Sal_Vader_808

Angular 6.1以降、ルーターはscrollPositionRestorationと呼ばれる 設定オプション を提供します。これはこのシナリオに対応するように設計されています。

imports: [
  RouterModule.forRoot(routes, {
    scrollPositionRestoration: 'enabled'
  }),
  ...
]
3
Marty A

単にページを上にスクロールする必要がある場合は、これを実行できます(最善の解決策ではありませんが高速です)。

document.getElementById('elementId').scrollTop = 0;
3
Aliaksei

このソリューションは@ FernandoEcheverriaと@ GuilhermeMeirelesのソリューションに基づいていますが、より簡潔でAngularルーターが提供するpopstateメカニズムと連携します。これにより、複数の連続したナビゲーションのスクロールレベルを保存および復元できます。

各ナビゲーション状態のスクロール位置をマップscrollLevelsに格納します。 popstateイベントが発生すると、復元されようとしている状態のIDがAngular Router:event.restoredState.navigationIdによって提供されます。これはその後、その状態の最後のスクロールレベルをscrollLevelsから取得するために使用されます。

ルートに格納されているスクロールレベルがない場合は、予想どおりに一番上にスクロールします。

import { Component, OnInit } from '@angular/core';
import { Router, NavigationStart, NavigationEnd } from '@angular/router';

@Component({
    selector: 'my-app',
    template: '<ng-content></ng-content>',
})
export class AppComponent implements OnInit {

  constructor(private router: Router) { }

  ngOnInit() {
    const scrollLevels: { [navigationId: number]: number } = {};
    let lastId = 0;
    let restoredId: number;

    this.router.events.subscribe((event: Event) => {

      if (event instanceof NavigationStart) {
        scrollLevels[lastId] = window.scrollY;
        lastId = event.id;
        restoredId = event.restoredState ? event.restoredState.navigationId : undefined;
      }

      if (event instanceof NavigationEnd) {
        if (restoredId) {
          // Optional: Wrap a timeout around the next line to wait for
          // the component to finish loading
          window.scrollTo(0, scrollLevels[restoredId] || 0);
        } else {
          window.scrollTo(0, 0);
        }
      }

    });
  }

}
3
Simon Mathewson

Angularは最近angularルーティングモジュール内で以下のような変更を行う新しい機能を導入しました

@NgModule({
  imports: [RouterModule.forRoot(routes,{
    scrollPositionRestoration: 'top'
  })],
1
Pran R.V

iphone/iosサファリのためにあなたはsetTimeoutで包むことができます

setTimeout(function(){
    window.scrollTo(0, 1);
}, 0);
1
tubbsy

こんにちはみんなこれは角度4で私のために動作しますあなたはルータの変更をスクロールするために親を参照する必要があります

layout.component.pug

.wrapper(#outlet="")
    router-outlet((activate)='routerActivate($event,outlet)')

layout.component.ts

 public routerActivate(event,outlet){
    outlet.scrollTop = 0;
 }`
1
Jonas Arguelles

Router自体を使用すると、一貫したブラウザエクスペリエンスを維持するために完全には克服できない問題が発生します。私の意見では、最善の方法はカスタムのdirectiveを使うことで、クリック時にスクロールをリセットすることです。これについての良いところは、あなたがクリックしたのと同じurl上にいる場合、ページも上にスクロールバックするということです。これは通常のWebサイトと一致しています。基本的なdirectiveは次のようになります。

import {Directive, HostListener} from '@angular/core';

@Directive({
    selector: '[linkToTop]'
})
export class LinkToTopDirective {

    @HostListener('click')
    onClick(): void {
        window.scrollTo(0, 0);
    }
}

次のように使用します。

<a routerLink="/" linkToTop></a>

ほとんどのユースケースではこれで十分ですが、これから発生する可能性があるいくつかの問題を想像できます。

  • universalを使用しているため、windowでは動作しません
  • クリックするたびにトリガーされるため、変化検出へのわずかなスピードの影響
  • このディレクティブを無効にする方法はありません

実際にこれらの問題を克服することはかなり簡単です:

@Directive({
  selector: '[linkToTop]'
})
export class LinkToTopDirective implements OnInit, OnDestroy {

  @Input()
  set linkToTop(active: string | boolean) {
    this.active = typeof active === 'string' ? active.length === 0 : active;
  }

  private active: boolean = true;

  private onClick: EventListener = (event: MouseEvent) => {
    if (this.active) {
      window.scrollTo(0, 0);
    }
  };

  constructor(@Inject(PLATFORM_ID) private readonly platformId: Object,
              private readonly elementRef: ElementRef,
              private readonly ngZone: NgZone
  ) {}

  ngOnDestroy(): void {
    if (isPlatformBrowser(this.platformId)) {
      this.elementRef.nativeElement.removeEventListener('click', this.onClick, false);
    }
  }

  ngOnInit(): void {
    if (isPlatformBrowser(this.platformId)) {
      this.ngZone.runOutsideAngular(() => 
        this.elementRef.nativeElement.addEventListener('click', this.onClick, false)
      );
    }
  }
}

これは基本的な使い方と同じ使い方で、ほとんどのユースケースを考慮に入れています。

<a routerLink="/" linkToTop></a> <!-- always active -->
<a routerLink="/" [linkToTop]="isActive"> <!-- active when `isActive` is true -->

コマーシャル、宣伝したくない場合は読んではいけない

ブラウザがpassiveイベントをサポートしているかどうかを確認するために、もう1つの改善が行われる可能性があります。これはコードをもう少し複雑にするでしょう、そしてあなたがあなたのカスタムディレクティブ/テンプレートでこれらすべてを実装したいならば、少しあいまいです。だから私はあなたがこれらの問題に取り組むのに使える小さな library を書いたのです。 ng-event-optionsライブラリを使用する場合は、上記と同じ機能を持ち、追加されたpassiveイベントを使用して、ディレクティブをこれに変更できます。ロジックはclick.pnbリスナーの内部にあります。

@Directive({
    selector: '[linkToTop]'
})
export class LinkToTopDirective {

    @Input()
    set linkToTop(active: string|boolean) {
        this.active = typeof active === 'string' ? active.length === 0 : active;
    }

    private active: boolean = true;

    @HostListener('click.pnb')
    onClick(): void {
      if (this.active) {
        window.scrollTo(0, 0);
      }        
    }
}
0
PierreDuc

以下に示すように @Guilherme Meireles によって提供される完璧な答えに加えて、以下に示すようにスムーズスクロールを追加することで実装を微調整できます。

 import { Component, OnInit } from '@angular/core';
    import { Router, NavigationEnd } from '@angular/router';

    @Component({
        selector: 'my-app',
        template: '<ng-content></ng-content>',
    })
    export class MyAppComponent implements OnInit {
        constructor(private router: Router) { }

        ngOnInit() {
            this.router.events.subscribe((evt) => {
                if (!(evt instanceof NavigationEnd)) {
                    return;
                }
                window.scrollTo(0, 0)
            });
        }
    }

それから下にスニペットを追加します

 html {
      scroll-behavior: smooth;
    }

あなたのstyles.cssに

0

このコードの背後にある主な考え方は、すべてのアクセスされたURLをそれぞれのscrollYデータとともに配列に保持することです。ユーザーがページ(NavigationStart)を放棄するたびに、この配列は更新されます。ユーザーが新しいページに移動するたびに(NavigationEnd)、Y位置を元に戻すか、このページへのアクセス方法に依存しないかを決定します。あるページの参照が使用されている場合は、0にスクロールします。ブラウザのバック/フォワード機能が使用されている場合は、配列に保存されているYにスクロールします。私の英語ですみません:)

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Location, PopStateEvent } from '@angular/common';
import { Router, Route, RouterLink, NavigationStart, NavigationEnd, 
    RouterEvent } from '@angular/router';
import { Subscription } from 'rxjs/Subscription';

@Component({
  selector: 'my-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy {

  private _subscription: Subscription;
  private _scrollHistory: { url: string, y: number }[] = [];
  private _useHistory = false;

  constructor(
    private _router: Router,
    private _location: Location) {
  }

  public ngOnInit() {

    this._subscription = this._router.events.subscribe((event: any) => 
    {
      if (event instanceof NavigationStart) {
        const currentUrl = (this._location.path() !== '') 
           this._location.path() : '/';
        const item = this._scrollHistory.find(x => x.url === currentUrl);
        if (item) {
          item.y = window.scrollY;
        } else {
          this._scrollHistory.Push({ url: currentUrl, y: window.scrollY });
        }
        return;
      }
      if (event instanceof NavigationEnd) {
        if (this._useHistory) {
          this._useHistory = false;
          window.scrollTo(0, this._scrollHistory.find(x => x.url === 
          event.url).y);
        } else {
          window.scrollTo(0, 0);
        }
      }
    });

    this._subscription.add(this._location.subscribe((event: PopStateEvent) 
      => { this._useHistory = true;
    }));
  }

  public ngOnDestroy(): void {
    this._subscription.unsubscribe();
  }
}
0

window.scrollTo()はAngular 5では私のところでは動作しないので、私はdocument.body.scrollTopのように使いました、

this.router.events.subscribe((evt) => {
   if (evt instanceof NavigationEnd) {
      document.body.scrollTop = 0;
   }
});
0
Rohan Kumar

これは私にとってハッシュナビゲーションを含むすべてのナビゲーションの変更に最適でした。

constructor(private route: ActivatedRoute) {}

ngOnInit() {
  this._sub = this.route.fragment.subscribe((hash: string) => {
    if (hash) {
      const cmp = document.getElementById(hash);
      if (cmp) {
        cmp.scrollIntoView();
      }
    } else {
      window.scrollTo(0, 0);
    }
  });
}
0
Jorg Janke

@ Fernando Echeverriaすごい!しかし、このコードはハッシュルータやレイジールータでは動作しません。それらは位置の変更を引き起こさないからです。これを試すことができます:

private lastRouteUrl: string[] = []
  

ngOnInit(): void {
  this.router.events.subscribe((ev) => {
    const len = this.lastRouteUrl.length
    if (ev instanceof NavigationEnd) {
      this.lastRouteUrl.Push(ev.url)
      if (len > 1 && ev.url === this.lastRouteUrl[len - 2]) {
        return
      }
      window.scrollTo(0, 0)
    }
  })
}
0
Witt Bulter