Skip to content

Commit 818dc25

Browse files
authored
feat(pagination): new segmentation primitive, jump pages, fix set page to non-existent page, fix demo (#815)
2 parents 6dde0a6 + 2a3f9c1 commit 818dc25

File tree

9 files changed

+265
-32
lines changed

9 files changed

+265
-32
lines changed

.changeset/beige-icons-visit.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@solid-primitives/pagination": minor
3+
---
4+
5+
new segmentation primitive, jump pages, fix set page to non-existent page, fix demo

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ The goal of Solid Primitives is to wrap client and server side functionality to
103103
|<h4>*UI Patterns*</h4>|
104104
|[marker](https://github.com/solidjs-community/solid-primitives/tree/main/packages/marker#readme)|[![STAGE](https://img.shields.io/endpoint?style=for-the-badge&label=&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-1.json)](https://github.com/solidjs-community/solid-primitives/blob/main/CONTRIBUTING.md#contribution-process)|[createMarker](https://github.com/solidjs-community/solid-primitives/tree/main/packages/marker#createmarker)|[![SIZE](https://img.shields.io/bundlephobia/minzip/@solid-primitives/marker?style=for-the-badge&label=)](https://bundlephobia.com/package/@solid-primitives/marker)|[![VERSION](https://img.shields.io/npm/v/@solid-primitives/marker?style=for-the-badge&label=)](https://www.npmjs.com/package/@solid-primitives/marker)|
105105
|[masonry](https://github.com/solidjs-community/solid-primitives/tree/main/packages/masonry#readme)|[![STAGE](https://img.shields.io/endpoint?style=for-the-badge&label=&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-0.json)](https://github.com/solidjs-community/solid-primitives/blob/main/CONTRIBUTING.md#contribution-process)|[createMasonry](https://github.com/solidjs-community/solid-primitives/tree/main/packages/masonry#createmasonry)|[![SIZE](https://img.shields.io/bundlephobia/minzip/@solid-primitives/masonry?style=for-the-badge&label=)](https://bundlephobia.com/package/@solid-primitives/masonry)|[![VERSION](https://img.shields.io/npm/v/@solid-primitives/masonry?style=for-the-badge&label=)](https://www.npmjs.com/package/@solid-primitives/masonry)|
106-
|[pagination](https://github.com/solidjs-community/solid-primitives/tree/main/packages/pagination#readme)|[![STAGE](https://img.shields.io/endpoint?style=for-the-badge&label=&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-0.json)](https://github.com/solidjs-community/solid-primitives/blob/main/CONTRIBUTING.md#contribution-process)|[createPagination](https://github.com/solidjs-community/solid-primitives/tree/main/packages/pagination#createpagination)<br />[createInfiniteScroll](https://github.com/solidjs-community/solid-primitives/tree/main/packages/pagination#createinfinitescroll)|[![SIZE](https://img.shields.io/bundlephobia/minzip/@solid-primitives/pagination?style=for-the-badge&label=)](https://bundlephobia.com/package/@solid-primitives/pagination)|[![VERSION](https://img.shields.io/npm/v/@solid-primitives/pagination?style=for-the-badge&label=)](https://www.npmjs.com/package/@solid-primitives/pagination)|
106+
|[pagination](https://github.com/solidjs-community/solid-primitives/tree/main/packages/pagination#readme)|[![STAGE](https://img.shields.io/endpoint?style=for-the-badge&label=&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-0.json)](https://github.com/solidjs-community/solid-primitives/blob/main/CONTRIBUTING.md#contribution-process)|[createPagination](https://github.com/solidjs-community/solid-primitives/tree/main/packages/pagination#createpagination)<br />[createSegment](https://github.com/solidjs-community/solid-primitives/tree/main/packages/pagination#createsegment)<br />[createInfiniteScroll](https://github.com/solidjs-community/solid-primitives/tree/main/packages/pagination#createinfinitescroll)|[![SIZE](https://img.shields.io/bundlephobia/minzip/@solid-primitives/pagination?style=for-the-badge&label=)](https://bundlephobia.com/package/@solid-primitives/pagination)|[![VERSION](https://img.shields.io/npm/v/@solid-primitives/pagination?style=for-the-badge&label=)](https://www.npmjs.com/package/@solid-primitives/pagination)|
107107
|[virtual](https://github.com/solidjs-community/solid-primitives/tree/main/packages/virtual#readme)|[![STAGE](https://img.shields.io/endpoint?style=for-the-badge&label=&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-0.json)](https://github.com/solidjs-community/solid-primitives/blob/main/CONTRIBUTING.md#contribution-process)|[createVirutalList](https://github.com/solidjs-community/solid-primitives/tree/main/packages/virtual#createvirutallist)<br />[VirtualList](https://github.com/solidjs-community/solid-primitives/tree/main/packages/virtual#virtuallist)|[![SIZE](https://img.shields.io/bundlephobia/minzip/@solid-primitives/virtual?style=for-the-badge&label=)](https://bundlephobia.com/package/@solid-primitives/virtual)|[![VERSION](https://img.shields.io/npm/v/@solid-primitives/virtual?style=for-the-badge&label=)](https://www.npmjs.com/package/@solid-primitives/virtual)|
108108
|<h4>*Animation*</h4>|
109109
|[presence](https://github.com/solidjs-community/solid-primitives/tree/main/packages/presence#readme)|[![STAGE](https://img.shields.io/endpoint?style=for-the-badge&label=&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-0.json)](https://github.com/solidjs-community/solid-primitives/blob/main/CONTRIBUTING.md#contribution-process)|[createPresence](https://github.com/solidjs-community/solid-primitives/tree/main/packages/presence#createpresence)|[![SIZE](https://img.shields.io/bundlephobia/minzip/@solid-primitives/presence?style=for-the-badge&label=)](https://bundlephobia.com/package/@solid-primitives/presence)|[![VERSION](https://img.shields.io/npm/v/@solid-primitives/presence?style=for-the-badge&label=)](https://www.npmjs.com/package/@solid-primitives/presence)|

packages/pagination/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
A primitive that creates all the reactive data to manage your pagination:
1212

1313
- [`createPagination`](#createpagination) - Provides an array with the properties to fill your pagination with and a page setter/getter.
14+
- [`createSegment`](#createSegment) - Provides a reactive segment of an array (e.g. a page of a number of items).
1415
- [`createInfiniteScroll`](#createinfinitescroll) - Provides an easy way to implement infinite scrolling.
1516

1617
## Installation
@@ -53,6 +54,8 @@ type PaginationOptions = {
5354
nextContent?: JSX.Element;
5455
/** content for the last page element, e.g. an SVG icon, default is ">|" */
5556
lastContent?: JSX.Element;
57+
/** number of pages a large jump, if it should exist, should skip */
58+
jumpPages?: number;
5659
};
5760

5861
// Returns a tuple of props, page and setPage.
@@ -109,11 +112,29 @@ return (
109112
- options for aria-labels
110113
- optional: touch controls
111114

115+
## `createSegment`
116+
117+
It is a common requirement to put multiple items on a single page, which exactly is what `createSegment` is for.
118+
119+
```tsx
120+
const segment = createSegment(items, limit, page);
121+
122+
return (
123+
<For each={segment()}>{(item) => <Item item={item} />}</For>
124+
)
125+
```
126+
127+
* `items` can be any array of items or an accessor with an array of items; even if the array increases in size, the segment will only change if the growth brings an actual change
128+
* `limit` is the limit for the number of items within a segment; this can be a number or an accessor containing a number
129+
* `page` is an accessor with the number or the segment page, starting with 1
130+
131+
112132
### Demo
113133

114134
You may view a working example here:
115135
https://primitives.solidjs.community/playground/pagination/
116136

137+
117138
## `createInfiniteScroll`
118139

119140
Combines [`createResource`](https://www.solidjs.com/docs/latest/api#createresource) with [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) to provide an easy way to implement infinite scrolling.

packages/pagination/dev/index.tsx

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { type Component, For, Show } from "solid-js";
1+
import { type Component, For, Show, createSignal } from "solid-js";
22

3-
import { createInfiniteScroll, createPagination } from "../src/index.js";
3+
import { createInfiniteScroll, createSegment, createPagination } from "../src/index.js";
44

55
async function fetcher(page: number) {
66
let elements: string[] = [];
@@ -14,37 +14,45 @@ async function fetcher(page: number) {
1414
return elements;
1515
}
1616

17-
const PaginationDemo: Component = () => {
18-
const [paginationProps, page, setPage] = createPagination({ pages: 100 });
17+
const PaginationSegmentationDemo: Component = () => {
18+
const items = Array.from({ length: 1000 }, (_, i) => i + 1);
19+
const [limit, setLimit] = createSignal(10);
20+
const [paginationProps, page, setPage] = createPagination(() => ({
21+
pages: Math.ceil(items.length / limit()),
22+
maxPages: 8,
23+
jumpPages: 10,
24+
}));
25+
const segment = createSegment(items, limit, page);
1926

2027
return (
21-
<div class="box-border flex w-1/2 flex-col items-center justify-center space-y-4 bg-gray-800 p-24 text-white">
22-
<div class="wrapper-v">
23-
<h4>Pagination component</h4>
24-
<p>Current page: {page()} / 100</p>
25-
<nav class="flex flex-row">
26-
<For each={paginationProps()}>{props => <button {...props} />}</For>
27-
</nav>
28-
<button onClick={() => setPage(Math.round(Math.random() * 100 + 1))}>
29-
jump to random page
30-
</button>
28+
<div class="box-border flex w-1/2 mx-auto flex-col items-center justify-center space-y-4 bg-gray-800 p-24 text-white">
29+
<h4 class="text-xl">Pagination & Segmentation:</h4>
30+
<For each={segment()}>{(i) => (<p class="border-1 w-full text-center">{i}</p>)}</For>
31+
<nav class="flex flex-row">
32+
<For each={paginationProps()}>{props => <button class="whitespace-nowrap" {...props} />}</For>
33+
</nav>
34+
<div class="flex flex-row">
35+
<button onClick={() => setLimit(5)} disabled={limit() === 5}>5 items/page</button>
36+
<button onClick={() => setLimit(10)} disabled={limit() === 10}>10 items/page</button>
37+
<button onClick={() => setLimit(20)} disabled={limit() === 20}>20 items/page</button>
38+
<button onClick={() => setPage(Math.round(Math.random() * 1000 / limit() + 1))}>jump to random page</button>
3139
</div>
3240
</div>
3341
);
34-
};
42+
}
3543

3644
const InfiniteScrollDemo = () => {
3745
const [pages, infiniteScrollLoader, { end }] = createInfiniteScroll(fetcher);
3846
infiniteScrollLoader;
3947
return (
40-
<div class="flex max-h-screen w-1/2 flex-col bg-gray-800 text-white">
41-
<div class="flex h-[10%] items-center justify-center overflow-scroll">
42-
<h1>Infinite Scrolling:</h1>
48+
<div class="flex max-h-screen w-1/2 mx-auto mt-10 p-4 flex-col bg-gray-800 text-white">
49+
<div class="flex items-center justify-center">
50+
<h4 class="text-xl">Infinite Scrolling:</h4>
4351
</div>
4452
<div class="h-[90%] overflow-scroll">
4553
<For each={pages()}>{item => <p>{item}</p>}</For>
4654
<Show when={!end()}>
47-
<h1 use:infiniteScrollLoader>Loading...</h1>
55+
<span use:infiniteScrollLoader>Loading...</span>
4856
</Show>
4957
</div>
5058
</div>
@@ -53,8 +61,8 @@ const InfiniteScrollDemo = () => {
5361

5462
const App: Component = () => {
5563
return (
56-
<div class="flex min-h-screen w-full">
57-
<PaginationDemo />
64+
<div class="flex flex-col min-h-screen w-full">
65+
<PaginationSegmentationDemo />
5866
<InfiniteScrollDemo />
5967
</div>
6068
);

packages/pagination/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"stage": 0,
2222
"list": [
2323
"createPagination",
24+
"createSegment",
2425
"createInfiniteScroll"
2526
],
2627
"category": "UI Patterns"

packages/pagination/src/index.ts

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,43 @@ import {
99
createResource,
1010
createSignal,
1111
onCleanup,
12+
untrack,
1213
} from "solid-js";
1314
import { isServer } from "solid-js/web";
1415

16+
/**
17+
* createSegment - create a reactive segment out of an array of items
18+
* @param {MaybeAccessor<any[]>} items - array of items
19+
* @param {MaybeAccessor<number>} limit - limit of items per segment
20+
* @param {Accessor<number>} page - segment number starting with 1
21+
*
22+
* ```ts
23+
* const [limit, setLimit] = createSignal(10);
24+
* const [paginationProps, page, setPage] = createPagination(() => ({
25+
* pages: Math.ceil(items().length / limit())
26+
* }));
27+
* const segment = createSegment(items, limit, page);
28+
* ```
29+
*/
30+
export const createSegment = <T>(
31+
items: MaybeAccessor<T[]>,
32+
limit: MaybeAccessor<number>,
33+
page: Accessor<number>
34+
): Accessor<T[]> => {
35+
let previousStart = NaN, previousEnd = NaN;
36+
return createMemo((previous) => {
37+
const currentItems = access(items);
38+
const start = (page() - 1) * access(limit);
39+
const end = Math.min(start + access(limit), currentItems.length);
40+
if (previous && (previous.length === 0 && end <= start || start === previousStart && end === previousEnd)) {
41+
return previous;
42+
}
43+
previousStart = start;
44+
previousEnd = end;
45+
return currentItems.slice(start, end);
46+
});
47+
}
48+
1549
export type PaginationOptions = {
1650
/** the overall number of pages */
1751
pages: number;
@@ -35,6 +69,8 @@ export type PaginationOptions = {
3569
nextContent?: JSX.Element;
3670
/** content for the last page element, e.g. an SVG icon, default is ">|" */
3771
lastContent?: JSX.Element;
72+
/** number of pages a large jump, if it should exist, should skip */
73+
jumpPages?: number;
3874
};
3975

4076
export type PaginationProps = {
@@ -98,22 +134,43 @@ export const createPagination = (
98134
): [props: Accessor<PaginationProps>, page: Accessor<number>, setPage: Setter<number>] => {
99135
const opts = createMemo(() => Object.assign({}, PAGINATION_DEFAULTS, access(options)));
100136
const [page, _setPage] = createSignal(opts().initialPage || 1);
137+
138+
// do not allow pages beyond the number of pages in case the latter changes
101139
const setPage = (p: number | ((_p: number) => number)) => {
102140
if (typeof p === "function") {
103141
p = p(page());
104142
}
105-
return p >= 1 && p <= opts().pages ? _setPage(p) : page();
143+
if (p < 1) {
144+
return _setPage(1);
145+
}
146+
const pages = opts().pages;
147+
if (p > pages) {
148+
return _setPage(pages);
149+
}
150+
return _setPage(p);
106151
};
107152

153+
// normalize in case the number of pages changes, do not run the first time
154+
createComputed((previous) => {
155+
opts().pages;
156+
return previous ? setPage(untrack(page)) : true;
157+
});
158+
159+
const goPage = (p: number | ((p: number) => number), ev: KeyboardEvent) => {
160+
setPage(p);
161+
if ('currentTarget' in ev)
162+
(ev.currentTarget as HTMLElement).parentNode?.querySelector<HTMLElement>('[aria-current="true"]')?.focus();
163+
}
164+
108165
const onKeyUp = (pageNo: number, ev: KeyboardEvent) =>
109166
(
110167
({
111-
ArrowLeft: () => setPage(p => p - 1),
112-
ArrowRight: () => setPage(p => p + 1),
113-
Home: () => setPage(1),
114-
End: () => setPage(opts().pages),
115-
Space: () => setPage(pageNo),
116-
Return: () => setPage(pageNo),
168+
ArrowLeft: () => goPage(p => p - 1, ev),
169+
ArrowRight: () => goPage(p => p + 1, ev),
170+
Home: () => goPage(1, ev),
171+
End: () => goPage(opts().pages, ev),
172+
Space: () => goPage(pageNo, ev),
173+
Return: () => goPage(pageNo, ev),
117174
})[ev.key] || noop
118175
)();
119176

@@ -197,6 +254,32 @@ export const createPagination = (
197254
page: { get: () => opts().pages, enumerable: false },
198255
},
199256
);
257+
const jumpBack = Object.defineProperties(
258+
isServer
259+
? ({} as PaginationProps[number])
260+
: ({
261+
onClick: () => setPage(Math.max(1, page() - (opts().jumpPages || Infinity))),
262+
onKeyUp: (ev: KeyboardEvent) => onKeyUp(page() - (opts().jumpPages || Infinity), ev),
263+
} as unknown as PaginationProps[number]),
264+
{
265+
disabled: { get: () => page() - (opts().jumpPages || Infinity) < 0, set: noop, enumerable: true },
266+
children: { get: () => `-${opts().jumpPages}`, set: noop, enumerable: true },
267+
page: { get: () => Math.max(1, page() - (opts().jumpPages || Infinity)), enumerable: false },
268+
}
269+
);
270+
const jumpForth = Object.defineProperties(
271+
isServer
272+
? ({} as PaginationProps[number])
273+
: ({
274+
onClick: () => setPage(Math.min(opts().pages, page() + (opts().jumpPages || Infinity))),
275+
onKeyUp: (ev: KeyboardEvent) => onKeyUp(Math.min(opts().pages, page() + (opts().jumpPages || Infinity)), ev),
276+
} as unknown as PaginationProps[number]),
277+
{
278+
disabled: { get: () => page() + (opts().jumpPages || Infinity) > opts().pages, set: noop, enumerable: true },
279+
children: { get: () => `+${opts().jumpPages}`, set: noop, enumerable: true },
280+
page: { get: () => Math.min(opts().pages, page() + (opts().jumpPages || Infinity)), enumerable: false },
281+
}
282+
)
200283

201284
const start = createMemo(() =>
202285
Math.min(opts().pages - maxPages(), Math.max(1, page() - (maxPages() >> 1)) - 1),
@@ -219,13 +302,19 @@ export const createPagination = (
219302
if (showFirst()) {
220303
props.push(first);
221304
}
305+
if (opts().jumpPages) {
306+
props.push(jumpBack);
307+
}
222308
if (showPrev()) {
223309
props.push(back);
224310
}
225311
props.push(...pages().slice(start(), start() + maxPages()));
226312
if (showNext()) {
227313
props.push(next);
228314
}
315+
if (opts().jumpPages) {
316+
props.push(jumpForth);
317+
}
229318
if (showLast()) {
230319
props.push(last);
231320
}

0 commit comments

Comments
 (0)