Angular 2 滚动到路线更改顶部

Angular 2 Scroll to top on Route Change

在我的 Angular 2 应用程序中,当我向下滚动页面并单击页面底部的 link 时,它确实改变了路线并将我带到下一页,但它没有' 滚动到页面顶部。因此,如果第一页很长,第二页内容很少,给人的印象是第二页没有内容。因为仅当用户滚动到页面顶部时内容才可见。

我可以在组件的 ngInit 中将 window 滚动到页面顶部但是,有没有更好的解决方案可以自动处理我应用程序中的所有路由?

您可以在您的主要组件上注册一个路由更改侦听器,并在路由更改时滚动到顶部。

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

对于 iphone/ios safari,您可以使用 setTimeout

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

您可以利用可观察的 filter 方法将其写得更简洁:

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

如果您在使用 Angular Material 2 sidenav 时无法滚动到顶部,这会有所帮助。 window 或文档正文没有滚动条,因此您需要获取 sidenav 内容容器并滚动该元素,否则请尝试默认滚动 window。

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 现在有一个 scrolling package 可能有助于处理滚动。

Angular 6.1 及更高版本:

Angular 6.1(于 2018-07-25 发布)通过名为“路由器滚动位置恢复”的功能添加了内置支持来处理此问题。正如官方Angular blog中描述的那样,您只需要在路由器配置中启用它,如下所示:

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

此外,该博客指出“预计这将成为未来主要版本的默认设置”。到目前为止,这还没有发生(从 Angular 11.0 开始),但最终您根本不需要在代码中做任何事情,开箱即用即可正常工作。

您可以在 the official docs 中查看有关此功能以及如何自定义此行为的更多详细信息。

Angular 6.0 及更早版本:

虽然@GuilhermeMeireles 的出色回答解决了原始问题,但它引入了一个新问题,它打破了您在向后或向前导航(使用浏览器按钮或通过代码中的位置)时预期的正常行为。预期的行为是,当您导航回该页面时,它应该保持向下滚动到您单击 link 时的相同位置,但是当到达每个页面时滚动到顶部显然打破了这种预期。

下面的代码通过订阅 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);
            }
        });
    }
}

只需单击操作即可轻松完成

在您的主要组件中 html 引用 #scrollContainer

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

在主要组件.ts

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

如果您有服务器端呈现,您应该注意不要 运行 在服务器上使用 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')

最佳答案在 Angular GitHub 讨论 (Changing route doesn't scroll to top in the new page) 中。

Maybe you want go to top only in root router changes (not in children, because you can load routes with lazy load in f.e. a tabset)

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 (original post)

@费尔南多·埃切维里亚 伟大的!但是此代码不适用于散列路由器或惰性路由器。因为它们不会触发位置更改。 可以试试这个:

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

如果您只需要将页面滚动到顶部,您可以这样做(不是最好的解决方案,但速度很快)

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

您可以将 AfterViewInit 生命周期挂钩添加到您的组件。

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

使用 Router 本身会导致您无法完全克服这些问题以保持一致的浏览器体验。在我看来,最好的方法是只使用自定义 directive 并让它在点击时重置滚动条。这样做的好处是,如果您与单击的位置相同 url,页面也会滚动回顶部。这与正常网站一致。基本的 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)
      );
    }
  }
}

这考虑了大多数用例,与基本的用法相同,优点是enable/disabling它:

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

广告,不想被打广告就别看了

可以进行另一项改进来检查浏览器是否支持 passive 事件。这会使代码更加复杂,如果您想在自定义 directives/templates 中实现所有这些,则有点晦涩难懂。这就是为什么我写了一些 library 可以用来解决这些问题的原因。要具有与上述相同的功能,并添加 passive 事件,如果您使用 ng-event-options 库,您可以将指令更改为此。逻辑在 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);
      }        
    }
}

