Skip to content
Draft
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
17 changes: 17 additions & 0 deletions .changeset/pagination-solid2-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@solid-primitives/pagination": major
---

Migrate to Solid.js v2.0 (beta.10)

## Breaking Changes

**Peer dependencies**: `solid-js@^2.0.0-beta.10` and `@solidjs/web@^2.0.0-beta.10` are now required.

### `@solid-primitives/pagination`

- `isServer` now imported from `@solidjs/web` (not `solid-js/web`)
- `createPagination`: page clamping when pages count decreases is now implemented via a derived memo instead of `createComputed` (which was removed in Solid 2.0). The clamping is reactive and automatic.
- `createInfiniteScroll`: removed `createResource` dependency (removed in Solid 2.0). Fetching is now implemented with `createEffect` and a cancellation pattern. The `pages.loading` and `pages.error` resource properties are no longer available; use `end()` or wrap the fetcher to handle errors externally.
- `batch()` calls removed — Solid 2.0 batches updates automatically via microtasks. Tests require `flush()` after signal writes to observe committed values.
- All internal signals use `{ ownedWrite: true }` to allow setters to be called from reactive scopes and event handlers without triggering ownership warnings.
6 changes: 4 additions & 2 deletions packages/pagination/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ yarn add @solid-primitives/pagination
pnpm add @solid-primitives/pagination
```

> **Peer dependencies:** `solid-js@^2.0.0-beta.10` and `@solidjs/web@^2.0.0-beta.10`

## `createPagination`

Provides an array with the properties to fill your pagination with and a page setter/getter.
Expand Down Expand Up @@ -133,7 +135,7 @@ 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.
Combines [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) with a page-based fetcher to provide an easy way to implement infinite scrolling. Browser-only: the loader and fetching are skipped on the server.

### How to use it

Expand Down Expand Up @@ -171,7 +173,7 @@ return (
```ts
function createInfiniteScroll<T>(fetcher: (page: number) => Promise<T[]>): [
pages: Accessor<T[]>,
loader: Directive<true>,
loader: (el: Element) => void,
options: {
page: Accessor<number>;
setPage: Setter<number>;
Expand Down
6 changes: 4 additions & 2 deletions packages/pagination/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,11 @@
"@solid-primitives/utils": "workspace:^"
},
"peerDependencies": {
"solid-js": "^1.6.12"
"@solidjs/web": "^2.0.0-beta.10",
"solid-js": "^2.0.0-beta.10"
},
"devDependencies": {
"solid-js": "^1.9.7"
"@solidjs/web": "2.0.0-beta.10",
"solid-js": "2.0.0-beta.10"
}
}
137 changes: 68 additions & 69 deletions packages/pagination/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { access, tryOnCleanup, noop, type MaybeAccessor } from "@solid-primitives/utils";
import {
type Accessor,
batch,
type JSX,
type Setter,
createComputed,
createEffect,
createMemo,
createResource,
createSignal,
onCleanup,
untrack,
} from "solid-js";
import { isServer } from "solid-js/web";
import { isServer } from "@solidjs/web";

