-
Notifications
You must be signed in to change notification settings - Fork 144
fix: issue #795 #797
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
007qr
wants to merge
6
commits into
solidjs-community:main
Choose a base branch
from
007qr:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
fix: issue #795 #797
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
9c7bf25
fix: issue #795
007qr 393d965
fix: issues with reactivity
007qr 6d648e5
fix: removed duplicate imports
007qr 2b33fed
fix: reactivity issue with error and loding
007qr 6bd4189
Merge branch 'main' into main
007qr 8a7d125
Merge branch 'solidjs-community:main' into main
007qr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -246,71 +246,166 @@ declare module "solid-js" { | |
export type _E = JSX.Element; | ||
|
||
/** | ||
* Provides an easy way to implement infinite scrolling. | ||
* A SolidJS utility to create an infinite scrolling experience with IntersectionObserver and reactivity. | ||
* | ||
* This function handles page fetching, auto-incrementing on scroll, error tracking, and supports | ||
* dynamic parameters for API requests. It returns an accessor for the loaded pages, a directive | ||
* to attach to a loader DOM element, and control state for pagination. | ||
* | ||
* ### Usage Example: | ||
* ```ts | ||
* const [pages, loader, { page, setPage, setPages, end, setEnd }] = createInfiniteScroll(fetcher); | ||
* const [pages, loader, { page, setPage, setPages, end, setEnd }] = createInfiniteScroll(fetcher, { | ||
* params: () => ({ query: 'search-term' }), | ||
* resetOnParamsChange: true | ||
* }); | ||
* ``` | ||
* @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 | ||
* @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 | ||
* | ||
* @template T - The type of individual page items (e.g., object, string). | ||
* @template E - (Optional) The error type. Defaults to `unknown`. | ||
* @template P - (Optional) The type of dynamic fetcher parameters. | ||
* | ||
* @param {function(page: number, params: P): Promise<T[]>} fetcher | ||
* A function that takes a page number and optional parameters and returns a Promise resolving to an array of items. | ||
* | ||
* @param {Object} [options] - Optional configuration object. | ||
* @param {() => P} [options.params] - A reactive accessor for dynamic fetcher parameters. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We usually use the pattern that we take a params object or a params object signal. If it is a signal, we expect that we should reset state on params change; otherwise, we don't. |
||
* @param {boolean} [options.resetOnParamsChange=true] - Whether to reset state (pages, page, end) when parameters change. | ||
* | ||
* @returns {[Accessor<T[]>, (el: Element) => void, { | ||
* page: Accessor<number>, | ||
* setPage: Setter<number>, | ||
* setPages: Setter<T[]>, | ||
* end: Accessor<boolean>, | ||
* setEnd: Setter<boolean>, | ||
* error: Accessor<Error | null>, | ||
* loading: Accessor<boolean> | ||
* }]} A tuple: | ||
* - `pages`: an accessor for the full list of loaded items. | ||
* - `loader`: a directive function that should be attached to the DOM element used as the intersection trigger (e.g., a loading spinner). | ||
* - `state`: an object with methods and accessors for managing pagination state. | ||
* | ||
* @property {Accessor<number>} page - Current page number accessor. | ||
* @property {Setter<number>} setPage - Function to manually set the current page number. | ||
* @property {Setter<T[]>} setPages - Function to manually set the entire list of loaded items. | ||
* @property {Accessor<boolean>} end - Accessor that indicates if the end of available content has been reached. | ||
* @property {Setter<boolean>} setEnd - Function to manually set the end state. | ||
* @property {Accessor<Error | null>} error - Accessor for any error encountered during fetching. | ||
* @property {Accessor<boolean>} loading - Accessor for the current loading state. | ||
*/ | ||
export function createInfiniteScroll<T>(fetcher: (page: number) => Promise<T[]>): [ | ||
pages: Accessor<T[]>, | ||
loader: (el: Element) => void, | ||
options: { | ||
page: Accessor<number>; | ||
setPage: Setter<number>; | ||
setPages: Setter<T[]>; | ||
end: Accessor<boolean>; | ||
setEnd: Setter<boolean>; | ||
}, | ||
export function createInfiniteScroll<T, E = unknown, P = unknown>( | ||
fetcher: (page: number, params: P) => Promise<T[]>, | ||
options?: InfiniteScrollOptions<P> | ||
): [ | ||
pages: Accessor<T[]>, | ||
loader: (el: Element) => void, | ||
state: { | ||
page: Accessor<number> | ||
setPage: Setter<number> | ||
setPages: Setter<T[]> | ||
end: Accessor<boolean> | ||
setEnd: Setter<boolean> | ||
error: Accessor<Error | null> | ||
loading: Accessor<boolean> | ||
} | ||
] { | ||
const [pages, setPages] = createSignal<T[]>([]); | ||
const [page, setPage] = createSignal(0); | ||
const [end, setEnd] = createSignal(false); | ||
const [pages, setPages] = createSignal<T[]>([]) | ||
const [page, setPage] = createSignal(0) | ||
const [end, setEnd] = createSignal(false) | ||
const [error, setError] = createSignal<Error | null>(null) | ||
|
||
let add: (el: Element) => void = noop; | ||
if (!isServer) { | ||
const io = new IntersectionObserver(e => { | ||
if (e.length > 0 && e[0]!.isIntersecting && !end() && !contents.loading) { | ||
setPage(p => p + 1); | ||
} | ||
}); | ||
onCleanup(() => io.disconnect()); | ||
add = (el: Element) => { | ||
io.observe(el); | ||
tryOnCleanup(() => io.unobserve(el)); | ||
}; | ||
} | ||
// Fix: Handle undefined options by providing defaults | ||
const { params, resetOnParamsChange = true } = options || {} | ||
|
||
const [contents] = createResource(page, fetcher); | ||
const [contents] = createResource( | ||
() => [page(), params?.()] as const, | ||
async ([p, param]) => { | ||
setError(null) | ||
try { | ||
const result = await fetcher(p, param) | ||
return result | ||
} catch (e) { | ||
setError(e as Error) | ||
console.error(e) | ||
return [] | ||
} | ||
} | ||
) | ||
|
||
createComputed(() => { | ||
const content = contents.latest; | ||
if (!content) return; | ||
batch(() => { | ||
if (content.length === 0) setEnd(true); | ||
setPages(p => [...p, ...content]); | ||
}); | ||
}); | ||
createComputed(() => { | ||
const content = contents() | ||
007qr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (!content) return | ||
batch(() => { | ||
if (content.length === 0) setEnd(true) | ||
setPages((prev) => [...prev, ...content]) | ||
}) | ||
}) | ||
|
||
return [ | ||
pages, | ||
add, | ||
{ | ||
page: page, | ||
setPage: setPage, | ||
setPages: setPages, | ||
end: end, | ||
setEnd: setEnd, | ||
}, | ||
]; | ||
// Reset state if params change | ||
createComputed(() => { | ||
if (params) { | ||
params() | ||
if (resetOnParamsChange) { | ||
batch(() => { | ||
setPages([]) | ||
setPage(0) | ||
setEnd(false) | ||
}) | ||
} | ||
} | ||
}) | ||
|
||
let add: (el: Element) => void = () => {} | ||
let currentObservedElement: Element | null = null | ||
|
||
if (!isServer) { | ||
const io = new IntersectionObserver( | ||
(entries) => { | ||
const entry = entries[0] | ||
if ( | ||
entry?.isIntersecting && | ||
!end() && | ||
!contents.loading | ||
) { | ||
setPage((p) => p + 1) | ||
} | ||
}, | ||
{ | ||
rootMargin: '50px' | ||
} | ||
) | ||
|
||
onCleanup(() => io.disconnect()) | ||
|
||
add = (el: Element) => { | ||
// Unobserve the previous element if it exists | ||
if (currentObservedElement) { | ||
io.unobserve(currentObservedElement) | ||
} | ||
|
||
// Observe the new last element | ||
io.observe(el) | ||
currentObservedElement = el | ||
|
||
tryOnCleanup(() => { | ||
io.unobserve(el) | ||
if (currentObservedElement === el) { | ||
currentObservedElement = null | ||
} | ||
}) | ||
} | ||
} | ||
|
||
return [ | ||
pages, | ||
add, | ||
{ | ||
page, | ||
setPage, | ||
setPages, | ||
end, | ||
setEnd, | ||
error, | ||
loading: () => contents.loading | ||
}, | ||
] | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no need to explicitly mention SolidJS here when the whole package is part of solid-primitives.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure will improve this