svelte-infinite 0.1.6 β 0.2.1
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 +25 -14
- package/dist/InfiniteLoader.svelte +39 -20
- package/dist/InfiniteLoader.svelte.d.ts +16 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
π `IntersectionObserver` based
|
|
21
21
|
π§βπ§ **Demo**: [svelte-5-infinite.vercel.app](https://svelte-5-infinite.vercel.app)
|
|
22
22
|
|
|
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 was working on and was pretty happy with it, so I decided to clean it up and share it with the world! 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.
|
|
24
|
+
|
|
23
25
|
## ποΈ Getting Started
|
|
24
26
|
|
|
25
27
|
1. Install `svelte-infinite`
|
|
@@ -132,10 +134,15 @@ See the example below and [in this repository](https://github.com/ndom91/svelte-
|
|
|
132
134
|
<UserCard {user} />
|
|
133
135
|
{/each}
|
|
134
136
|
|
|
135
|
-
<!-- There are a few optional
|
|
136
|
-
of the scroller in various states, see
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
<!-- 3. There are a few optional snippets for customizing what is shown at the bottom
|
|
138
|
+
of the scroller in various states, see the 'Snippets' section for more details -->
|
|
139
|
+
{#snippet loading()}
|
|
140
|
+
Loading...
|
|
141
|
+
{/snippet}
|
|
142
|
+
{#snippet error(load)}
|
|
143
|
+
<div>Error fetching data</div>
|
|
144
|
+
<button onclick={load}>Retry</button>
|
|
145
|
+
{/snippet}
|
|
139
146
|
</InfiniteLoader>
|
|
140
147
|
</main>
|
|
141
148
|
|
|
@@ -144,9 +151,9 @@ See the example below and [in this repository](https://github.com/ndom91/svelte-
|
|
|
144
151
|
|
|
145
152
|
## βΎοΈ Usage
|
|
146
153
|
|
|
147
|
-
|
|
154
|
+
This package consists of two parts, first the `InfiniteLoader` component which is a wrapper around your items. It will trigger whichever async function you've passed to the `triggerLoad` prop when the user scrolls to the bottom of the list.
|
|
148
155
|
|
|
149
|
-
|
|
156
|
+
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.
|
|
150
157
|
|
|
151
158
|
### loaderState
|
|
152
159
|
|
|
@@ -155,9 +162,9 @@ The `loaderState` import is an object with 4 methods on it:
|
|
|
155
162
|
- `loaderState.loaded()`
|
|
156
163
|
- Designed to be called after a successful fetch.
|
|
157
164
|
- `loaderState.error()`
|
|
158
|
-
- 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`
|
|
165
|
+
- 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.
|
|
159
166
|
- `loaderState.complete()`
|
|
160
|
-
- 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 `no-data`
|
|
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 `no-data` snippet.
|
|
161
168
|
- `loaderState.reset()`
|
|
162
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 infinite list and the user enters a new query.
|
|
163
170
|
|
|
@@ -168,21 +175,25 @@ The `loaderState` import is an object with 4 methods on it:
|
|
|
168
175
|
- `intersectionOptions: `[`IntersectionObserverInit`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver#options)` = { rootMargin: "0px 0px 200px 0px" }` - optional
|
|
169
176
|
- 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.
|
|
170
177
|
- It may also be required to pass in a reference to your scroll container as the `root` option, if your scroll container is not the window.
|
|
171
|
-
- `loopTimeout: number =
|
|
172
|
-
- If the `loopMaxCalls` is reached within
|
|
178
|
+
- `loopTimeout: number = 2000` - optional
|
|
179
|
+
- If the `loopMaxCalls` is reached within the detection timeout, a cool down period is triggered of this length (in milliseconds).
|
|
180
|
+
- `loopDetectionTimeout: number = 1000` - optional
|
|
181
|
+
- The time in milliseconds in which the `loopMaxCalls` count must be hit in order to trigger a cool down period of `loopTimeout` length.
|
|
173
182
|
- `loopMaxCalls: number = 5` - optional
|
|
174
183
|
- The number of calls to the `triggerLoad` function within timeout which should trigger cool down period.
|
|
175
184
|
|
|
176
|
-
###
|
|
185
|
+
### Snippets
|
|
186
|
+
|
|
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 4 props for snippets available.
|
|
177
188
|
|
|
178
189
|
- `loading`
|
|
179
190
|
- Shown while calling `triggerLoad` and waiting on a response.
|
|
180
|
-
- `
|
|
191
|
+
- `noResults`
|
|
181
192
|
- Shown when there are no more results to display and we haven't fetched any data yet (i.e. data is less than count of items to be shown on first "page").
|
|
182
|
-
- `
|
|
193
|
+
- `noData`
|
|
183
194
|
- Shown when `loaderState.complete()` is called, indicating we've fetched and displayed all available data.
|
|
184
195
|
- `error`
|
|
185
|
-
- Shown when there is an error or `loaderState.error()` has been called. The
|
|
196
|
+
- Shown when there is an error or `loaderState.error()` has been called. The snippet has an `attemptLoad` parameter passed to it which is just the internal `triggerLoad` function, designed for a "Retry" button or similar.
|
|
186
197
|
|
|
187
198
|
## π¦ Contributing
|
|
188
199
|
|
|
@@ -28,23 +28,34 @@ export const loaderState = {
|
|
|
28
28
|
<script>import { onMount, onDestroy } from "svelte";
|
|
29
29
|
const {
|
|
30
30
|
triggerLoad,
|
|
31
|
-
loopTimeout =
|
|
31
|
+
loopTimeout = 2e3,
|
|
32
|
+
loopDetectionTimeout = 1e3,
|
|
32
33
|
loopMaxCalls = 5,
|
|
33
|
-
intersectionOptions = {}
|
|
34
|
+
intersectionOptions = {},
|
|
35
|
+
children,
|
|
36
|
+
loading,
|
|
37
|
+
noResults,
|
|
38
|
+
noData,
|
|
39
|
+
error
|
|
34
40
|
} = $props();
|
|
35
|
-
const ERROR_INFINITE_LOOP = `
|
|
41
|
+
const ERROR_INFINITE_LOOP = `Attempted to execute load function ${loopMaxCalls} or more times within a short period. Please wait before trying again..`;
|
|
36
42
|
class LoopTracker {
|
|
37
43
|
coolingOff = false;
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
#coolingOffTimer = null;
|
|
45
|
+
#timer = null;
|
|
46
|
+
#count = 0;
|
|
40
47
|
track() {
|
|
41
|
-
this
|
|
42
|
-
|
|
48
|
+
this.#count += 1;
|
|
49
|
+
clearTimeout(this.#timer);
|
|
50
|
+
this.#timer = setTimeout(() => {
|
|
51
|
+
this.#count = 0;
|
|
52
|
+
}, loopDetectionTimeout);
|
|
53
|
+
if (this.#count >= loopMaxCalls) {
|
|
43
54
|
console.error(ERROR_INFINITE_LOOP);
|
|
44
55
|
this.coolingOff = true;
|
|
45
|
-
this
|
|
56
|
+
this.#coolingOffTimer = setTimeout(() => {
|
|
46
57
|
this.coolingOff = false;
|
|
47
|
-
this
|
|
58
|
+
this.#count = 0;
|
|
48
59
|
}, loopTimeout);
|
|
49
60
|
}
|
|
50
61
|
}
|
|
@@ -55,7 +66,7 @@ let observer = $state();
|
|
|
55
66
|
let showLoading = $derived(status === STATUS.LOADING);
|
|
56
67
|
let showError = $derived(status === STATUS.ERROR);
|
|
57
68
|
let showNoResults = $derived(status === STATUS.COMPLETE && isFirstLoad);
|
|
58
|
-
let
|
|
69
|
+
let showNoData = $derived(status === STATUS.COMPLETE && !isFirstLoad);
|
|
59
70
|
async function attemptLoad() {
|
|
60
71
|
if (status === STATUS.COMPLETE || status !== STATUS.READY && status !== STATUS.ERROR) {
|
|
61
72
|
return;
|
|
@@ -94,36 +105,44 @@ onDestroy(() => {
|
|
|
94
105
|
</script>
|
|
95
106
|
|
|
96
107
|
<div class="infinite-loader-wrapper">
|
|
97
|
-
|
|
108
|
+
{@render children()}
|
|
98
109
|
|
|
99
110
|
<div class="infinite-intersection-target" bind:this={intersectionTarget}>
|
|
100
111
|
{#if showLoading}
|
|
101
|
-
|
|
112
|
+
{#if loading}
|
|
113
|
+
{@render loading()}
|
|
114
|
+
{:else}
|
|
102
115
|
<div class="infinite-loading">Loading...</div>
|
|
103
|
-
|
|
116
|
+
{/if}
|
|
104
117
|
{/if}
|
|
105
118
|
|
|
106
119
|
{#if showNoResults}
|
|
107
|
-
|
|
120
|
+
{#if noResults}
|
|
121
|
+
{@render noResults()}
|
|
122
|
+
{:else}
|
|
108
123
|
<div class="infinite-no-results">No results</div>
|
|
109
|
-
|
|
124
|
+
{/if}
|
|
110
125
|
{/if}
|
|
111
126
|
|
|
112
|
-
{#if
|
|
113
|
-
|
|
127
|
+
{#if showNoData}
|
|
128
|
+
{#if noData}
|
|
129
|
+
{@render noData()}
|
|
130
|
+
{:else}
|
|
114
131
|
<div class="infinite-no-data">No more data</div>
|
|
115
|
-
|
|
132
|
+
{/if}
|
|
116
133
|
{/if}
|
|
117
134
|
|
|
118
135
|
{#if showError}
|
|
119
|
-
|
|
136
|
+
{#if error}
|
|
137
|
+
{@render error(attemptLoad)}
|
|
138
|
+
{:else}
|
|
120
139
|
<div class="infinite-error">
|
|
121
140
|
<div class="infinite-label">Oops, something went wrong</div>
|
|
122
141
|
<button class="infinite-btn" disabled={status === STATUS.COMPLETE} onclick={attemptLoad}>
|
|
123
142
|
Retry
|
|
124
143
|
</button>
|
|
125
144
|
</div>
|
|
126
|
-
|
|
145
|
+
{/if}
|
|
127
146
|
{/if}
|
|
128
147
|
</div>
|
|
129
148
|
</div>
|
|
@@ -5,29 +5,34 @@ export declare const loaderState: {
|
|
|
5
5
|
reset: () => void;
|
|
6
6
|
error: () => void;
|
|
7
7
|
};
|
|
8
|
+
import { type Snippet } from "svelte";
|
|
8
9
|
declare const __propDef: {
|
|
9
10
|
props: {
|
|
10
11
|
triggerLoad: () => Promise<void>;
|
|
11
12
|
loopTimeout?: number | undefined;
|
|
13
|
+
loopDetectionTimeout?: number | undefined;
|
|
12
14
|
loopMaxCalls?: number | undefined;
|
|
13
15
|
intersectionOptions?: IntersectionObserverInit | undefined;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
children: Snippet;
|
|
17
|
+
loading?: ((this: void) => typeof import("svelte").SnippetReturn & {
|
|
18
|
+
_: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\"";
|
|
19
|
+
}) | undefined;
|
|
20
|
+
noResults?: ((this: void) => typeof import("svelte").SnippetReturn & {
|
|
21
|
+
_: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\"";
|
|
22
|
+
}) | undefined;
|
|
23
|
+
noData?: ((this: void) => typeof import("svelte").SnippetReturn & {
|
|
24
|
+
_: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\"";
|
|
25
|
+
}) | undefined;
|
|
26
|
+
error?: ((this: void, args_0: {
|
|
27
|
+
attemptLoad: Promise<() => void>;
|
|
28
|
+
}) => typeof import("svelte").SnippetReturn & {
|
|
16
29
|
_: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\"";
|
|
17
30
|
}) | undefined;
|
|
18
31
|
};
|
|
19
32
|
events: {
|
|
20
33
|
[evt: string]: CustomEvent<any>;
|
|
21
34
|
};
|
|
22
|
-
slots: {
|
|
23
|
-
default: {};
|
|
24
|
-
loading: {};
|
|
25
|
-
'no-results': {};
|
|
26
|
-
'no-data': {};
|
|
27
|
-
error: {
|
|
28
|
-
attemptLoad: () => Promise<void>;
|
|
29
|
-
};
|
|
30
|
-
};
|
|
35
|
+
slots: {};
|
|
31
36
|
};
|
|
32
37
|
type InfiniteLoaderProps_ = typeof __propDef.props;
|
|
33
38
|
export { InfiniteLoaderProps_ as InfiniteLoaderProps };
|