Бесконечная прокрутка в Angular с помощью CDK

Бесконечная прокрутка в Angular с помощью CDK

От автора: при работе с большим количеством контента может оказаться полезным добавить на ваш сайт функцию бесконечной прокрутки. Когда я говорю «бесконечная прокрутка», я имею в виду страницу, добавляющую новый контент, пока пользователь продолжает прокручивать ее, создавая иллюзию бесконечной прокрутки страницы. Многие веб-сайты используют эту функцию, и она может быть гибкой альтернативой нумерации страниц.

Несмотря на то, что существует множество различных способов реализации этого, давайте рассмотрим, как мы можем сделать это с помощью Angular Component Dev Kit (CDK).

Настройка

Начнем с добавления в проект пакета @angular/cdk:

$ npm install @angular/cdk

Чтобы использовать функции бесконечной прокрутки в этом пакете, импортируйте ScrollingModule в app.module.ts:

import { ScrollingModule} from '@angular/cdk/scrolling';

Затем добавьте его в импорт:

imports: [ ScrollingModule
]

Теперь вы готовы начать!

Реализация бесконечной прокрутки

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

Пока предположим, что у нас есть FactService, который предоставляет только следующую функцию:

getRandomFact();

Мы будем извлекать 10 фактов за раз, и каждый раз, когда пользователь прокручивает страницу до конца, мы запрашиваем еще 10 фактов.

FactScrollerComponent

Создайте новый компонент, который будет действовать как бесконечный скроллер. Я вызываю FactScrollerComponent. Вы можете использовать для этого Angular CLI:

$ ng g component fact-scroller

Убедитесь, что новый компонент импортирован в app.module.ts и добавлен в объявления:

import { ScrollingModule } from '@angular/cdk/scrolling';
declarations: [ FactScrollerComponent
]

В fact-scroller.component.html мы создадим:

<cdk-virtual-scroll-viewport itemSize="100"> <li *cdkVirtualFor="let fact of dataSource"> <!-- Print stuff here --> </li>
</cdk-virtual-scroll-viewport>

Здесь мы используем наш виртуальный скроллер cdk-virtual-scroll-viewport. В нем мы перебираем через цикл элементаы, используя *cdkVirtualFor, что аналогично использованию *ngFor.

Чтобы компонент правильно определил размер внутреннего скроллера, нам нужно сообщить скроллеру, какой будет высота каждого элемента (в пикселях). Это делается с использованием директивы itemSize. Таким образом, itemSize=»100″ означает, что элемент в списке будет иметь высоту 100px.

Мы также указали скроллеру извлечь данные dataSource, которых еще нет, поэтому нужно создать их сейчас.

Пользовательский FactsDataSource

В файле fact-scroller.component.ts нам нужно определить, как выглядит источник данных. Для этого мы расширим класс DataSource в @angular/cdk/collections. Вот как выглядит источник данных:

import { CollectionViewer, DataSource } from '@angular/cdk/collections'; export interface Fact { text?: string; date?: string;
} export class FactsDataSource extends DataSource<Fact | undefined> { private cachedFacts = Array.from<Fact>({ length: 0 }); private dataStream = new BehaviorSubject<(Fact | undefined)[]>(this.cachedFacts); private subscription = new Subscription(); constructor(private factService: FactService) { super(); } connect(collectionViewer: CollectionViewer): Observable<(Fact | undefined)[] | ReadonlyArray<Fact | undefined>> { this.subscription.add(collectionViewer.viewChange.subscribe(range => { // Update the data })); return this.dataStream; } disconnect(collectionViewer: CollectionViewer): void { this.subscription.unsubscribe(); }
}

Здесь много кода, так что давайте разберемся с этим по порядку. Сначала мы определяем модель Fact, которая будет определять структуру данных.

Внутри FactsDataSource нам нужно реализовать две функции: connect() и disconnect(). Источник данных подписывается на любые изменения в средстве просмотра коллекции (например, прокрутки пользователя), а затем выполняет действие и возвращает поток данных. Мы указали источнику данных, чтобы он получал следующие данные, когда мы достигнем конца списка.

Мы также объявили три переменные-члена:

cachedFacts: кэшированные результаты,

dataStream: RxJS BehaviorSubject для распространения изменений в наших кэшированных результатах, и

subscription: подписка для прослушивания изменений коллекции представлений.

Давайте определим несколько вспомогательных функций в этом классе:

private pageSize = 10;
private lastPage = 0; private _fetchFactPage(): void { for (let i = 0; i < this.pageSize; ++i) { this.factService.getRandomFact().subscribe(res => { this.cachedFacts = this.cachedFacts.concat(res); this.dataStream.next(this.cachedFacts); }); }
} private _getPageForIndex(i: number): number { return Math.floor(i / this.pageSize);
}

Я устанавливаю размер страницы — 10, что означает, что я хочу получить 10 фактов за раз. Я также собираюсь отслеживать последнюю загруженную страницу.

_fetchFactPage() просто выполняет вызов к сервису, чтобы получить некоторые факты, которые затем добавляются в кеш.
_getPageForIndex() преобразует индекс строки в значение страницы (или пачки).

Собрав все это вместе, мы можем определить, как список будет обновляться в обратном вызове подписки:

connect(collectionViewer: CollectionViewer): Observable<(Fact | undefined)[] | ReadonlyArray<Fact | undefined>> { this.subscription.add(collectionViewer.viewChange.subscribe(range => { const currentPage = this._getPageForIndex(range.end); if (currentPage > this.lastPage) { this.lastPage = currentPage; this._fetchFactPage(); } })); return this.dataStream;
}

Мы также хотим, чтобы с самого начала у нас уже были некоторые данные, для этого мы можем вызвать функцию fetch в конструкторе:

constructor(private factService: FactService) { super(); // Start with some data. this._fetchFactPage();
}

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

@Component({ selector: 'app-fact-scroller', templateUrl: './fact-scroller.component.html', styleUrls: ['./fact-scroller.component.scss']
})
export class FactScrollerComponent { dataSource: FactsDataSource; constructor(private factService: FactService) { this.dataSource = new FactsDataSource(factService); } }

И все готово! С этого момента все форматируется. Я переписал HTML, чтобы вывести факты таким образом:

<cdk-virtual-scroll-viewport itemSize="100" class="fact-scroll-viewport"> <li *cdkVirtualFor="let fact of dataSource"> <div *ngIf="fact" class="fact-item"> <div class="fact-date">{{ fact.year }}</div> <div class="fact-text">{{ fact.text }}</div> </div> <div *ngIf="!fact"> Loading ... </div> </li>
</cdk-virtual-scroll-viewport>

@Component({ selector: 'app-fact-scroller', templateUrl: './fact-scroller.component.html', styleUrls: ['./fact-scroller.component.scss']
})
export class FactScrollerComponent { dataSource: FactsDataSource; constructor(private factService: FactService) { this.dataSource = new FactsDataSource(factService); } }

Удачной прокрутки!

Автор: Chris Engelsma

Источник: https://alligator.io

Редакция: Команда webformyself.