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