Возьмите N значений из Observable до его завершения на основе события.Ленивая загрузка списка выбора нескольких - PullRequest
0 голосов
/ 24 февраля 2019

Я новичок в rxjs, и я разрабатываю компонент углового множественного выбора, который должен отображать длинный список значений (более 500).Я рендеринг списка на основе UL, я перебираю наблюдаемые, которые будут визуализировать LI.Я думаю о своих возможностях избежать влияния на производительность, отрисовывая все элементы одновременно.Но я не знаю, возможно ли это или нет, и если это возможно, какой оператор лучше всего использовать.

Предлагаемое решение:

  • При инициализации я загружаю все данные в Observable.(src), я возьму из него 100 элементов и поместу их в целевую наблюдаемую область (ту, которая будет использоваться для визуализации списка)
  • Каждый раз, когда пользователь достигает концаlist (запускается событие scrollEnd) Я буду загружать еще 100 элементов, пока в src не будет больше значений.

  • Будет сгенерировано излучение новых значений в целевой наблюдаемой.по событию scrollEnd.

Найдите мой код ниже, мне все еще нужно реализовать предложенное решение, но я застрял в этой точке.

РЕДАКТИРОВАТЬ: я внедряю решение @martin, но все еще не могу заставить его работать в моем коде.Моим первым шагом было скопировать его в коде, чтобы получить записанные значения, но наблюдаемое завершается немедленно, без создания каких-либо значений.Вместо того, чтобы вызвать событие, я добавил тему.Каждый раз, когда выводится scrollindEnd, я добавляю новое значение в тему.Шаблон был изменен для поддержки этого.

multiselect.component.ts

import { Component, AfterViewInit } from '@angular/core';
import { zip, Observable, fromEvent, range } from 'rxjs';
import { map, bufferCount, startWith, scan } from 'rxjs/operators';
import { MultiSelectService, ProductCategory } from './multiselect.service';

@Component({
  selector: 'multiselect',
  templateUrl: './multiselect.component.html',
  styleUrls: ['./multiselect.component.scss']
})
export class MultiselectComponent implements AfterViewInit {

  SLICE_SIZE = 100;
  loadMore$: Observable<Event>;
  numbers$ = range(450);

  constructor() {}


  ngAfterViewInit() {
    this.loadMore$ = fromEvent(document.getElementsByTagName('button')[0], 'click');

    zip(
      this.numbers$.pipe(bufferCount(this.SLICE_SIZE)),
      this.loadMore$.pipe(),
    ).pipe(
      map(results => console.log(results)),
    ).subscribe({
      next: v => console.log(v),
      complete: () => console.log('complete ...'),
    });
  }

}

multiselect.component.html

<form action="#" class="multiselect-form">
  <h3>Categories</h3>
  <input type="text" placeholder="Search..." class="multiselect-form--search" tabindex="0"/>
  <multiselect-list [categories]="categories$ | async" (scrollingFinished)="lazySubject.next($event)">
  </multiselect-list>
  <button class="btn-primary--large">Proceed</button>
</form>

multiselect-list.component.ts

import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'multiselect-list',
  templateUrl: './multiselect-list.component.html'
})
export class MultiselectListComponent {
  @Output() scrollingFinished = new EventEmitter<any>();
  @Input() categories: Array<string> = [];

  constructor() {}

  onScrollingFinished() {
    this.scrollingFinished.emit(null);
  }
}

multiselect-list.component.html

<ul class="multiselect-list" (scrollingFinished)="onScrollingFinished($event)">
  <li *ngFor="let category of categories; let idx=index" scrollTracker class="multiselect-list--option">
    <input type="checkbox" id="{{ category }}" tabindex="{{ idx + 1 }}"/>
    <label for="{{ category }}">{{ category }}</label>
  </li>
</ul>

ПРИМЕЧАНИЕ: Событие scrollingFinished инициируется директивой scrollTracker, которая содержит логику отслеживания.Я передаю событие из списка множественного выбора в компонент множественного выбора.

Заранее спасибо!

Ответы [ 2 ]

0 голосов
/ 24 февраля 2019

