Angular 2滚动到路线更改顶部


295

在我的Angular 2应用程序中,当我向下滚动页面并单击页面底部的链接时,它确实会更改路线并带我进入下一页,但不会滚动到页面顶部。结果,如果第一页很长而第二页的内容很少,则给人的印象是第二页缺少内容。由于仅当用户滚动到页面顶部时内容才可见。

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


20
从Angular 6.1开始,我们可以在急切加载的模块上或仅在app.module中使用{scrollPositionRestoration:'enabled'},它将应用于所有路由。RouterModule.forRoot(appRoutes, { scrollPositionRestoration: 'enabled' })
曼沃尔

的muito obrigado suasoluçãofuncionou perfeitamente :)
Raphael Godoi

没有人提到重点吗?正确地支持可访问性/屏幕阅读器比以往任何时候都重要,如果只滚动到顶部而不考虑焦点,则下一个选项卡按键可以跳到屏幕底部。
Simon_Weaver

Answers:


384

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

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

11
window.scrollTo(0, 0)document.body.scrollTop = 0;IMO 更简洁,更易读。
Mark E. Haase

10
没有人注意到,即使实施了此问题,问题仍然存在于Iphone的野生动物园浏览器中。有什么想法吗?
rgk

1
@mehaase看来您的答案是最好的答案。window.body.scrollTop在Firefox桌面上不适合我。所以谢谢 !
卡纳尔(KCarnaille)'17

3
这对我有用,但是破坏了默认的“后退”按钮行为。返回时应记住先前的滚动位置。
JackKalish17年

6
这工作了!尽管我添加了动画$("body").animate({ scrollTop: 0 }, 1000);而不是window.scrollTo(0, 0)使其平滑滚动到顶部
Manubhargav

360

Angular 6.1及更高版本

Angular 6.1(于2018-07-25发行)通过称为“路由器滚动位置恢复”的功能添加了内置支持来解决此问题。如官方Angular博客所述,您只需要在路由器配置中启用此功能,如下所示:

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

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

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

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

2
这应该直接在应用程序组件中,或者在其中使用的单个组件中进行(因此由整个应用程序共享)。例如,我已将其包含在顶部导航栏组件中。您不应该将其包含在所有组件中。
Fernando Echeverria

3
您可以这样做,它将使代码与其他非浏览器平台更加广泛地兼容。有关实现的详细信息,请参见stackoverflow.com/q/34177221/2858481
费尔南多·埃切维里亚

3
如果在现代浏览器中单击并按住“后退/前进”按钮,则会出现一个菜单,可让您导航到上一个/下一个位置以外的位置。如果这样做,该解决方案就会中断。对于大多数人来说这是一个边缘情况,但值得一提。
adamdport


1
有没有一种方法可以为嵌套元素启用“路由器滚动位置恢复”,或者仅适用于body
vulp

61

从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官方文档


3
即使上面的答案更具描述性,我也喜欢这个答案告诉了我确切的去向
ryanovas

32

您可以利用可观察的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现在具有滚动包,可能有助于处理滚动。


2
大!对于工作的我document.querySelector('.mat-sidenav-content .content-div').scrollTop = 0;
Amir Tugi

不错的一个家伙...在mtpultz和@AmirTugi。立即处理此问题,您为我钉上了它,加油!由于md-toolbar在position:fixed(在顶部)时Material 2的效果不好,因此可能不可避免地要滚动我自己的侧面导航。除非你们有想法.... ????
蒂姆·哈克

可能找到了我的答案... stackoverflow.com/a/40396105/3389046
Tim Harker

16

如果具有服务器端渲染,则应注意不要在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为了安全起见,还可以检查是否存在variable ,如下所示:

if (typeof window != 'undefined')

1
不要你需要注入PLATFORM_IDconstructor,让这个值作为德参数isPlatformBrowser的方法?
Poul Kruijt

1
@PierreDuc是的,答案是错误的。isPlatformBrowser是一种功能,永远是真实的。我已经编辑了。
LazarLjubenović18年

谢谢!现在是正确的!刚刚验证了API:github.com/angular/angular/blob/…
Raptor

13

只需点击一下即可轻松完成

在您的主要组件html中创建参考#scrollContainer

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

在主要组件.ts中

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

要滚动的元素可能不在scrollContainer第一个节点中,您可能需要在对象中挖掘一点,对我来说,它真正起作用的是scrollContainer .scrollable._elementRef.nativeElement.scrollTop = 0
Byron Lopez

13

Angular最近引入了一项新功能,内部的角路由模块进行如下更改

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

12

最好的答案是在Angular GitHub讨论中(更改路线不会滚动到新页面的顶部)。

也许您只想在根路由器更改中排在首位(而不是在子路由器中,因为您可以在选项卡集中的延迟加载中加载路由)

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的全部学分(原始帖子



7

从Angular 6.1开始,路由器提供了名为的配置选项scrollPositionRestoration,旨在满足这种情况。

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

4

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

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

4

这是我提出的解决方案。我将LocationStrategy与Router事件配对。使用LocationStrategy设置一个布尔值,以知道用户当前何时浏览浏览器的历史记录。这样,我不必存储一堆URL和y-scroll数据(反正效果不佳,因为每个数据都是根据URL替换的)。当用户决定按住浏览器上的“后退”或“前进”按钮,然后前进或后退多个页面而不是一页时,这也解决了极端情况。

PS我仅在最新版本的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

该解决方案基于@FernandoEcheverria和@GuilhermeMeireles的解决方案,但它更为简洁,并且可以与Angular Router提供的popstate机制一起使用。这允许存储和恢复多个连续导航的滚动级别。

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

    });
  }

}

太棒了 我必须制作一个稍微自定义的版本才能滚动div而不是窗口,但是它可以工作。一个关键的区别是scrollTopVS scrollY
BBaysinger

4

除了如下所示的@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


1

对于iPhone / iOS Safari,您可以使用setTimeout包装

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

就我而言,它还需要将页面包装元素css设置为;height: 100vh + 1px;
tubbsy

1

嗨,大家好,我在角度4中工作

layout.component.pug

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

layout.component.ts

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

1
请原谅我懒洋洋地不去学习哈巴狗,但是您可以翻译成HTML吗?
CodyBugstein

0

@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

使用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事件。这会使代码更加复杂,并且如果要在自定义指令/模板中实现所有这些功能,则可能会有点晦涩。这就是为什么我写了一个小图书馆,您可以用来解决这些问题。要具有与上述相同的功能,并添加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);
      }        
    }
}

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

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()不为我工作在角5,所以我用document.body.scrollTop一样,

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

0

如果要使用相同的路线加载不同的组件,则可以使用ViewportScroller来实现相同的目的。

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

constructor(private viewportScroller: ViewportScroller) {}

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

0

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

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>
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.