/**
* createSegment - create a reactive segment out of an array of items
Expand All @@ -34,15 +32,15 @@ export const createSegment = <T>(
): Accessor<T[]> => {
let previousStart = NaN,
previousEnd = NaN;
return createMemo(previous => {
return createMemo(prev => {
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))
prev &&
((prev.length === 0 && end <= start) || (start === previousStart && end === previousEnd))
) {
return previous;
return prev;
}
previousStart = start;
previousEnd = end;
Expand Down Expand Up @@ -137,28 +135,25 @@ export const createPagination = (
options?: MaybeAccessor<PaginationOptions>,
): [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);
// ownedWrite allows setPage to be called from event handlers and reactive scopes
const [rawPage, setRawPage] = createSignal(opts().initialPage || 1, { ownedWrite: true });

// 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());
}
if (p < 1) {
return _setPage(1);
return setRawPage(1);
}
const pages = opts().pages;
if (p > pages) {
return _setPage(pages);
return setRawPage(pages);
}
return _setPage(p);
return setRawPage(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;
});
// Clamp page to valid range reactively — handles page count decreasing below current page
const page = createMemo(() => Math.max(1, Math.min(rawPage(), opts().pages)));

const goPage = (p: number | ((p: number) => number), ev: KeyboardEvent) => {
setPage(p);
Expand All @@ -182,31 +177,29 @@ export const createPagination = (

const maxPages = createMemo(() => Math.min(opts().maxPages, opts().pages));

const pages = createMemo<PaginationProps>(
previous =>
[...Array(opts().pages)].map(
(_, i) =>
previous[i] ||
((pageNo: number) =>
Object.defineProperties(
isServer
? { children: pageNo.toString() }
: {
children: pageNo.toString(),
onClick: [setPage, pageNo] as const,
onKeyUp: [onKeyUp, pageNo] as const,
},
{
"aria-current": {
get: () => (page() === pageNo ? "true" : undefined),
set: noop,
enumerable: true,
const pages = createMemo<PaginationProps>(prev =>
[...Array(opts().pages)].map(
(_, i) =>
(prev ?? [])[i] ||
((pageNo: number) =>
Object.defineProperties(
isServer
? { children: pageNo.toString() }
: {
children: pageNo.toString(),
onClick: [setPage, pageNo] as const,
onKeyUp: [onKeyUp, pageNo] as const,
},
page: { value: pageNo, enumerable: false },
{
"aria-current": {
get: () => (page() === pageNo ? "true" : undefined),
set: noop,
enumerable: true,
},
))(i + 1),
),
[],
page: { value: pageNo, enumerable: false },
},
))(i + 1),
),
);
const first = Object.defineProperties(
isServer
Expand Down Expand Up @@ -359,15 +352,13 @@ export type _E = JSX.Element;
* const [pages, loader, { page, setPage, setPages, end, setEnd }] = createInfiniteScroll(fetcher);
* ```
* @param fetcher `(page: number) => Promise<T[]>`
* @return `pages()` is an accessor contains array of contents
* @property `pages.loading` is a boolean indicator for the loading state
* @property `pages.error` contains any error encountered
* @return `infiniteScrollLoader` is a directive used to set the loader element
* @method `page` is an accessor that contains page number
* @return `pages()` is an accessor that contains the accumulated array of all fetched items
* @return `loader` is a ref function to attach to the sentinel element that triggers loading
* @method `page` is an accessor that contains the current page number
* @method `setPage` allows to manually change the page number
* @method `setPages` allows to manually change the contents of the page
* @method `end` is a boolean indicator for end of the page
* @method `setEnd` allows to manually change the end
* @method `setPages` allows to manually replace the accumulated items
* @method `end` is a boolean indicator for end of the content
* @method `setEnd` allows to manually change the end state
*/
export function createInfiniteScroll<T>(fetcher: (page: number) => Promise<T[]>): [
pages: Accessor<T[]>,
Expand All @@ -380,14 +371,16 @@ export function createInfiniteScroll<T>(fetcher: (page: number) => Promise<T[]>)
setEnd: Setter<boolean>;
},
] {
const [pages, setPages] = createSignal<T[]>([]);
const [page, setPage] = createSignal(0);
const [end, setEnd] = createSignal(false);
// ownedWrite allows setters to be called from reactive scopes and event handlers
const [pages, setPages] = createSignal<T[]>([], { ownedWrite: true });
const [page, setPage] = createSignal(0, { ownedWrite: true });
const [end, setEnd] = createSignal(false, { ownedWrite: true });
const [fetching, setFetching] = createSignal(false, { ownedWrite: true });

let add: (el: Element) => void = noop;
if (!isServer) {
const io = new IntersectionObserver(e => {
if (e.length > 0 && e[0]!.isIntersecting && !end() && !contents.loading) {
if (e.length > 0 && e[0]!.isIntersecting && !end() && !fetching()) {
setPage(p => p + 1);
}
});
Expand All @@ -396,28 +389,34 @@ export function createInfiniteScroll<T>(fetcher: (page: number) => Promise<T[]>)
io.observe(el);
tryOnCleanup(() => io.unobserve(el));
};
}

const [contents] = createResource(page, fetcher);

createComputed(() => {
const content = contents.latest;
if (!content) return;
batch(() => {
if (content.length === 0) setEnd(true);
setPages(p => [...p, ...content]);
});
});
createEffect(
() => page(),
currentPage => {
let cancelled = false;
setFetching(true);
fetcher(currentPage).then(content => {
if (cancelled) return;
if (content.length === 0) setEnd(true);
setPages(p => [...p, ...content]);
setFetching(false);
});
return () => {
cancelled = true;
};
},
);
}

return [
pages,
add,
{
page: page,
setPage: setPage,
setPages: setPages,
end: end,
setEnd: setEnd,
page,
setPage,
setPages,
end,
setEnd,
},
];
}
Loading