Angular 2 Прокрутите вверх, чтобы изменить маршрут

В моем приложении "Угловое 2", когда я прокручиваю страницу вниз и нажимаю ссылку внизу страницы, она меняет маршрут и берет меня на следующей странице, но не прокручивается до верхней части страницы. В результате, если первая страница длинна, а вторая страница имеет немного содержимого, создается впечатление, что на 2-й странице не хватает содержимого. Поскольку содержимое отображается только в том случае, если пользователь прокручивается до верхней части страницы.

Я могу прокрутить окно до верхней части страницы в ngInit компонента, но есть ли лучшее решение, которое может автоматически обрабатывать все маршруты в моем приложении?

Ответ 1

Вы можете зарегистрировать прослушиватель изменения маршрута на своем основном компоненте и прокрутите вверх до конца по изменению маршрута.

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

Ответ 2

Angular 6.1 и более поздние версии:

В Angular 6.1 (выпущено в 2018-07-25) добавлена встроенная поддержка для решения этой проблемы с помощью функции "Восстановление положения прокрутки маршрутизатора". Как описано в официальном блоге Angular, вам просто нужно включить это в конфигурации маршрутизатора следующим образом:

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

Кроме того, в блоге говорится: "Ожидается, что это станет по умолчанию в будущем крупном выпуске". Пока что этого не произошло (по состоянию на Angular 8.2), но в конечном итоге вам вообще ничего не нужно будет делать в вашем коде, и это будет работать корректно из коробки.

Подробнее об этой функции и о том, как настроить это поведение, можно узнать в официальных документах.

Angular 6.0 и более ранние:

Хотя отличный ответ @GuilhermeMeireles устраняет исходную проблему, он вводит новую, нарушая обычное поведение, которое вы ожидаете при переходе назад или вперед (с помощью кнопок браузера или с помощью Location в коде). Ожидаемое поведение заключается в том, что при переходе обратно на страницу она должна оставаться прокручиваемой вниз до того же места, в котором она была при нажатии на ссылку, но прокрутка к вершине при переходе на каждую страницу, очевидно, разрушает это ожидание.

Приведенный ниже код расширяет логику для обнаружения такого рода навигации, подписавшись на последовательность 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);
            }
        });
    }
}

Ответ 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 Официальные Документы

Ответ 4

Вы можете написать это более кратко, используя преимущества метода наблюдаемого filter:

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

Если у вас возникли проблемы с прокруткой вверх при использовании sidenav 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 теперь имеет пакет прокрутки, который может помочь с обработкой прокрутки.

Ответ 5

Если у вас есть рендеринг на стороне сервера, вы должны быть осторожны, чтобы не запускать код, используя 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')

Ответ 6

просто делайте это с помощью действия click

в вашем основном компоненте html сделайте ссылку #scrollContainer

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

в главном компоненте .ts

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

Ответ 7

Лучший ответ заключается в обсуждении в Angular GitHub (изменение маршрута не прокручивается вверху на новой странице).

Возможно, вы хотите перейти только к изменениям корневого маршрутизатора (а не к детям, поскольку вы можете загружать маршруты с ленивой загрузкой в fe 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 (оригинальный пост)

Ответ 8

Вы можете добавить хук жизненного цикла AfterViewInit к своему компоненту.

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

Ответ 9

Начиная с версии Angular 6.1, маршрутизатор предоставляет конфигурационную опцию под названием scrollPositionRestoration, которая предназначена для этого сценария.

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

Ответ 10

Если вам нужно просто прокрутить страницу вверх, вы можете сделать это (не лучшее решение, но быстро)

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

Ответ 11

Вот решение, которое я придумал. Я объединил 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;
      }
    });
  }
}

Ответ 12

Это решение основано на решении @FernandoEcheverria и @GuilhermeMeireles, но оно более кратким и работает с механизмами popstate, которые предоставляет Angular Router. Это позволяет сохранять и восстанавливать уровень прокрутки нескольких последовательных навигаций.

Мы сохраняем позиции прокрутки для каждого состояния навигации на карте scrollLevels. Когда есть событие popstate, идентификатор состояния, которое должно быть восстановлено, предоставляется Угловым маршрутизатором: 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);
        }
      }

    });
  }

}

Ответ 13

В дополнение к идеальному ответу, предоставленному @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

Ответ 14

В Angular недавно появилась новая функция, внутри модуля angular маршрутизации внесены изменения, как показано ниже

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

Ответ 15

для сафари iphone/ios вы можете обернуть с помощью setTimeout

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

Ответ 16

Привет, ребята, это работает для меня в угловом 4. Вам просто нужно обратиться к родителям, чтобы прокручивать по изменению маршрутизатора '

layout.component.pug

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

layout.component.ts

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

Ответ 17

@Фернандо Эчеверриа Великий! но этот код не работает в хэш-маршрутизаторе или ленивом маршрутизаторе. потому что они не вызывают изменения местоположения. можете попробовать следующее:

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

Ответ 18

Использование самой Router вызовет проблемы, которые вы не сможете полностью преодолеть, чтобы поддерживать постоянный опыт работы с браузером. На мой взгляд, лучший способ - просто использовать пользовательский directive и позволить этому reset прокручивать клик. Хорошо, что если вы находитесь на том же 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);
      }        
    }
}

Ответ 19

Это работало для меня лучше всего для всех изменений навигации, включая хэш-навигацию

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

Ответ 20

Основная идея этого кода - сохранить все посещаемые 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();
  }
}

Ответ 21

window.scrollTo() не работает для меня в Angular 5, поэтому я использовал document.body.scrollTop как,

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

Ответ 22

Если вы загружаете разные компоненты по одному и тому же маршруту, вы можете использовать ViewportScroller для достижения одной и той же цели.

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

constructor(private viewportScroller: ViewportScroller) {}

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