svelte-infinite 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -30
- package/dist/InfiniteLoader.svelte +19 -41
- package/dist/InfiniteLoader.svelte.d.ts +0 -6
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/loaderState.svelte.d.ts +16 -0
- package/dist/loaderState.svelte.js +26 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -12,15 +12,18 @@
|
|
|
12
12
|
[](https://npmjs.org/packages/svelte-infinite)
|
|
13
13
|
[](https://svelte-5-infinite.vercel.app)
|
|
14
14
|
|
|
15
|
-
> Svelte Infinite Loader designed and rebuilt specifically for use
|
|
15
|
+
> Svelte Infinite Loader designed and rebuilt specifically for use with **Svelte 5**
|
|
16
16
|
|
|
17
17
|
✨ Flexible
|
|
18
18
|
⏰ Infinite Loop Detection
|
|
19
19
|
📣 Control Loader State
|
|
20
20
|
🔎 `IntersectionObserver` based
|
|
21
|
+
🔥 Using Runes and Snippets
|
|
21
22
|
🧑🔧 **Demo**: [svelte-5-infinite.vercel.app](https://svelte-5-infinite.vercel.app)
|
|
22
23
|
|
|
23
|
-
Svelte 5 is still early days, but I couldn't find an infinite loader-type component that was maintained for the last few years of Svelte 4. So I had recently built this for a Svelte 5-based application I
|
|
24
|
+
Svelte 5 is still early days, but I couldn't find an infinite loader-type component that was maintained for the last few years of Svelte 4 even. So I had recently built this for a Svelte 5-based application I'm working on and was pretty happy with it, so I decided to share it with the world!
|
|
25
|
+
|
|
26
|
+
As Svelte 5 inevitably changes over the next weeks and months, I plan to keep this package updated and working with the latest available version of Svelte 5. Don't hesitate to open an issue if something has changed in the latest Svelte releases or you come across a bug!
|
|
24
27
|
|
|
25
28
|
## 🏗️ Getting Started
|
|
26
29
|
|
|
@@ -55,25 +58,26 @@ yarn add svelte-infinite
|
|
|
55
58
|
</InfiniteLoader>
|
|
56
59
|
```
|
|
57
60
|
|
|
58
|
-
The component should wrap your list of items, and `loaderState` should be used in your `triggerLoad` function (and/or elsewhere) to interact with the internal state of the Loader component. You tell it whether you're out of data, ran into an error, etc.
|
|
59
|
-
|
|
60
|
-
See the example below and [in this repository](https://github.com/ndom91/svelte-infinite/blob/main/src/routes/%2Bpage.svelte#L12-L50) for more details.
|
|
61
|
-
|
|
62
61
|
## 🍍 Example
|
|
63
62
|
|
|
63
|
+
This is a more realistic example use-case which includes a paginated data endpoint that your `triggerLoad` function should hit every time it's called to load more data. It also includes the use of some of the optional snippets to render custom markup inside the loader component.
|
|
64
|
+
|
|
64
65
|
```svelte
|
|
65
66
|
<script lang="ts">
|
|
67
|
+
// +page.svelte
|
|
68
|
+
|
|
66
69
|
import { InfiniteLoader, loaderState } from "svelte-infinite"
|
|
67
70
|
import UserCard from "$components/UserCard.svelte"
|
|
68
71
|
|
|
69
72
|
const LOAD_LIMIT = 20
|
|
73
|
+
// Assume `$page.data.items` is the `+page.server.ts` server-side loaded
|
|
74
|
+
// and rendered initial 20 items of the list
|
|
70
75
|
const allItems = $state<{ id: number, body: string }[]>($page.data.items)
|
|
71
76
|
let pageNumber = $state(1)
|
|
72
77
|
|
|
73
|
-
// 1.
|
|
78
|
+
// 1. This `loadMore` function is what we'll pass the InfiniteLoader component
|
|
74
79
|
// to its `triggerLoad` prop.
|
|
75
80
|
const loadMore = async () => {
|
|
76
|
-
// This is a relatively straight-forward load function with support for pagination
|
|
77
81
|
try {
|
|
78
82
|
pageNumber += 1
|
|
79
83
|
const limit = String(LOAD_LIMIT)
|
|
@@ -82,20 +86,21 @@ See the example below and [in this repository](https://github.com/ndom91/svelte-
|
|
|
82
86
|
// If there are less results on the first page (page.server loaded data)
|
|
83
87
|
// than the limit, don't keep trying to fetch more. We're done.
|
|
84
88
|
if (allItems.length < LOAD_LIMIT) {
|
|
85
|
-
loaderState.complete()
|
|
89
|
+
loaderState.complete() // <--- using loaderState
|
|
86
90
|
return
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
const searchParams = new URLSearchParams({ limit, skip })
|
|
90
94
|
|
|
91
|
-
//
|
|
95
|
+
// Fetch an endpoint that supports server-side pagination
|
|
92
96
|
const dataResponse = await fetch(`/api/data?${searchParams}`)
|
|
93
97
|
|
|
94
98
|
// Ideally, like most paginated endpoints, this should return the data
|
|
95
99
|
// you've requested for your page, as well as the total amount of data
|
|
96
100
|
// available to page through
|
|
101
|
+
|
|
97
102
|
if (!dataResponse.ok) {
|
|
98
|
-
loaderState.error()
|
|
103
|
+
loaderState.error() // <--- using loaderState
|
|
99
104
|
|
|
100
105
|
// On errors, set the pageNumber back so we can retry
|
|
101
106
|
// that page's data on the next 'loadMore' attempt
|
|
@@ -104,8 +109,7 @@ See the example below and [in this repository](https://github.com/ndom91/svelte-
|
|
|
104
109
|
}
|
|
105
110
|
const data = await dataResponse.json()
|
|
106
111
|
|
|
107
|
-
// If we've received data, push it to the reactive state variable
|
|
108
|
-
// rendering our items inside the `<InfiniteLoader />` below.
|
|
112
|
+
// If we've successfully received data, push it to the reactive state variable
|
|
109
113
|
if (data.items.length) {
|
|
110
114
|
allItems.push(...data.items)
|
|
111
115
|
}
|
|
@@ -113,13 +117,13 @@ See the example below and [in this repository](https://github.com/ndom91/svelte-
|
|
|
113
117
|
// If there are more (or equal) number of items loaded as are totally available
|
|
114
118
|
// from the API, don't keep trying to fetch more. We're done.
|
|
115
119
|
if (allItems.length >= data.totalCount) {
|
|
116
|
-
loaderState.complete()
|
|
120
|
+
loaderState.complete() // <--- using loaderState
|
|
117
121
|
} else {
|
|
118
|
-
loaderState.loaded()
|
|
122
|
+
loaderState.loaded() // <--- using loaderState
|
|
119
123
|
}
|
|
120
124
|
} catch (error) {
|
|
121
125
|
console.error(error)
|
|
122
|
-
loaderState.error()
|
|
126
|
+
loaderState.error() // <--- using loaderState
|
|
123
127
|
pageNumber -= 1
|
|
124
128
|
}
|
|
125
129
|
}
|
|
@@ -155,36 +159,36 @@ This package consists of two parts, first the `InfiniteLoader` component which i
|
|
|
155
159
|
|
|
156
160
|
Second, there is also a `loaderState` import which you should use to interact with the internal state of the loader. For example, if your `fetch` call errored, or you've reached the maximum number of items, etc. you can communicate that to the loader. The most basic usage example can be seen in the 'Getting Started' section above. A more complex example can be seen in the 'Example' section, and of course the application in `/src/routes/+page.svelte` in this repository also has a "real-world" usage example.
|
|
157
161
|
|
|
158
|
-
### loaderState
|
|
162
|
+
### `loaderState` Controller
|
|
159
163
|
|
|
160
|
-
The `loaderState`
|
|
164
|
+
The `loaderState` controller has 4 methods on it. You should call these at the appropriate times to control the internal state of the `InfiniteLoader`.
|
|
161
165
|
|
|
162
166
|
- `loaderState.loaded()`
|
|
163
|
-
- Designed to be called after a successful fetch.
|
|
167
|
+
- Designed to be called after a successful fetch. Will set the internal state back to `READY` so another fetch can be attempted.
|
|
164
168
|
- `loaderState.error()`
|
|
165
169
|
- Designed to be called after a failed fetch or any other error. This will cause the `InfiniteLoader` to render a "Retry" button by default, or the `error` snippet.
|
|
166
170
|
- `loaderState.complete()`
|
|
167
|
-
- Designed to be called when you've reached the end of your list and there are no more items to fetch. This will render a "No more data" string, or the `
|
|
171
|
+
- Designed to be called when you've reached the end of your list and there are no more items to fetch. This will render a "No more data" string, or the `noData` snippet.
|
|
168
172
|
- `loaderState.reset()`
|
|
169
|
-
- Designed to be called when you want to reset the state of the `InfiniteLoader` to its initial state, for example if there is a search input tied to your
|
|
173
|
+
- Designed to be called when you want to reset the state of the `InfiniteLoader` to its initial state, for example if there is a search input tied to your data and the user enters a new query.
|
|
170
174
|
|
|
171
|
-
### Props
|
|
175
|
+
### `InfiniteLoader` Props
|
|
172
176
|
|
|
173
177
|
- `triggerLoad: () => Promise<void>` - **required**
|
|
174
178
|
- The async function to call when we should attempt to load more data to show.
|
|
175
179
|
- `intersectionOptions: `[`IntersectionObserverInit`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver#options)` = { rootMargin: "0px 0px 200px 0px" }` - optional
|
|
176
180
|
- The options to pass to the `IntersectionObserver` instance. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver#options) for more details. The default `rootMargin` value will cause the target to intersect 200px earlier and trigger the `loadMore` function before it actually intersects with the root element (window by default). This has the effect of beginning to load the next page of data before the user has actually reached the current bottom of the list, making the experience feel more smooth.
|
|
177
|
-
-
|
|
178
|
-
- `loopTimeout: number =
|
|
179
|
-
-
|
|
180
|
-
- `loopDetectionTimeout: number =
|
|
181
|
-
- The time in milliseconds in which the `loopMaxCalls` count must be hit in order to trigger a cool down period
|
|
181
|
+
- If you are using a separate scroll container (element with `overflow-y: scroll`) other than the window / viewport, then it might be necessary for you to also pass a [custom `root` element](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/root) here.
|
|
182
|
+
- `loopTimeout: number = 3000` - optional
|
|
183
|
+
- Length of the cool down period (in milliseconds).
|
|
184
|
+
- `loopDetectionTimeout: number = 2000` - optional
|
|
185
|
+
- The time in milliseconds in which the `loopMaxCalls` count must be hit in order to trigger a cool down period.
|
|
182
186
|
- `loopMaxCalls: number = 5` - optional
|
|
183
|
-
- The
|
|
187
|
+
- The limit of `triggerLoad` executions which will trigger a cool down period, if reached within the `loopDetectionTimeout`.
|
|
184
188
|
|
|
185
|
-
### Snippets
|
|
189
|
+
### `InfiniteLoader` Snippets
|
|
186
190
|
|
|
187
|
-
Snippets [replace slots](https://svelte-5-preview.vercel.app/docs/snippets#snippets-and-slots) in Svelte 5, and as such are used here to customize the content shown at the bottom of the scroller in various states. The `InfiniteLoader` component has
|
|
191
|
+
Snippets [replace slots](https://svelte-5-preview.vercel.app/docs/snippets#snippets-and-slots) in Svelte 5, and as such are used here to customize the content shown at the bottom of the scroller in various states. The `InfiniteLoader` component has 5 snippet "slots" available.
|
|
188
192
|
|
|
189
193
|
- `loading`
|
|
190
194
|
- Shown while calling `triggerLoad` and waiting on a response.
|
|
@@ -1,35 +1,9 @@
|
|
|
1
|
-
<script context="module">const STATUS = {
|
|
2
|
-
READY: "READY",
|
|
3
|
-
LOADING: "LOADING",
|
|
4
|
-
COMPLETE: "COMPLETE",
|
|
5
|
-
ERROR: "ERROR"
|
|
6
|
-
};
|
|
7
|
-
let isFirstLoad = $state(true);
|
|
8
|
-
let status = $state(STATUS.READY);
|
|
9
|
-
export const loaderState = {
|
|
10
|
-
loaded: () => {
|
|
11
|
-
isFirstLoad = false;
|
|
12
|
-
status = STATUS.READY;
|
|
13
|
-
},
|
|
14
|
-
complete: () => {
|
|
15
|
-
isFirstLoad = false;
|
|
16
|
-
status = STATUS.COMPLETE;
|
|
17
|
-
},
|
|
18
|
-
reset: () => {
|
|
19
|
-
status = STATUS.READY;
|
|
20
|
-
isFirstLoad = true;
|
|
21
|
-
},
|
|
22
|
-
error: () => {
|
|
23
|
-
status = STATUS.ERROR;
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
</script>
|
|
27
|
-
|
|
28
1
|
<script>import { onMount, onDestroy } from "svelte";
|
|
2
|
+
import { STATUS, loaderState } from "./loaderState.svelte";
|
|
29
3
|
const {
|
|
30
4
|
triggerLoad,
|
|
31
|
-
loopTimeout =
|
|
32
|
-
loopDetectionTimeout =
|
|
5
|
+
loopTimeout = 3e3,
|
|
6
|
+
loopDetectionTimeout = 2e3,
|
|
33
7
|
loopMaxCalls = 5,
|
|
34
8
|
intersectionOptions = {},
|
|
35
9
|
children,
|
|
@@ -64,24 +38,24 @@ class LoopTracker {
|
|
|
64
38
|
const loopTracker = new LoopTracker();
|
|
65
39
|
let intersectionTarget = $state();
|
|
66
40
|
let observer = $state();
|
|
67
|
-
let showLoading = $derived(status === STATUS.LOADING);
|
|
68
|
-
let showError = $derived(status === STATUS.ERROR);
|
|
69
|
-
let showNoResults = $derived(status === STATUS.COMPLETE && isFirstLoad);
|
|
70
|
-
let showNoData = $derived(status === STATUS.COMPLETE && !isFirstLoad);
|
|
71
|
-
let showCoolingOff = $derived(status !== STATUS.COMPLETE && loopTracker.coolingOff);
|
|
41
|
+
let showLoading = $derived(loaderState.status === STATUS.LOADING);
|
|
42
|
+
let showError = $derived(loaderState.status === STATUS.ERROR);
|
|
43
|
+
let showNoResults = $derived(loaderState.status === STATUS.COMPLETE && loaderState.isFirstLoad);
|
|
44
|
+
let showNoData = $derived(loaderState.status === STATUS.COMPLETE && !loaderState.isFirstLoad);
|
|
45
|
+
let showCoolingOff = $derived(loaderState.status !== STATUS.COMPLETE && loopTracker.coolingOff);
|
|
72
46
|
async function attemptLoad() {
|
|
73
|
-
if (status === STATUS.COMPLETE || status !== STATUS.READY && status !== STATUS.ERROR) {
|
|
47
|
+
if (loaderState.status === STATUS.COMPLETE || loaderState.status !== STATUS.READY && loaderState.status !== STATUS.ERROR) {
|
|
74
48
|
return;
|
|
75
49
|
}
|
|
76
|
-
status = STATUS.LOADING;
|
|
50
|
+
loaderState.status = STATUS.LOADING;
|
|
77
51
|
if (!loopTracker.coolingOff) {
|
|
78
52
|
await triggerLoad();
|
|
79
53
|
loopTracker.track();
|
|
80
54
|
}
|
|
81
|
-
if (status !== STATUS.ERROR && status !== STATUS.COMPLETE) {
|
|
82
|
-
if (status === STATUS.LOADING) {
|
|
83
|
-
status = STATUS.READY;
|
|
84
|
-
isFirstLoad = false;
|
|
55
|
+
if (loaderState.status !== STATUS.ERROR && loaderState.status !== STATUS.COMPLETE) {
|
|
56
|
+
if (loaderState.status === STATUS.LOADING) {
|
|
57
|
+
loaderState.status = STATUS.READY;
|
|
58
|
+
loaderState.isFirstLoad = false;
|
|
85
59
|
}
|
|
86
60
|
}
|
|
87
61
|
}
|
|
@@ -148,7 +122,11 @@ onDestroy(() => {
|
|
|
148
122
|
{:else}
|
|
149
123
|
<div class="infinite-error">
|
|
150
124
|
<div class="infinite-label">Oops, something went wrong</div>
|
|
151
|
-
<button
|
|
125
|
+
<button
|
|
126
|
+
class="infinite-btn"
|
|
127
|
+
disabled={loaderState.status === STATUS.COMPLETE}
|
|
128
|
+
onclick={attemptLoad}
|
|
129
|
+
>
|
|
152
130
|
Retry
|
|
153
131
|
</button>
|
|
154
132
|
</div>
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const STATUS: {
|
|
2
|
+
readonly READY: "READY";
|
|
3
|
+
readonly LOADING: "LOADING";
|
|
4
|
+
readonly COMPLETE: "COMPLETE";
|
|
5
|
+
readonly ERROR: "ERROR";
|
|
6
|
+
};
|
|
7
|
+
declare class LoaderState {
|
|
8
|
+
isFirstLoad: boolean;
|
|
9
|
+
status: "READY" | "LOADING" | "COMPLETE" | "ERROR";
|
|
10
|
+
loaded: () => void;
|
|
11
|
+
complete: () => void;
|
|
12
|
+
reset: () => void;
|
|
13
|
+
error: () => void;
|
|
14
|
+
}
|
|
15
|
+
export declare const loaderState: LoaderState;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const STATUS = {
|
|
2
|
+
READY: "READY",
|
|
3
|
+
LOADING: "LOADING",
|
|
4
|
+
COMPLETE: "COMPLETE",
|
|
5
|
+
ERROR: "ERROR"
|
|
6
|
+
};
|
|
7
|
+
class LoaderState {
|
|
8
|
+
isFirstLoad = $state(true);
|
|
9
|
+
status = $state(STATUS.READY);
|
|
10
|
+
loaded = () => {
|
|
11
|
+
this.isFirstLoad = false;
|
|
12
|
+
this.status = STATUS.READY;
|
|
13
|
+
};
|
|
14
|
+
complete = () => {
|
|
15
|
+
this.isFirstLoad = false;
|
|
16
|
+
this.status = STATUS.COMPLETE;
|
|
17
|
+
};
|
|
18
|
+
reset = () => {
|
|
19
|
+
this.isFirstLoad = true;
|
|
20
|
+
this.status = STATUS.READY;
|
|
21
|
+
};
|
|
22
|
+
error = () => {
|
|
23
|
+
this.status = STATUS.ERROR;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export const loaderState = new LoaderState();
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"email": "yo@ndo.dev",
|
|
7
7
|
"url": "https://ndo.dev"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.3.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"homepage": "https://svelte-5-infinite.vercel.app",
|
|
12
12
|
"keywords": [
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"svelte-check": "^3.6.6",
|
|
58
58
|
"tslib": "^2.6.2",
|
|
59
59
|
"typescript": "^5.3.3",
|
|
60
|
+
"typescript-svelte-plugin": "^0.3.37",
|
|
60
61
|
"vite": "^5.1.4",
|
|
61
62
|
"vitest": "^1.3.1"
|
|
62
63
|
},
|