Avatar ·

Прокрутка страницы вверх при смене маршрута в Angular 2+

Всем привет! Я начинающий разработчик на Angular (версия 15). Столкнулся с проблемой: при переходе между страницами через Router (использую `routerLink` в шаблоне и `this.router.navigate()` в компонентах) страница не прокручивается вверх. Если я нахожусь внизу длинной страницы и перехожу на другой маршрут, пользователь видит середину новой страницы, а не её начало. Это очень раздражает, особенно на мобильных устройствах. Я пробовал добавить `scrollPositionRestoration: 'top'` в конфигурацию RouterModule, но это не сработало для всех случаев. Также пробовал подписываться на события Router в AppComponent и вызывать `window.scrollTo(0,0)`, но тогда прокрутка происходит до того, как новый компонент полностью загрузится, и визуально это выглядит дёргано. Ещё я заметил, что если использовать `routerLink` с фрагментом (#section), то прокрутка вообще ломается. Вот пример моего текущего кода: ```typescript // app-routing.module.ts (фрагмент) const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'about', component: AboutComponent }, { path: 'contact', component: ContactComponent }, ]; @NgModule({ imports: [RouterModule.forRoot(routes, { scrollPositionRestoration: 'top' // не помогает })], exports: [RouterModule] }) export class AppRoutingModule { } ``` Подскажите, как правильно реализовать прокрутку вверх при смене маршрута? Нужно учитывать: а) фрагменты URL (#section); б) ленивую загрузку модулей; в) чтобы это работало и при программной навигации. Спасибо! <<>>

Правильное решение: используем 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.