Прокрутка страницы вверх при смене маршрута в Angular 2+
Правильное решение: используем ViewportScroller и кастомный сервис
Проблема, которую вы описали, очень распространена в Angular. Решение с scrollPositionRestoration: 'top' не всегда работает корректно, особенно с ленивой загрузкой и фрагментами. Лучший подход — использовать встроенный сервис ViewportScroller из @angular/common и создать отдельный сервис для управления прокруткой.
Шаг 1: Создаём сервис ScrollService
// scroll.service.ts
import { Injectable } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { ViewportScroller } from '@angular/common';
import { filter } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class ScrollService {
constructor(
private router: Router,
private viewportScroller: ViewportScroller
) {
this.router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe(() => {
// Проверяем, есть ли фрагмент в URL
const tree = this.router.parseUrl(this.router.url);
if (tree.fragment) {
// Если есть фрагмент — прокручиваем к элементу
this.viewportScroller.scrollToAnchor(tree.fragment);
} else {
// Иначе — в начало страницы
this.viewportScroller.scrollToPosition([0, 0]);
}
});
}
}
Шаг 2: Подключаем сервис в AppModule
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { ScrollService } from './scroll.service';
@NgModule({
imports: [
BrowserModule,
RouterModule.forRoot(routes, {
// Важно: отключаем встроенное восстановление позиции
scrollPositionRestoration: 'disabled',
anchorScrolling: 'enabled' // разрешаем прокрутку к якорям
})
],
declarations: [AppComponent],
bootstrap: [AppComponent],
providers: [ScrollService] // или просто @Injectable({ providedIn: 'root' })
})
export class AppModule { }
Шаг 3: Дополнительная настройка для ленивых модулей
Если у вас есть лениво загружаемые модули, добавьте в их конфигурацию роутинга:
// lazy.module.ts
const routes: Routes = [
{
path: 'lazy',
component: LazyComponent,
// Важно: для дочерних маршрутов
runGuardsAndResolvers: 'always'
}
];
Почему это работает?
- ViewportScroller — встроенный сервис Angular, который корректно работает даже с виртуальной прокруткой и фрагментами
- NavigationEnd — событие срабатывает только после полной загрузки компонента, что решает проблему "дёрганой" прокрутки
- Проверка фрагмента — если в URL есть #section, мы прокручиваем к элементу, а не в начало
- scrollPositionRestoration: 'disabled' — отключаем встроенный механизм, чтобы наш сервис был единственным управляющим
Важно!
Если вы используете routerLink с фрагментом, убедитесь, что у элемента есть id, совпадающий с фрагментом:
<!-- template.html -->
<a routerLink="/page" fragment="section1">К разделу 1</a>
<!-- В целевом компоненте -->
<div id="section1">Содержимое раздела</div>
Этот подход работает во всех версиях Angular (2+), включая версию 15 и 17. Он также поддерживает программную навигацию через this.router.navigate().
Альтернативный подход: использование директивы
Если вы предпочитаете не создавать отдельный сервис, можно реализовать прокрутку через директиву, которая будет применяться к корневому элементу компонента.
// scroll-to-top.directive.ts
import { Directive, HostListener, OnInit } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';
@Directive({
selector: '[appScrollToTop]'
})
export class ScrollToTopDirective implements OnInit {
constructor(private router: Router) {}
ngOnInit() {
this.router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe(() => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
}
}
Используйте директиву в шаблоне компонента:
<!-- app.component.html -->
<router-outlet appScrollToTop></router-outlet>
Плюсы: простота реализации, не требует дополнительных сервисов.
Минусы:
- Не обрабатывает фрагменты URL (#section)
- Может вызвать "дёрганье" при медленной загрузке данных в компоненте
- Не работает, если компонент использует ленивую загрузку и не имеет корневого элемента с директивой
Этот вариант подходит для простых приложений без фрагментов и с быстрой загрузкой данных. Для продакшена рекомендую использовать первый подход с ViewportScroller.