Skip to content

Commit

Permalink
feat: add pagination methods to withEntitiesRemoteScrollPagination, a…
Browse files Browse the repository at this point in the history
…nd preload page logic
  • Loading branch information
Gabriel Guerrero authored and gabrielguerrero committed Apr 29, 2024
1 parent 8d9670a commit 3298a13
Show file tree
Hide file tree
Showing 14 changed files with 630 additions and 135 deletions.
9 changes: 8 additions & 1 deletion apps/example-app/src/app/examples/examples-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,19 @@ const routes: Routes = [
).then((m) => m.SignalProductListPaginatedPageContainerComponent),
},
{
path: 'infinite-scroll',
path: 'infinite-scroll-dropdown',
loadComponent: () =>
import(
'./signals/infinete-scroll-page/infinite-scroll-page.component'
).then((m) => m.InfiniteScrollPageComponent),
},
{
path: 'infinite-scroll-list',
loadComponent: () =>
import(
'./signals/infinete-scroll-page/products-branch-list.component'
).then((m) => m.ProductsBranchListComponent),
},
],
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';

import { ProductsBranchDropdownComponent } from './components/products-branch-dropdown/products-branch-dropdown.component';
import { ProductsBranchDropdownComponent } from './products-branch-dropdown.component';
import { ProductsBranchListComponent } from './products-branch-list.component';
import { ProductsBranchStore } from './products-branch.store';

@Component({
selector: 'infinite-scroll-page',
standalone: true,
imports: [CommonModule, ProductsBranchDropdownComponent],
imports: [
CommonModule,
ProductsBranchDropdownComponent,
ProductsBranchListComponent,
],
template: `<products-branch-dropdown />`,
styles: ``,
providers: [ProductsBranchStore],
})
export class InfiniteScrollPageComponent {}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { MatOption, MatSelect } from '@angular/material/select';
import { getInfiniteScrollDataSource } from '@ngrx-traits/signals';

import { SearchOptionsComponent } from '../../../../components/search-options/search-options.component';
import { Branch } from '../../../../models';
import { SearchOptionsComponent } from '../../components/search-options/search-options.component';
import { Branch } from '../../models';
import { ProductsBranchStore } from './products-branch.store';

