Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/beige-icons-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solid-primitives/pagination": minor
---

new segmentation primitive, jump pages, fix set page to non-existent page, fix demo
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ The goal of Solid Primitives is to wrap client and server side functionality to
|<h4>*UI Patterns*</h4>|
|[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)|
|[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)|
|[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)|
|[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)|
|[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)|
|<h4>*Animation*</h4>|
|[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)|
Expand Down
21 changes: 21 additions & 0 deletions packages/pagination/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
A primitive that creates all the reactive data to manage your pagination:

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

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

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

## `createSegment`

It is a common requirement to put multiple items on a single page, which exactly is what `createSegment` is for.

```tsx
const segment = createSegment(items, limit, page);

return (
<For each={segment()}>{(item) => <Item item={item} />}</For>
)
```

* `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
* `limit` is the limit for the number of items within a segment; this can be a number or an accessor containing a number
* `page` is an accessor with the number or the segment page, starting with 1


### Demo

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


## `createInfiniteScroll`

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.
Expand Down
50 changes: 29 additions & 21 deletions packages/pagination/dev/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type Component, For, Show } from "solid-js";
import { type Component, For, Show, createSignal } from "solid-js";

import { createInfiniteScroll, createPagination } from "../src/index.js";
import { createInfiniteScroll, createSegment, createPagination } from "../src/index.js";

async function fetcher(page: number) {
let elements: string[] = [];
Expand All @@ -14,37 +14,45 @@ async function fetcher(page: number) {
return elements;
}

const PaginationDemo: Component = () => {
const [paginationProps, page, setPage] = createPagination({ pages: 100 });
const PaginationSegmentationDemo: Component = () => {
const items = Array.from({ length: 1000 }, (_, i) => i + 1);
const [limit, setLimit] = createSignal(10);
const [paginationProps, page, setPage] = createPagination(() => ({
pages: Math.ceil(items.length / limit()),
maxPages: 8,
jumpPages: 10,
}));
const segment = createSegment(items, limit, page);

return (
<div class="box-border flex w-1/2 flex-col items-center justify-center space-y-4 bg-gray-800 p-24 text-white">
<div class="wrapper-v">
<h4>Pagination component</h4>
<p>Current page: {page()} / 100</p>
<nav class="flex flex-row">
<For each={paginationProps()}>{props => <button {...props} />}</For>
</nav>
<button onClick={() => setPage(Math.round(Math.random() * 100 + 1))}>
jump to random page
</button>
<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">
<h4 class="text-xl">Pagination & Segmentation:</h4>
<For each={segment()}>{(i) => (<p class="border-1 w-full text-center">{i}</p>)}</For>
<nav class="flex flex-row">
<For each={paginationProps()}>{props => <button class="whitespace-nowrap" {...props} />}</For>
</nav>
<div class="flex flex-row">
<button onClick={() => setLimit(5)} disabled={limit() === 5}>5 items/page</button>
<button onClick={() => setLimit(10)} disabled={limit() === 10}>10 items/page</button>
<button onClick={() => setLimit(20)} disabled={limit() === 20}>20 items/page</button>
<button onClick={() => setPage(Math.round(Math.random() * 1000 / limit() + 1))}>jump to random page</button>
</div>
</div>
);
};
}

const InfiniteScrollDemo = () => {
const [pages, infiniteScrollLoader, { end }] = createInfiniteScroll(fetcher);
infiniteScrollLoader;
return (
<div class="flex max-h-screen w-1/2 flex-col bg-gray-800 text-white">
<div class="flex h-[10%] items-center justify-center overflow-scroll">
<h1>Infinite Scrolling:</h1>
<div class="flex max-h-screen w-1/2 mx-auto mt-10 p-4 flex-col bg-gray-800 text-white">
<div class="flex items-center justify-center">
<h4 class="text-xl">Infinite Scrolling:</h4>
</div>
<div class="h-[90%] overflow-scroll">
<For each={pages()}>{item => <p>{item}</p>}</For>
<Show when={!end()}>
<h1 use:infiniteScrollLoader>Loading...</h1>
<span use:infiniteScrollLoader>Loading...</span>
</Show>
</div>
</div>
Expand All @@ -53,8 +61,8 @@ const InfiniteScrollDemo = () => {

const App: Component = () => {
return (
<div class="flex min-h-screen w-full">
<PaginationDemo />
<div class="flex flex-col min-h-screen w-full">
<PaginationSegmentationDemo />
<InfiniteScrollDemo />
</div>
);
Expand Down
1 change: 1 addition & 0 deletions packages/pagination/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"stage": 0,
"list": [
"createPagination",
"createSegment",
"createInfiniteScroll"
],
"category": "UI Patterns"
Expand Down
103 changes: 96 additions & 7 deletions packages/pagination/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,43 @@ import {
createResource,
createSignal,
onCleanup,
untrack,
} from "solid-js";
import { isServer } from "solid-js/web";

/**
* createSegment - create a reactive segment out of an array of items
* @param {MaybeAccessor<any[]>} items - array of items
* @param {MaybeAccessor<number>} limit - limit of items per segment
* @param {Accessor<number>} page - segment number starting with 1
*
* ```ts
* const [limit, setLimit] = createSignal(10);
* const [paginationProps, page, setPage] = createPagination(() => ({
* pages: Math.ceil(items().length / limit())
* }));
* const segment = createSegment(items, limit, page);
* ```
*/
export const createSegment = <T>(
items: MaybeAccessor<T[]>,
limit: MaybeAccessor<number>,
page: Accessor<number>
): Accessor<T[]> => {
let previousStart = NaN, previousEnd = NaN;
return createMemo((previous) => {
const currentItems = access(items);
const start = (page() - 1) * access(limit);
const end = Math.min(start + access(limit), currentItems.length);
if (previous && (previous.length === 0 && end <= start || start === previousStart && end === previousEnd)) {
return previous;
}
previousStart = start;
previousEnd = end;
return currentItems.slice(start, end);
});
}

export type PaginationOptions = {
/** the overall number of pages */
pages: number;
Expand All @@ -35,6 +69,8 @@ export type PaginationOptions = {
nextContent?: JSX.Element;
/** content for the last page element, e.g. an SVG icon, default is ">|" */
lastContent?: JSX.Element;
/** number of pages a large jump, if it should exist, should skip */
jumpPages?: number;
};

export type PaginationProps = {
Expand Down Expand Up @@ -98,22 +134,43 @@ export const createPagination = (
): [props: Accessor<PaginationProps>, page: Accessor<number>, setPage: Setter<number>] => {
const opts = createMemo(() => Object.assign({}, PAGINATION_DEFAULTS, access(options)));
const [page, _setPage] = createSignal(opts().initialPage || 1);

// do not allow pages beyond the number of pages in case the latter changes
const setPage = (p: number | ((_p: number) => number)) => {
if (typeof p === "function") {
p = p(page());
}
return p >= 1 && p <= opts().pages ? _setPage(p) : page();
if (p < 1) {
return _setPage(1);
}
const pages = opts().pages;
if (p > pages) {
return _setPage(pages);
}
return _setPage(p);
};

// normalize in case the number of pages changes, do not run the first time
createComputed((previous) => {
opts().pages;
return previous ? setPage(untrack(page)) : true;
});

const goPage = (p: number | ((p: number) => number), ev: KeyboardEvent) => {
setPage(p);
if ('currentTarget' in ev)
(ev.currentTarget as HTMLElement).parentNode?.querySelector<HTMLElement>('[aria-current="true"]')?.focus();
}

const onKeyUp = (pageNo: number, ev: KeyboardEvent) =>
(
({
ArrowLeft: () => setPage(p => p - 1),
ArrowRight: () => setPage(p => p + 1),
Home: () => setPage(1),
End: () => setPage(opts().pages),
Space: () => setPage(pageNo),
Return: () => setPage(pageNo),
ArrowLeft: () => goPage(p => p - 1, ev),
ArrowRight: () => goPage(p => p + 1, ev),
Home: () => goPage(1, ev),
End: () => goPage(opts().pages, ev),
Space: () => goPage(pageNo, ev),
Return: () => goPage(pageNo, ev),
})[ev.key] || noop
)();

Expand Down Expand Up @@ -197,6 +254,32 @@ export const createPagination = (
page: { get: () => opts().pages, enumerable: false },
},
);
const jumpBack = Object.defineProperties(
isServer
? ({} as PaginationProps[number])
: ({
onClick: () => setPage(Math.max(1, page() - (opts().jumpPages || Infinity))),
onKeyUp: (ev: KeyboardEvent) => onKeyUp(page() - (opts().jumpPages || Infinity), ev),
} as unknown as PaginationProps[number]),
{
disabled: { get: () => page() - (opts().jumpPages || Infinity) < 0, set: noop, enumerable: true },
children: { get: () => `-${opts().jumpPages}`, set: noop, enumerable: true },
page: { get: () => Math.max(1, page() - (opts().jumpPages || Infinity)), enumerable: false },
}
);
const jumpForth = Object.defineProperties(
isServer
? ({} as PaginationProps[number])
: ({
onClick: () => setPage(Math.min(opts().pages, page() + (opts().jumpPages || Infinity))),
onKeyUp: (ev: KeyboardEvent) => onKeyUp(Math.min(opts().pages, page() + (opts().jumpPages || Infinity)), ev),
} as unknown as PaginationProps[number]),
{
disabled: { get: () => page() + (opts().jumpPages || Infinity) > opts().pages, set: noop, enumerable: true },
children: { get: () => `+${opts().jumpPages}`, set: noop, enumerable: true },
page: { get: () => Math.min(opts().pages, page() + (opts().jumpPages || Infinity)), enumerable: false },
}
)

const start = createMemo(() =>
Math.min(opts().pages - maxPages(), Math.max(1, page() - (maxPages() >> 1)) - 1),
Expand All @@ -219,13 +302,19 @@ export const createPagination = (
if (showFirst()) {
props.push(first);
}
if (opts().jumpPages) {
props.push(jumpBack);
}
if (showPrev()) {
props.push(back);
}
props.push(...pages().slice(start(), start() + maxPages()));
if (showNext()) {
props.push(next);
}
if (opts().jumpPages) {
props.push(jumpForth);
}
if (showLast()) {
props.push(last);
}
Expand Down
Loading
Loading