Angular 2在Route Change中滚动到顶部

在我的Angular 2应用中,当我向下滚动页面并单击页面底部的链接时,它确实会改变路由并将我带到下一页,但它不会滚动到页面顶部。因此,如果第一页很长,第二页内容很少,就会给人一种第二页缺乏内容的印象。因为只有当用户滚动到页面顶部时,内容才可见。

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

248767 次浏览

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

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时遇到滚动到顶部的问题,这将有所帮助。窗口或文档主体不会有滚动条,因此您需要获取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现在有一个滚动计划,这可能有助于处理滚动。

Angular 6.1及更高版本:

Angular 6.1(发布于2018-07-25)通过一个名为“路由器滚动位置恢复”的特性,添加了内置支持来处理这个问题。正如官方角的博客中所描述的,你只需要在路由器配置中启用它,如下所示:

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

此外,该博客表示“预计这将成为未来主要版本的默认设置”。到目前为止,这种情况还没有发生(从Angular 11.0开始),但最终你将不需要在代码中做任何事情,它将会正确地开箱工作。

你可以在官方文件中看到关于此特性以及如何自定义此行为的更多细节。

Angular 6.0及更早版本:

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

下面的代码扩展了检测这种导航的逻辑,方法是订阅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的讨论中(在新页面中,更改路由不会滚动到顶部)。

也许你想只在根路由器的更改中访问top(不是在子路由器中)。 因为你可以在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 (最初的发布)

< p > @Fernando埃切维里亚 太棒了!但是这段代码不能在哈希路由器或惰性路由器中工作。因为它们不会触发位置更改。 可以试试这个:

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

这考虑了大多数用例,使用方法与基本用例相同,优点是启用/禁用它:

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

如果你不想被广告吸引,就不要看广告

另一个改进是检查浏览器是否支持passive事件。这将使代码更加复杂,如果你想在你的自定义指令/模板中实现所有这些,这有点模糊。这就是为什么我写了一些< >强图书馆< / >强,你可以用它来解决这些问题。如果你使用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);
}
}
}

大家好,这在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替换的)。这也解决了当用户决定按住浏览器上的后退或前进按钮,返回或前进多个页面而不是一个页面时的边缘情况。

附注:我只测试了最新版本的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;
}
});
}
}

这段代码背后的主要思想是将所有访问过的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();
}
}

这个解决方案基于@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 { }

< a href = " https://angular。io/api/router/ExtraOptions" rel="noreferrer">Angular官方文档 . io/api/router/ExtraOptions" rel="noreferrer">

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

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莱斯提供的完美答案,如下所示, 您可以通过添加平滑滚动来调整实现,如下所示

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

到你的样式。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]);
< p > 窗口滚动顶部 < br > 两个窗口。pageYOffset和document.documentElement.scrollTop在所有情况下返回相同的结果。窗口。 . pageYOffset在ie9以下不支持

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。 Ref。路由器ExtraOptions < / p >
@NgModule({
imports: [
SomeModule.forRoot(
SomeRouting,
{
scrollPositionRestoration: 'enabled',
scrollOffset:[0,0]
})],
exports: [RouterModule]
})

对于每个正在寻找解决方案并阅读这篇文章的人。的

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

并没有回答这个主题的问题。如果我们查看Angular的源代码,我们会看到一些有趣的行:

enter image description here

这个东西只对反向导航有用。其中一个解决方案可能是:

constructor(router: Router) {


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

这将查看每个导航到该id的div并滚动到它;

另一种方法是使用相同的逻辑,但在decorator或指令的帮助下,允许你选择何时何地滚动顶部;

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;


});