Angular 2在Route Change上滚动到顶部

问题描述 投票:201回答:20

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

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

angular typescript angular2-routing angular2-template angular2-directives
20个回答
310
投票

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

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

4
投票

从Angular 6.1开始,路由器提供了一个名为configuration optionscrollPositionRestoration,旨在满足这种情况。

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

3
投票

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

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

3
投票

该解决方案基于@ FernandoEcheverria和@ GuilhermeMeireles的解决方案,但它更简洁,并与Angular Router提供的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);
        }
      }

    });
  }

}

1
投票

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

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

1
投票

嗨伙计这对我有用4角。你只需要引用父母滚动路由器改变`

layout.component.pug

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

layout.component.ts

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

1
投票

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

@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事件。这会使代码复杂化,如果要在自定义指令/模板中实现所有这些,则有点模糊。这就是为什么我写了一些你可以用来解决这些问题的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);
      }        
    }
}

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

230
投票

Angular 6.1及更高版本:

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

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

此外,该博客还表示“预计这将成为未来主要版本的默认版本”。到目前为止,这还没有发生(从Angular 7.x开始),但最终你不需要在你的代码中做任何事情,这将只是开箱即用。

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

0
投票

window.scrollTo()在Angular 5中不适用于我,所以我使用了document.body.scrollTop

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

47
投票

从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


28
投票

你可以利用可观察的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现在有一个scrolling package可能有助于处理滚动。


15
投票

如果您有服务器端渲染,则应注意不要在服务器上使用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')

12
投票

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

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

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

在主要组件.ts

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

11
投票

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

也许你只想在根路由器更改中转到顶部(不在子节点中,因为你可以在f.e.一个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)
}

JoniJnmoriginal post)的完整学分


7
投票

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

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

4
投票

这是我提出的解决方案。我将LocationStrategy与Router事件配对。使用LocationStrategy设置一个布尔值,以了解用户当前浏览浏览器历史记录的时间。这样,我不必存储一堆URL和y滚动数据(无论如何都不能很好地工作,因为每个数据都是基于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;
      }
    });
  }
}
© www.soinside.com 2019 - 2024. All rights reserved.