@Component({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { BreakpointObserver } from '@angular/cdk/layout';
import {
CdkFixedSizeVirtualScroll,
CdkVirtualForOf,
CdkVirtualScrollViewport,
} from '@angular/cdk/scrolling';
import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { FormsModule } from '@angular/forms';
import { MatButton } from '@angular/material/button';
import { MatFormField, MatLabel } from '@angular/material/form-field';
import { MatIcon } from '@angular/material/icon';
import { MatInput } from '@angular/material/input';
import { MatList, MatListItem } from '@angular/material/list';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { getInfiniteScrollDataSource } from '@ngrx-traits/signals';
import { map } from 'rxjs/operators';

import { Branch } from '../../models';
import { ProductsBranchStore } from './products-branch.store';

@Component({
selector: 'products-branch-list',
standalone: true,
imports: [
CommonModule,
MatList,
MatListItem,
MatButton,
MatIcon,
MatFormField,
MatInput,
FormsModule,
MatLabel,
MatProgressSpinner,
CdkVirtualScrollViewport,
CdkFixedSizeVirtualScroll,
CdkVirtualForOf,
],
template: `
<div class="grid grid-rows-[auto_1fr]">
<form class="p-8 pb-0">
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-label>Search</mat-label>
<input
type="text"
matInput
[ngModel]="store.entitiesFilter().search"
name="search"
(ngModelChange)="
store.filterEntities({ filter: { search: $event } })
"
/>
</mat-form-field>
</form>
@if (isMobile()) {
<cdk-virtual-scroll-viewport
itemSize="42"
class="fact-scroll-viewport"
minBufferPx="200"
maxBufferPx="200"
>
<mat-list>
<mat-list-item
*cdkVirtualFor="let item of dataSource; trackBy: trackByFn"
>{{ item.name }}</mat-list-item
>
</mat-list>
</cdk-virtual-scroll-viewport>
} @else {
@if (store.entitiesCurrentPage().isLoading) {
<mat-spinner />
} @else {
<mat-list>
@for (
product of store.entitiesCurrentPage().entities;
track product.id
) {
<mat-list-item>{{ product.name }}</mat-list-item>
}
</mat-list>
<div>
<button
mat-button
[disabled]="!store.entitiesCurrentPage().hasPrevious"
(click)="store.loadEntitiesPreviousPage()"
>
previous
</button>
<button
mat-button
[disabled]="!store.entitiesCurrentPage().hasNext"
(click)="store.loadEntitiesNextPage()"
>
next
</button>
</div>
}
}
</div>
`,
styles: `
:host {
display: block;
height: 100dvh;
}
.fact-scroll-viewport {
display: flex;
flex-direction: column;
box-sizing: border-box;
overflow-y: auto;
overflow-x: hidden;
height: 80dvh;
width: 100%;
}
`,
providers: [ProductsBranchStore],
})
export class ProductsBranchListComponent {
store = inject(ProductsBranchStore);
dataSource = getInfiniteScrollDataSource({ store: this.store });
breakpointObserver = inject(BreakpointObserver);
isMobile = toSignal(
this.breakpointObserver
.observe('(max-width: 640px)')
.pipe(map((result) => result.matches)),
);

trackByFn(index: number, item: Branch) {
return item.id;
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { inject } from '@angular/core';
import {
getInfiniteScrollDataSource,
withCallStatus,
withEntitiesLoadingCall,
withEntitiesRemoteFilter,
withEntitiesRemoteScrollPagination,
withLogger,
} from '@ngrx-traits/signals';
import { signalStore, type } from '@ngrx/signals';
import { withEntities } from '@ngrx/signals/entities';
import { lastValueFrom } from 'rxjs';

import { Branch } from '../../../../models';
import { BranchService } from '../../../../services/branch.service';
import { Branch } from '../../models';
import { BranchService } from '../../services/branch.service';

const entity = type<Branch>();
export const ProductsBranchStore = signalStore(
Expand All @@ -25,7 +23,7 @@ export const ProductsBranchStore = signalStore(
defaultFilter: { search: '' },
}),
withEntitiesRemoteScrollPagination({
bufferSize: 30,
pageSize: 10,
entity,
}),
withEntitiesLoadingCall({
Expand All @@ -37,8 +35,7 @@ export const ProductsBranchStore = signalStore(
take: entitiesPagedRequest().size,
}),
);
return { entities: res.resultList, total: res.total };
return { entities: res.resultList };
},
}),
withLogger('branchStore'),
);
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,19 @@ import { RouterLink } from '@angular/router';
</mat-card-header>
<mat-card-content>
<mat-list>
<mat-list-item [routerLink]="'infinite-scroll'">
<mat-list-item [routerLink]="'infinite-scroll-dropdown'">
<div matListItemTitle><b>Infinite Scroll Dropdown</b></div>
<div matListItemLine>
Example using trait to load a product list with filtering and
sorting in memory
Example of withEntitiesRemoteScrollPagination used to create a
dropdown with infinite scroll
</div>
</mat-list-item>
<mat-list-item [routerLink]="'infinite-scroll-list'">
<div matListItemTitle><b>Infinite Scroll List</b></div>
<div matListItemLine>
Example of withEntitiesRemoteScrollPagination used to create a
list tha is paginated for desktop mode and uses an infinite scroll
in mobile mode
</div>
</mat-list-item>
<mat-divider></mat-divider>
Expand All @@ -33,10 +41,11 @@ import { RouterLink } from '@angular/router';
<mat-list-item [routerLink]="'product-list-paginated'">
<div matListItemTitle><b>Paginated List</b></div>
<div matListItemLine>
Example using trait to load a product list with remote filtering
and sorting and pagination
Example using store features to load a product list with remote
filtering and detail view
</div>
</mat-list-item>
<!-- <mat-divider></mat-divider>-->
<!-- <mat-list-item [routerLink]="'product-picker'">-->
<!-- <div matListItemTitle>-->
Expand Down
5 changes: 5 additions & 0 deletions apps/example-app/src/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,8 @@ $angular-client-theme: mat.define-dark-theme(
@tailwind base;
@tailwind components;
@tailwind utilities;

.mdc-notched-outline__notch
{
border-right: none;
}
8 changes: 4 additions & 4 deletions libs/ngrx-traits/signals/api-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ or a hasMore param set[Collection]Result({entities, hasMore}) that you can set t
| Param | Description |
| --- | --- |
| config | |
| config.bufferSize | <p>The number of entities to show per page</p> |
| config.pageSize | <p>The number of entities to show per page</p> |
| config.entity | <p>The entity type</p> |
| config.collection | <p>The name of the collection</p> |
Expand All @@ -544,7 +544,7 @@ export const store = signalStore(
withEntitiesRemoteScrollPagination({
entity,
collection,
bufferSize: 5,
pageSize: 5,
pagesToCache: 2,
})
// after you can use withEntitiesLoadingCall to connect the filter to
Expand Down Expand Up @@ -598,9 +598,9 @@ export const store = signalStore(
store = inject(ProductsRemoteStore);
dataSource = getInfiniteScrollDataSource(store, { collection: 'products' }) // pass this to your cdkVirtualFor see examples section
// generates the following signals
store.productsPagination // { currentPage: number, requestPage: number, bufferSize: 5, total: number, pagesToCache: number, cache: { start: number, end: number } } used internally
store.productsPagination // { currentPage: number, requestPage: number, pageSize: 5, total: number, pagesToCache: number, cache: { start: number, end: number } } used internally
// generates the following computed signals
store.productsPageInfo // { pageIndex: number, total: number, bufferSize: 5, pagesCount: number, hasPrevious: boolean, hasNext: boolean, isLoading: boolean }
store.productsPageInfo // { pageIndex: number, total: number, pageSize: 5, pagesCount: number, hasPrevious: boolean, hasNext: boolean, isLoading: boolean }
store.productsPagedRequest // { startIndex: number, size: number, page: number }
// generates the following methods
store.loadProductsNextPage() // loads next page
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
EntitySignals,
NamedEntitySignals,
} from '@ngrx/signals/entities/src/models';
import { Observable, Subscription } from 'rxjs';
import { debounceTime, Observable, Subscription } from 'rxjs';
import { filter } from 'rxjs/operators';

import { getWithEntitiesKeys } from '../util';
Expand All @@ -20,25 +20,26 @@ export function getInfiniteScrollDataSource<Entity, Collection extends string>(
options:
| {
store: EntitySignals<Entity> & EntitiesScrollPaginationMethods<Entity>;
debounceLoadMoreTime?: number;
}
| {
collection: Collection;
entity: Entity;
store: NamedEntitySignals<Entity, Collection> &
NamedEntitiesScrollPaginationMethods<Entity, Collection>;
debounceLoadMoreTime?: number;
},
) {
const collection = 'collection' in options ? options.collection : undefined;
const { loadMoreEntitiesKey, entitiesScrollCacheKey } =
const debounceLoadMoreTime = options.debounceLoadMoreTime ?? 300;
const { loadMoreEntitiesKey, paginationKey } =
getWithEntitiesInfinitePaginationKeys({
collection,
});
const { entitiesKey } = getWithEntitiesKeys({ collection });
const store = options.store as Record<string, unknown>;
const entities = store[entitiesKey] as Signal<Entity[]>;
const entitiesScrollCache = store[
entitiesScrollCacheKey
] as Signal<ScrollPaginationState>;
const pagination = store[paginationKey] as Signal<ScrollPaginationState>;
const loadMoreEntities = store[loadMoreEntitiesKey] as () => void;

class MyDataSource extends DataSource<Entity> {
Expand All @@ -48,12 +49,10 @@ export function getInfiniteScrollDataSource<Entity, Collection extends string>(
this.subscription = collectionViewer.viewChange
.pipe(
filter(({ end, start }) => {
const { bufferSize, hasMore } = entitiesScrollCache();
// filter first request that is done by the cdkscroll,
// filter last request
// only do requests when you pass a specific threshold
return start != 0 && hasMore && end + bufferSize >= entities.length;
const { pageSize, hasMore } = pagination();
return start != 0 && hasMore && end + pageSize >= entities().length;
}),
debounceTime(debounceLoadMoreTime),
)
.subscribe(() => {
loadMoreEntities();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,11 @@ export function withEntitiesRemotePagination<
* and call the api with the [collection]PagedRequest params and use set[Collection]Result to set the result
* and changing the status errors manually
* or use withEntitiesLoadingCall to call the api with the [collection]PagedRequest params which handles setting
* the result and errors automatically.
* the result and errors automatically. Requires withEntities and withCallStatus to be used.
* This will only keeps pagesToCache pages in memory, so previous pages will be removed from the cache.
* If you need to keep all previous pages in memory, use withEntitiesRemoteScrollPagination instead.
*
* Requires withEntities and withCallStatus to be present before this function.
* Requires withEntities and withCallStatus to be present in the store.
* @param config
* @param config.pageSize - The number of entities to show per page
* @param config.currentPage - The current page to show
Expand Down Expand Up @@ -214,7 +216,10 @@ export function withEntitiesRemotePagination<
* // .pipe(
* // takeUntilDestroyed(),
* // tap((res) =>
* // setProductsPagedResult({ entities: res.resultList, total: res.total } )
* // patchState(
* // state,
* // setProductsPagedResult({ entities: res.resultList, total: res.total } ),
* // ),
* // ),
* // catchError((error) => {
* // setProductsError(error);
Expand Down Expand Up @@ -266,6 +271,9 @@ export function withEntitiesRemotePagination<
* and changing the status errors manually
* or use withEntitiesLoadingCall to call the api with the [collection]PagedRequest params which handles setting
* the result and errors automatically. Requires withEntities and withCallStatus to be used.
* This will only keeps pagesToCache pages in memory, so previous pages will be removed from the cache.
* If you need to keep all previous pages in memory, use withEntitiesRemoteScrollPagination instead.
*
* Requires withEntities and withCallStatus to be present in the store.
* @param config
* @param config.pageSize - The number of entities to show per page
Expand Down
Loading

0 comments on commit 3298a13

Please sign in to comment.