svelte-infinite 0.1.1 → 0.1.2
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/LICENSE +0 -0
- package/README.md +96 -32
- package/dist/InfiniteLoader.svelte +62 -46
- package/dist/InfiniteLoader.svelte.d.ts +1 -0
- package/package.json +27 -24
package/LICENSE
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
@@ -1,48 +1,84 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img align="center" src="https://raw.githubusercontent.com/ndom91/svelte-infinite/51683d459ae954a99e7c5c25817ed667678a0840/src/assets/SvelteLogo.svg" height="96" />
|
|
3
|
+
<h1 align="center">
|
|
4
|
+
Svelte Infinite
|
|
5
|
+
</h1>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+

|
|
10
|
+
[](https://npmjs.org/packages/svelte-infinite)
|
|
11
|
+
[](https://npmjs.org/packages/svelte-infinite)
|
|
12
|
+
[](https://npmjs.org/packages/svelte-infinite)
|
|
13
|
+
[](https://svelte-5-infinite.vercel.app)
|
|
14
|
+
|
|
15
|
+
> Svelte Infinite Loader designed and rebuilt specifically for use in **Svelte 5** with runes
|
|
16
|
+
|
|
17
|
+
✨ Flexible
|
|
18
|
+
⏰ Infinite Loop Detection
|
|
19
|
+
📣 Control Loader State
|
|
20
|
+
🔎 `IntersectionObserver` based
|
|
21
|
+
🧑🔧 **Demo**: [svelte-5-infinite.vercel.app](https://svelte-5-infinite.vercel.app)
|
|
13
22
|
|
|
14
23
|
## 🏗️ Getting Started
|
|
15
24
|
|
|
16
25
|
1. Install `svelte-infinite`
|
|
17
26
|
|
|
18
27
|
```bash
|
|
28
|
+
npm install svelte-infinite
|
|
19
29
|
pnpm install svelte-infinite
|
|
30
|
+
yarn add svelte-infinite
|
|
20
31
|
```
|
|
21
32
|
|
|
22
33
|
2. Import both `InfiniteLoader` and `stateChanger` from `svelte-infinite`
|
|
23
34
|
|
|
24
|
-
|
|
35
|
+
```svelte
|
|
36
|
+
<script lang="ts">
|
|
37
|
+
import { InfiniteLoader, stateChanger } from "svelte-infinite"
|
|
38
|
+
|
|
39
|
+
const allItems = $state([])
|
|
40
|
+
|
|
41
|
+
const loadMore = async () => {
|
|
42
|
+
const res = fetch("...")
|
|
43
|
+
const data = await jes.json()
|
|
44
|
+
allItems.push(...data)
|
|
45
|
+
stateChanger.loaded()
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<InfiniteLoader triggerLoad={loadMore}>
|
|
50
|
+
{#each allItems as user (user.id)}
|
|
51
|
+
<div>{user.name}</div>
|
|
52
|
+
{/each}
|
|
53
|
+
</InfiniteLoader>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The component should wrap your list of items, and `stateChanger` 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.
|
|
57
|
+
|
|
58
|
+
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.
|
|
25
59
|
|
|
26
60
|
## 🍍 Example
|
|
27
61
|
|
|
28
62
|
```svelte
|
|
29
63
|
<script lang="ts">
|
|
30
64
|
import { InfiniteLoader, stateChanger } from "svelte-infinite"
|
|
65
|
+
import UserCard from "$components/UserCard.svelte"
|
|
31
66
|
|
|
32
67
|
const LOAD_LIMIT = 20
|
|
33
|
-
const allItems = $state<number[]>($page.data.items)
|
|
68
|
+
const allItems = $state<{ id: number, body: string }[]>($page.data.items)
|
|
34
69
|
let pageNumber = $state(1)
|
|
35
70
|
|
|
36
71
|
// 1. You'll have to pass the InfiniteLoader component a load function
|
|
37
72
|
// to its `triggerLoad` prop.
|
|
38
73
|
const loadMore = async () => {
|
|
74
|
+
// This is a relatively straight-forward load function with support for pagination
|
|
39
75
|
try {
|
|
40
76
|
pageNumber += 1
|
|
41
|
-
const limit = LOAD_LIMIT
|
|
42
|
-
const skip = LOAD_LIMIT * (pageNumber - 1)
|
|
77
|
+
const limit = String(LOAD_LIMIT)
|
|
78
|
+
const skip = String(LOAD_LIMIT * (pageNumber - 1))
|
|
43
79
|
|
|
44
|
-
// If there are less results on the first page
|
|
45
|
-
// don't keep trying to fetch more. We're done.
|
|
80
|
+
// If there are less results on the first page (page.server loaded data)
|
|
81
|
+
// than the limit, don't keep trying to fetch more. We're done.
|
|
46
82
|
if (allItems.length < LOAD_LIMIT) {
|
|
47
83
|
stateChanger.complete()
|
|
48
84
|
return
|
|
@@ -50,20 +86,28 @@ pnpm install svelte-infinite
|
|
|
50
86
|
|
|
51
87
|
const searchParams = new URLSearchParams({ limit, skip })
|
|
52
88
|
|
|
89
|
+
// Execute the API call to grab more data
|
|
53
90
|
const dataResponse = await fetch(`/api/data?${searchParams}`)
|
|
54
91
|
|
|
92
|
+
// Ideally, like most paginated endpoints, this should return the data
|
|
93
|
+
// you've requested for your page, as well as the total amount of data
|
|
94
|
+
// available to page through
|
|
95
|
+
|
|
55
96
|
if (!dataResponse.ok) {
|
|
56
97
|
stateChanger.error()
|
|
98
|
+
pageNumber -= 1
|
|
57
99
|
return
|
|
58
100
|
}
|
|
59
101
|
const data = await dataResponse.json()
|
|
60
102
|
|
|
103
|
+
// If we've received data, push it to the reactive state variable
|
|
104
|
+
// rendering our items inside the `<InfiniteLoader />` below.
|
|
61
105
|
if (data.items.length) {
|
|
62
106
|
allItems.push(...data.items)
|
|
63
107
|
}
|
|
64
108
|
|
|
65
|
-
//
|
|
66
|
-
// don't keep trying to fetch more. We're done.
|
|
109
|
+
// If there are more (or equal) number of items loaded as are totally available
|
|
110
|
+
// from the API, don't keep trying to fetch more. We're done.
|
|
67
111
|
if (allItems.length >= data.totalCount) {
|
|
68
112
|
stateChanger.complete()
|
|
69
113
|
} else {
|
|
@@ -72,6 +116,7 @@ pnpm install svelte-infinite
|
|
|
72
116
|
} catch (error) {
|
|
73
117
|
console.error(error)
|
|
74
118
|
stateChanger.error()
|
|
119
|
+
pageNumber -= 1
|
|
75
120
|
}
|
|
76
121
|
}
|
|
77
122
|
</script>
|
|
@@ -84,6 +129,11 @@ pnpm install svelte-infinite
|
|
|
84
129
|
{#each allItems as user (user.id)}
|
|
85
130
|
<UserCard {user} />
|
|
86
131
|
{/each}
|
|
132
|
+
|
|
133
|
+
<!-- There are a few optional slots for customizing what is shown at the bottom
|
|
134
|
+
of the scroller in various states, see README.md for more details -->
|
|
135
|
+
<div slot="loading">Loading...</div>
|
|
136
|
+
<div slot="no-data">Thats it, no more users left!</div>
|
|
87
137
|
</InfiniteLoader>
|
|
88
138
|
</main>
|
|
89
139
|
|
|
@@ -100,28 +150,42 @@ However, there is also a `stateChanger` export which you should use to interact
|
|
|
100
150
|
|
|
101
151
|
The `stateChanger` import is an object with 4 methods on it:
|
|
102
152
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
153
|
+
- `stateChanger.loaded()`
|
|
154
|
+
- Designed to be called after a successful fetch.
|
|
155
|
+
- `stateChanger.error()`
|
|
156
|
+
- 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` slot.
|
|
157
|
+
- `stateChanger.complete()`
|
|
158
|
+
- 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` slot.
|
|
159
|
+
- `stateChanger.reset()`
|
|
160
|
+
- 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.
|
|
107
161
|
|
|
108
162
|
### Props
|
|
109
163
|
|
|
110
|
-
- `triggerLoad: () => Promise<void>` - **required**
|
|
111
|
-
-
|
|
112
|
-
- `
|
|
164
|
+
- `triggerLoad: () => Promise<void>` - **required**
|
|
165
|
+
- The async function to call when we should attempt to load more data to show.
|
|
166
|
+
- `intersectionOptions` - optional
|
|
167
|
+
- 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. Default is `{ rootMargin: "0px 0px 200px 0px" }`, making it trigger the `loadMore` function 200px 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 bottom of the list, making the experience feel more smooth.
|
|
168
|
+
- 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.
|
|
169
|
+
- `loopTimeout: number = 1000` - optional
|
|
170
|
+
- If the `loopMaxCalls` is reached within this duration (in milliseconds), a cool down period is triggered.
|
|
171
|
+
- `loopMaxCalls: number = 5` - optional
|
|
172
|
+
- The number of calls to the `triggerLoad` function within timeout which should trigger cool down period.
|
|
113
173
|
|
|
114
174
|
### Slots
|
|
115
175
|
|
|
116
|
-
- `loading`
|
|
117
|
-
-
|
|
118
|
-
- `no-
|
|
119
|
-
-
|
|
176
|
+
- `loading`
|
|
177
|
+
- Shown while calling `triggerLoad` and waiting on a response.
|
|
178
|
+
- `no-results`
|
|
179
|
+
- 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").
|
|
180
|
+
- `no-data`
|
|
181
|
+
- Shown when `stateChanger.complete()` is called, indicating we've fetched and displayed all available data.
|
|
182
|
+
- `error`
|
|
183
|
+
- Shown when there is an error or `stateChanger.error()` has been called. The slot has an `attemptLoad` prop passed to it which is just the internal `triggerLoad` function, designed for a "Retry" button or similar.
|
|
120
184
|
|
|
121
185
|
## 📦 Contributing
|
|
122
186
|
|
|
123
187
|
- Initially inspired by [jonasgeiler/svelte-infinite-loading](https://github.com/jonasgeiler/svelte-infinite-loading)
|
|
124
|
-
- Open to contributions, issues, and feedback
|
|
188
|
+
- Open to contributions, issues, and feedback 🙏
|
|
125
189
|
|
|
126
190
|
## 📝 License
|
|
127
191
|
|
|
@@ -26,7 +26,12 @@ export const stateChanger = {
|
|
|
26
26
|
</script>
|
|
27
27
|
|
|
28
28
|
<script>import { onDestroy } from "svelte";
|
|
29
|
-
const {
|
|
29
|
+
const {
|
|
30
|
+
triggerLoad,
|
|
31
|
+
loopTimeout = 1e3,
|
|
32
|
+
loopMaxCalls = 5,
|
|
33
|
+
intersectionOptions = {}
|
|
34
|
+
} = $props();
|
|
30
35
|
const ERROR_INFINITE_LOOP = `Executed load function ${loopMaxCalls} or more times within a short period. Cooling off..`;
|
|
31
36
|
class LoopTracker {
|
|
32
37
|
coolingOff = false;
|
|
@@ -70,14 +75,15 @@ async function attemptLoad() {
|
|
|
70
75
|
$effect(() => {
|
|
71
76
|
if (observer || !intersectionTarget)
|
|
72
77
|
return;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
const appliedIntersectionOptions = {
|
|
79
|
+
rootMargin: "0px 0px 200px 0px",
|
|
80
|
+
...intersectionOptions
|
|
81
|
+
};
|
|
82
|
+
observer = new IntersectionObserver((entries) => {
|
|
83
|
+
if (entries[0]?.isIntersecting) {
|
|
84
|
+
attemptLoad();
|
|
85
|
+
}
|
|
86
|
+
}, appliedIntersectionOptions);
|
|
81
87
|
observer.observe(intersectionTarget);
|
|
82
88
|
});
|
|
83
89
|
onDestroy(() => {
|
|
@@ -87,86 +93,96 @@ onDestroy(() => {
|
|
|
87
93
|
});
|
|
88
94
|
</script>
|
|
89
95
|
|
|
90
|
-
<div class="loader-wrapper">
|
|
96
|
+
<div class="infinite-loader-wrapper">
|
|
91
97
|
<slot />
|
|
92
98
|
|
|
93
|
-
{
|
|
94
|
-
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
{#if showNoResults}
|
|
100
|
-
<slot name="no-results">
|
|
101
|
-
<div class="no-results">No results</div>
|
|
102
|
-
</slot>
|
|
103
|
-
{/if}
|
|
99
|
+
<div class="infinite-intersection-target" bind:this={intersectionTarget}>
|
|
100
|
+
{#if showLoading}
|
|
101
|
+
<slot name="loading">
|
|
102
|
+
<div class="infinite-loading">Loading...</div>
|
|
103
|
+
</slot>
|
|
104
|
+
{/if}
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
{#if showNoResults}
|
|
107
|
+
<slot name="no-results">
|
|
108
|
+
<div class="infinite-no-results">No results</div>
|
|
109
|
+
</slot>
|
|
110
|
+
{/if}
|
|
110
111
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
</div>
|
|
117
|
-
</slot>
|
|
118
|
-
{/if}
|
|
112
|
+
{#if showNoMore}
|
|
113
|
+
<slot name="no-data">
|
|
114
|
+
<div class="infinite-no-data">No more data</div>
|
|
115
|
+
</slot>
|
|
116
|
+
{/if}
|
|
119
117
|
|
|
120
|
-
|
|
118
|
+
{#if showError}
|
|
119
|
+
<slot name="error" {attemptLoad}>
|
|
120
|
+
<div class="infinite-error">
|
|
121
|
+
<div class="infinite-label">Oops, something went wrong</div>
|
|
122
|
+
<button class="infinite-btn" disabled={status === STATUS.COMPLETE} onclick={attemptLoad}>
|
|
123
|
+
Retry
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
</slot>
|
|
127
|
+
{/if}
|
|
128
|
+
</div>
|
|
121
129
|
</div>
|
|
122
130
|
|
|
123
131
|
<style>
|
|
124
|
-
.loader-wrapper {
|
|
132
|
+
.infinite-loader-wrapper {
|
|
125
133
|
display: grid;
|
|
126
134
|
width: 100%;
|
|
127
135
|
place-items: center;
|
|
128
136
|
|
|
129
|
-
.loading {
|
|
137
|
+
.infinite-loading {
|
|
130
138
|
margin-top: 1rem;
|
|
131
139
|
font-size: 1.5rem;
|
|
132
140
|
}
|
|
133
141
|
|
|
134
|
-
.no-results {
|
|
142
|
+
.infinite-no-results {
|
|
135
143
|
margin-top: 1rem;
|
|
136
144
|
font-size: 1.5rem;
|
|
137
145
|
}
|
|
138
146
|
|
|
139
|
-
.no-data {
|
|
147
|
+
.infinite-no-data {
|
|
140
148
|
margin-top: 1rem;
|
|
141
149
|
font-size: 1.5rem;
|
|
142
150
|
}
|
|
143
151
|
|
|
144
|
-
.error {
|
|
152
|
+
.infinite-error {
|
|
145
153
|
display: flex;
|
|
146
154
|
flex-direction: column;
|
|
147
155
|
gap: 1rem;
|
|
148
156
|
font-size: 1.5rem;
|
|
149
157
|
margin-block: 1rem;
|
|
150
158
|
|
|
151
|
-
.label {
|
|
152
|
-
color:
|
|
159
|
+
.infinite-label {
|
|
160
|
+
color: crimson;
|
|
153
161
|
}
|
|
154
162
|
|
|
155
|
-
.btn {
|
|
163
|
+
.infinite-btn {
|
|
156
164
|
color: white;
|
|
157
165
|
background-color: #333;
|
|
158
166
|
padding-inline: 1.5rem;
|
|
159
167
|
padding-block: 0.75rem;
|
|
160
168
|
border-radius: 0.25rem;
|
|
161
169
|
border: none;
|
|
170
|
+
transition: background-color 0.3s;
|
|
171
|
+
line-height: normal;
|
|
162
172
|
}
|
|
163
|
-
.btn:hover {
|
|
173
|
+
.infinite-btn:hover {
|
|
164
174
|
cursor: pointer;
|
|
175
|
+
background-color: #222;
|
|
165
176
|
}
|
|
166
177
|
}
|
|
167
178
|
|
|
168
|
-
.target {
|
|
169
|
-
|
|
179
|
+
.infinite-intersection-target {
|
|
180
|
+
width: 100%;
|
|
181
|
+
min-height: 1px;
|
|
182
|
+
display: flex;
|
|
183
|
+
flex-direction: column;
|
|
184
|
+
align-items: center;
|
|
185
|
+
justify-content: center;
|
|
170
186
|
}
|
|
171
187
|
}
|
|
172
188
|
</style>
|
|
@@ -10,6 +10,7 @@ declare const __propDef: {
|
|
|
10
10
|
triggerLoad: () => Promise<void>;
|
|
11
11
|
loopTimeout?: number | undefined;
|
|
12
12
|
loopMaxCalls?: number | undefined;
|
|
13
|
+
intersectionOptions?: IntersectionObserverInit | undefined;
|
|
13
14
|
} & {
|
|
14
15
|
children?: ((this: void) => typeof import("svelte").SnippetReturn & {
|
|
15
16
|
_: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\"";
|
package/package.json
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-infinite",
|
|
3
3
|
"description": "Infinite scroll for Svelte 5 with Runes",
|
|
4
|
-
"author":
|
|
5
|
-
|
|
4
|
+
"author": {
|
|
5
|
+
"name": "Nico Domino",
|
|
6
|
+
"email": "yo@ndo.dev",
|
|
7
|
+
"url": "https://ndo.dev"
|
|
8
|
+
},
|
|
9
|
+
"version": "0.1.2",
|
|
6
10
|
"license": "MIT",
|
|
7
11
|
"homepage": "https://svelte-5-infinite.vercel.app",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"infinite-loader",
|
|
14
|
+
"svelte",
|
|
15
|
+
"svelte5",
|
|
16
|
+
"sveltekit"
|
|
17
|
+
],
|
|
8
18
|
"repository": {
|
|
9
19
|
"type": "git",
|
|
10
20
|
"url": "git+https://github.com/ndom91/svelte-infinite.git"
|
|
@@ -12,24 +22,9 @@
|
|
|
12
22
|
"bugs": {
|
|
13
23
|
"url": "https://github.com/ndom91/svelte-infinite/issues"
|
|
14
24
|
},
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"preview": "vite preview",
|
|
19
|
-
"package": "svelte-kit sync && svelte-package && publint",
|
|
20
|
-
"prepublishOnly": "npm run package",
|
|
21
|
-
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
22
|
-
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
23
|
-
"test": "vitest",
|
|
24
|
-
"lint": "prettier --check . && eslint .",
|
|
25
|
-
"format": "prettier --write ."
|
|
26
|
-
},
|
|
27
|
-
"keywords": [
|
|
28
|
-
"infinite-loader",
|
|
29
|
-
"svelte",
|
|
30
|
-
"svelte5",
|
|
31
|
-
"sveltekit"
|
|
32
|
-
],
|
|
25
|
+
"svelte": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"type": "module",
|
|
33
28
|
"exports": {
|
|
34
29
|
".": {
|
|
35
30
|
"types": "./dist/index.d.ts",
|
|
@@ -65,7 +60,15 @@
|
|
|
65
60
|
"vite": "^5.1.4",
|
|
66
61
|
"vitest": "^1.3.1"
|
|
67
62
|
},
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
"scripts": {
|
|
64
|
+
"dev": "vite dev",
|
|
65
|
+
"build": "vite build && npm run package",
|
|
66
|
+
"preview": "vite preview",
|
|
67
|
+
"package": "svelte-kit sync && svelte-package && publint",
|
|
68
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
69
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
70
|
+
"test": "vitest",
|
|
71
|
+
"lint": "prettier --check . && eslint .",
|
|
72
|
+
"format": "prettier --write ."
|
|
73
|
+
}
|
|
74
|
+
}
|