首页 文章

Angular 2在Route Change上滚动到顶部

提问于
浏览
143

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

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

18 回答

  • 21

    从Angular 6.1开始,路由器提供了configuration option,称为 scrollPositionRestoration ,这是为了满足这种情况而设计的 .

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

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

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

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

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

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

    我们将每个导航状态的滚动位置存储在 Map 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);
            }
          }
    
        });
      }
    
    }
    
  • 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>
    

    这对于大多数用例来说已经足够了,但我可以想象一些可能由此产生的问题:

    • window 的使用而无效 universal

    • 对速度检测的小速度影响,因为它是由每次点击触发的

    • 无法禁用此指令

    实际上很容易克服这些问题:

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

    commercials, don't read if you don't want to be advertised

    可以进行另一项改进以检查浏览器是否支持 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);
          }        
        }
    }
    
  • 256

    最好的答案在于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

  • 111

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

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

    从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

  • 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

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

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

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

    在主要组件.ts

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

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

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

    如果您有服务器端呈现,则应注意不要在服务器上使用 windows 运行代码,该变量不存在 . 这会导致代码破坏 .

    export class AppComponent implements {
      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')
    
  • 21

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

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

    Angular 6.1 and later

    Angular 6.1(发布于2018-07-25)通过名为"Router Scroll Position Restoration"的功能添加了内置支持来处理此问题 . 如官方Angular blog中所述,您只需在路由器配置中启用此功能,如下所示:

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

    而且,博客声明“预计这将成为未来主要版本的默认版本”,因此很可能从Angular 7开始,您不需要在代码中做任何事情,这样就可以正常工作了盒子 .

    Angular 6.0 and earlier

    虽然@ 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

    这是我提出的解决方案 . 我将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;
          }
        });
      }
    }
    
  • 10

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

    你可以利用可观察的 filter 方法更简洁地写出这个:

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

    如果您有滚动条,则需要获取 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可能有助于处理滚动 .

  • 0

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

    layout.component.pug

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

    layout.component.ts

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

相关问题