大家好,这对我有用 angular 4. 您只需引用父项即可滚动路由器更改`

layout.component.pug

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

layout.component.ts

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

这对我来说最适合所有导航更改,包括哈希导航

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

这是我想出的解决方案。我将 LocationStrategy 与 Router 事件配对。使用 LocationStrategy 设置一个布尔值以了解用户当前何时遍历浏览器历史记录。这样,我就不必存储一堆 URL 和 y-scroll 数据(无论如何都不能很好地工作,因为每个数据都是基于 URL 替换的)。这也解决了用户决定按住浏览器上的后退或前进按钮并后退或前进多个页面而不是一个页面的边缘情况。

P.S。我只在最新版本的 IE、Chrome、FireFox、Safari 和 Opera 上进行了测试(截至 post)。

希望对您有所帮助。

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

此代码背后的主要思想是将所有访问过的 url 与相应的 scrollY 数据一起保存在一个数组中。每当用户放弃一个页面 (NavigationStart) 时,这个数组就会更新。每次用户进入新页面(NavigationEnd)时,我们决定恢复 Y 位置或不恢复 Y 位置取决于我们如何到达该页面。如果使用了某个页面上的引用,我们将滚动到 0。如果使用了浏览器 back/forward 功能,我们将滚动到保存在数组中的 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();
  }
}

此解决方案基于@FernandoEcheverria 和@GuilhermeMeireles 的解决方案,但更简洁并且适用于 Angular 路由器提供的 popstate 机制。这允许存储和恢复多个连续导航的滚动级别。

我们将每个导航状态的滚动位置存储在地图中 scrollLevels。一旦有 popstate 事件,即将恢复的状态的 ID 由 Angular 路由器提供: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);
        }
      }

    });
  }

}

从 Angular 6.1 开始,您现在可以避免麻烦,将 extraOptions 作为第二个参数传递给您的 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 Official Docs

从Angular 6.1开始,路由器提供了一个名为scrollPositionRestorationconfiguration option,这是为了满足这种情况而设计的。

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

window.scrollTo() 在 Angular 5 中对我不起作用,所以我使用了 document.body.scrollTop 之类的

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

除了@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

Angular 最近引入了一个新功能,在 angular 路由模块中进行如下更改

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

如果您使用相同的路径加载不同的组件,那么您可以使用 ViewportScroller 来实现相同的目的。

import { ViewportScroller } from '@angular/common';

constructor(private viewportScroller: ViewportScroller) {}

this.viewportScroller.scrollToPosition([0, 0]);

window 滚动到顶部
window.pageYOffset 和 document.documentElement.scrollTop returns 在所有情况下的结果相同。 window.pageYOffset IE 9 以下不支持。

app.component.ts

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

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  isShow: boolean;
  topPosToStartShowing = 100;

  @HostListener('window:scroll')
  checkScroll() {

    const scrollPosition = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;

    console.log('[scroll]', scrollPosition);

    if (scrollPosition >= this.topPosToStartShowing) {
      this.isShow = true;
    } else {
      this.isShow = false;
    }
  }

  gotoTop() {
    window.scroll({ 
      top: 0, 
      left: 10, 
      behavior: 'smooth' 
    });
  }
}

app.component.html

<style>
  p {
  font-family: Lato;
}

button {
  position: fixed;
  bottom: 5px;
  right: 5px;
  font-size: 20px;
  text-align: center;
  border-radius: 5px;
  outline: none;
}
  </style>
<p>
  Lorem ipsum dolor sit, amet consectetur adipisicing elit. Minus, repudiandae quia. Veniam amet fuga, eveniet velit ipsa repudiandae nemo? Sit dolorem itaque laudantium dignissimos, rerum maiores nihil ad voluptates nostrum.
</p>
<p>
  Lorem ipsum dolor sit, amet consectetur adipisicing elit. Minus, repudiandae quia. Veniam amet fuga, eveniet velit ipsa repudiandae nemo? Sit dolorem itaque laudantium dignissimos, rerum maiores nihil ad voluptates nostrum.
</p>
<p>
  Lorem ipsum dolor sit, amet consectetur adipisicing elit. Minus, repudiandae quia. Veniam amet fuga, eveniet velit ipsa repudiandae nemo? Sit dolorem itaque laudantium dignissimos, rerum maiores nihil ad voluptates nostrum.
</p>
<p>
  Lorem ipsum dolor sit, amet consectetur adipisicing elit. Minus, repudiandae quia. Veniam amet fuga, eveniet velit ipsa repudiandae nemo? Sit dolorem itaque laudantium dignissimos, rerum maiores nihil ad voluptates nostrum.
</p>
<p>
  Lorem ipsum dolor sit, amet consectetur adipisicing elit. Minus, repudiandae quia. Veniam amet fuga, eveniet velit ipsa repudiandae nemo? Sit dolorem itaque laudantium dignissimos, rerum maiores nihil ad voluptates nostrum.
</p>
<p>
  Lorem ipsum dolor sit, amet consectetur adipisicing elit. Minus, repudiandae quia. Veniam amet fuga, eveniet velit ipsa repudiandae nemo? Sit dolorem itaque laudantium dignissimos, rerum maiores nihil ad voluptates nostrum.
</p>
<p>
  Lorem ipsum dolor sit, amet consectetur adipisicing elit. Minus, repudiandae quia. Veniam amet fuga, eveniet velit ipsa repudiandae nemo? Sit dolorem itaque laudantium dignissimos, rerum maiores nihil ad voluptates nostrum.
</p>
<p>
  Lorem ipsum dolor sit, amet consectetur adipisicing elit. Minus, repudiandae quia. Veniam amet fuga, eveniet velit ipsa repudiandae nemo? Sit dolorem itaque laudantium dignissimos, rerum maiores nihil ad voluptates nostrum.
</p>

<button *ngIf="isShow" (click)="gotoTop()"></button>

您也可以在Route.ts中使用scrollOffset。 参考Router ExtraOptions

@NgModule({
  imports: [
    SomeModule.forRoot(
      SomeRouting,
      {
        scrollPositionRestoration: 'enabled',
        scrollOffset:[0,0]
      })],
  exports: [RouterModule]
})

对于所有正在寻找解决方案并阅读本文的人 post。

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

没有回答题目的问题。如果我们查看 Angular 源代码,那么我们可以在那里读到有趣的行:

所以这个东西只对后退导航有效。解决方案之一可能是这样的:

constructor(router: Router) {

    router.events
        .pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd))
        .subscribe(() => {
            this.document.querySelector('#top').scrollIntoView();
        });
}

这将在每个导航到具有该 ID 的 div 并滚动到它;

另一种方法是使用相同的逻辑,但在装饰器或指令的帮助下,这将允许您 select 滚动顶部的位置和时间;

lastRoutePath?: string;

ngOnInit(): void {
  void this.router.events.forEach((event) => {
    if (event instanceof ActivationEnd) {
      if (this.lastRoutePath !== event.snapshot.routeConfig?.path) {
        window.scrollTo(0, 0);
      }
      this.lastRoutePath = event.snapshot.routeConfig?.path;
    }
  });
}

如果您停留在同一页面,它不会滚动到顶部,而只会更改 slug / id 或其他内容

在下面调用它执行,它对我有效 %100

  document.body.scrollTop = 0;

比如

 this.brandCollectionList$.subscribe((response) => {
  document.body.scrollTop = 0;

});