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 CHANGED
@@ -12,15 +12,18 @@
12
12
  [![](https://img.shields.io/npm/dm/svelte-infinite?style=for-the-badge&labelColor=black&color=black)](https://npmjs.org/packages/svelte-infinite)
13
13
  [![](https://img.shields.io/badge/demo-black?style=for-the-badge&logo=&logoColor=white&labelColor=black&color=black)](https://svelte-5-infinite.vercel.app)
14
14
 
15
- > Svelte Infinite Loader designed and rebuilt specifically for use in **Svelte 5** with runes
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 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
+ 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. You'll have to pass the InfiniteLoader component a load function
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
- // Execute the API call to grab more data
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` import is an object with 4 methods on it:
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 `no-data` snippet.
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 infinite list and the user enters a new query.
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
- - 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.
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.
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 number of calls to the `triggerLoad` function within timeout which should trigger cool down period.
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 4 props for snippets available.
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 = 2e3,
32
- loopDetectionTimeout = 1e3,
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 class="infinite-btn" disabled={status === STATUS.COMPLETE} onclick={attemptLoad}>
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>
@@ -1,10 +1,4 @@
1
1
  import { SvelteComponent } from "svelte";
2
- export declare const loaderState: {
3
- loaded: () => void;
4
- complete: () => void;
5
- reset: () => void;
6
- error: () => void;
7
- };
8
2
  import { type Snippet } from "svelte";
9
3
  declare const __propDef: {
10
4
  props: {
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- import InfiniteLoader, { loaderState } from "./InfiniteLoader.svelte";
1
+ import InfiniteLoader from "./InfiniteLoader.svelte";
2
+ import { loaderState } from "./loaderState.svelte";
2
3
  export { InfiniteLoader, loaderState };
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
- import InfiniteLoader, { loaderState } from "./InfiniteLoader.svelte";
1
+ import InfiniteLoader from "./InfiniteLoader.svelte";
2
+ import { loaderState } from "./loaderState.svelte";
2
3
  export { InfiniteLoader, loaderState };
@@ -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.2.2",
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
  },