В предыдущем посте мы обсудили основы RouteReuseStrategy. Мы создали образец реализации стратегии и заставили ее работать. Одна из основных проблем здесь заключается в том, что RouteReuseStrategy не может управлять вашими прокрутками. Давай попробуем решить это

Проблема

Наша главная цель состояла в том, чтобы повторно использовать уже инициализированные компоненты. Angular может сохранять контекст компонента, область действия и элемент и использовать их снова, когда RouteReuseStartegy спрашивает об этом.

Но Angular хранит только DOM. Если мы хотим сохранить нашу позицию прокрутки, мы должны помнить, что прокрутка находится за пределами DOM.

Концепция

Итак, нам нужно реализовать сохранение свитков самостоятельно. Здесь может быть много способов:

  • кастомная директива;
  • компонентный провайдер;
  • настраиваемое поведение внутри нашей реализации RouteReuseStrategy.

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

Пользовательская директива

Почему бы и нет: как мы можем решить, когда следует восстанавливать свиток? Где событие NavigationEnd? Стоит ли подписываться на каждый NavigationEnd? А как насчет проблем с производительностью и утечек памяти?

Пользовательское поведение внутри RouteReuseStrategy

Почему бы и нет: как хранить свитки в глобальном контексте? Что, если у нас есть более одного экземпляра компонента? Как восстановить прокрутку после асинхронных вызовов, а не только после восстановления снимка маршрута?

Компонентный поставщик

Хороший способ - создать настраиваемого поставщика, который должен иметь возможность хранить ссылки на ваши HTMLElements и прокрутки, а также обрабатывать события маршрутизатора и настраиваемые эмиттеры из компонента.

class ElementScroll {
  el: HTMLElement;
  scroll: number;
}

class ScrollStoreConfig {
  componentContext: any;
  router: Router;
  route: ActivatedRoute;
  storage: {
    [key: string]: ElementScroll
  };
}

export class ScrollStoreProvider {
  public config: ScrollStoreConfig = <ScrollStoreConfig>{};
}

Здесь у нас есть конфигурация хранилища с внешними ссылками на контекст компонента, маршрутизатор и текущий маршрут.

constructor(options: {
  compContext: any,
  router: Router,
  route: ActivatedRoute
}) {
  this.config.componentContext = options.compContext;
  this.config.router = options.router;
  this.config.route = options.route;
  this.config.storage = {};

  // subscribe to NavigationEnd
  // subscribe to async updates
  
}

Обратите внимание, что наш поставщик не @Injectable(), нам нужно создать экземпляр вручную с настраиваемыми параметрами в контексте компонента:

this.scrollStoreProvider = new ScrollStoreProvider({
  compContext: this,
  router: this.router,
  route: this.route
})

Маршрутизатор будет использоваться для подписки на событие NavigationEnd, ActivatedRoute и ссылку на компонент - чтобы сопоставить текущий компонент из маршрута с нашим родительским компонентом.

Обработка событий прокрутки

Теперь мы хотим иметь метод, который принимает ссылку на прокручиваемый элемент и выполняет все это:

private storeScroll(key: string, el: HTMLElement) {
  this.config.storage[key] = <ElementScroll>{
    el: el,
    scroll: $(el).scrollTop()
  };
}
public handleScroll(key: string, el: HTMLElement) {
  let providerRef = this;
  $(el).scroll(()=>{
    providerRef.storeScroll(key, el);
  })
}

Теперь мы можем вызвать его в нашем компоненте:

this.scrollStoreProvider
       .handleScroll("key", this.viewChild.nativeElement);

Итак, мы просто говорим провайдеру «возьмите этот элемент и управляйте прокруткой». Положение прокрутки будет сохраняться при каждом звонке

Восстановление позиции прокрутки

Нам нужно восстановить положение прокрутки после возврата на эту страницу - другими словами, при каждом событии NavigationEnd. Это событие срабатывает каждый раз, когда вы переходите по другому маршруту внутри вашего приложения Angular. Мы не хотим восстанавливать позицию прокрутки для каждой навигации, поэтому нам нужно сопоставить компонент из текущего маршрута с нашим родительским компонентом:

this.config.router.events.subscribe((event: NavigationEnd) => {
  if (event instanceof NavigationEnd 
        && this.config.componentContext 
             instanceof (this.config.route.component as Function)) {
    setTimeout(()=>{
      this.restoreAll();
    })
  }
}, error => console.error(error));

Если вы хотите восстановить какие-либо асинхронные данные, просто объявите настраиваемый эмиттер в родительском компоненте и обработайте его здесь:

if (this.config.componentContext.resultsReady) {
  let subscription = this.config.componentContext.resultsReady.subscribe(()=>{
    setTimeout(()=>{
      this.restoreAll();
    })
    subscription.unsubscribe();
  })
}

Не забудьте отказаться от подписки, иначе ваш свиток будет прыгать каждый раз, когда вы получаете какие-либо данные

Результат

class ElementScroll {
  el: HTMLElement;
  scroll: number;
}

class ScrollStoreConfig {
  componentContext: any;
  router: Router;
  route: ActivatedRoute;
  storage: {
    [key: string]: ElementScroll
  };
}

export class ScrollStoreProvider {
  public config: ScrollStoreConfig = <ScrollStoreConfig>{};

  constructor(options: {
    compContext: any,
    router: Router,
    route: ActivatedRoute
  }) {
    // init
    // subscribe to NavigationEnd
    // subscribe to async updates

  private restoreAll() {...}

  public clearStorage() {...}

  public storeScroll(key: string, el: HTMLElement) {...}

  public handleScroll(key: string, el: HTMLElement) {...}

}

Наконец-то

Я не уверен, что этот подход является лучшим, но он работает, и он надежен, расширяемый и многоразовый. Теперь мы можем восстановить положение прокрутки для наших списков и сеток.

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