В этом примере создается массив из 450 элементов, а затем разбивается на куски по 100.Сначала он сбрасывает первые 100 элементов и после каждого нажатия кнопки берет еще 100 и добавляет его к предыдущим результатам.Эта цепочка правильно завершается после загрузки всех данных.

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

import { fromEvent, range, zip } from 'rxjs'; 
import { map, bufferCount, startWith, scan } from 'rxjs/operators';

const SLICE_SIZE = 100;

const loadMore$ = fromEvent(document.getElementsByTagName('button')[0], 'click');
const data$ = range(450);

zip(
  data$.pipe(bufferCount(SLICE_SIZE)),
  loadMore$.pipe(startWith(0)),
).pipe(
  map(results => results[0]),
  scan((acc, chunk) => [...acc, ...chunk], []),
).subscribe({
  next: v => console.log(v),
  complete: () => console.log('complete'),
});

Демонстрационная версия: https://stackblitz.com/edit/rxjs-au9pt7?file=index.ts

Если вы беспокоитесь о производительности, выследует использовать trackBy для *ngFor, чтобы избежать повторного рендеринга существующих элементов DOM, но я думаю, вы уже это знаете.

0 голосов
/ 24 февраля 2019

Вот демо live на Stackblitz .

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

Для службы используйте BehaviorSubject Внедрить ваши новые предметы в ваш наблюдаемый.Вы можете получить наблюдаемое из него, используя метод asObservable().Используйте другое свойство для хранения вашего текущего списка.Каждый раз, когда вызывается loadMore(), вставляйте новые элементы в ваш список, а затем добавляйте этот список в тему, что также подтолкнет его в наблюдаемую область, и ваши компоненты будут перерисованы.

Здесь яначиная со списка, содержащего все элементы (allCategories), каждый раз, когда вызывается loadMore(), блок из 100 элементов, если он взят и помещен в текущий список с использованием Array.splice():

@Injectable({
  providedIn: 'root'
})
export class MultiSelectService {
  private categoriesSubject = new BehaviorSubject<Array<string>>([]);
  categories$ = this.categoriesSubject.asObservable();
  categories: Array<string> = [];
  allCategories: Array<string> = Array.from({ length: 1000 }, (_, i) => `item #${i}`);

  constructor() {
    this.getNextItems();
    this.categoriesSubject.next(this.categories);
  }

  loadMore(): void {
    if (this.getNextItems()) {
      this.categoriesSubject.next(this.categories);
    }
  }

  getNextItems(): boolean {
    if (this.categories.length >= this.allCategories.length) {
      return false;
    }
    const remainingLength = Math.min(100, this.allCategories.length - this.categories.length);
    this.categories.push(...this.allCategories.slice(this.categories.length, this.categories.length + remainingLength));
    return true;
  }
}

, затем вызовметод loadMore() в вашем сервисе из компонента multiselect при достижении нижнего уровня:

export class MultiselectComponent {
  categories$: Observable<Array<string>>;

  constructor(private dataService: MultiSelectService) {
    this.categories$ = dataService.categories$;
  }

  onScrollingFinished() {
    console.log('load more');
    this.dataService.loadMore();
  }
}

В вашем компоненте multiselect-list поместите директиву scrollTracker на содержащий ul, а нена li:

<ul class="multiselect-list" scrollTracker (scrollingFinished)="onScrollingFinished()">
  <li *ngFor="let category of categories; let idx=index"  class="multiselect-list--option">
    <input type="checkbox" id="{{ category }}" tabindex="{{ idx + 1 }}"/>
    <label for="{{ category }}">{{ category }}</label>
  </li>
</ul>

Чтобы обнаружить прокрутку вниз и запустить событие только один раз, используйте эту логику для реализации вашей директивы scrollTracker:

@Directive({
  selector: '[scrollTracker]'
})
export class ScrollTrackerDirective {
  @Output() scrollingFinished = new EventEmitter<void>();

  emitted = false;

  @HostListener("window:scroll", [])
  onScroll(): void {
    if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight && !this.emitted) {
      this.emitted = true;
      this.scrollingFinished.emit();
    } else if ((window.innerHeight + window.scrollY) < document.body.offsetHeight) {
      this.emitted = false;
    }
  }
}

Надеюсь, это поможет!

...