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 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 slots for customizing what is shown at the bottom
136
- of the scroller in various states, see README.md for more details -->
137
- <div slot="loading">Loading...</div>
138
- <div slot="no-data">Thats it, no more users left!</div>
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
- The `InfiniteLoader` component is a wrapper around your items, which will trigger the `triggerLoad` function when the user scrolls to the bottom of the list.
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
- However, there is also a `loaderState` export 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. See the `loadMore` function above or the example application in `/src/routes` in this repository.
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` slot.
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` slot.
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 = 1000` - optional
172
- - If the `loopMaxCalls` is reached within this duration (in milliseconds), a cool down period is triggered.
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
- ### Slots
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
- - `no-results`
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
- - `no-data`
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 slot has an `attemptLoad` prop passed to it which is just the internal `triggerLoad` function, designed for a "Retry" button or similar.
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 = 1e3,
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 = `Executed load function ${loopMaxCalls} or more times within a short period. Cooling off..`;
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
- timer = null;
39
- count = 0;
44
+ #coolingOffTimer = null;
45
+ #timer = null;
46
+ #count = 0;
40
47
  track() {
41
- this.count += 1;
42
- if (this.count >= loopMaxCalls) {
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.timer = setTimeout(() => {
56
+ this.#coolingOffTimer = setTimeout(() => {
46
57
  this.coolingOff = false;
47
- this.count = 0;
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 showNoMore = $derived(status === STATUS.COMPLETE && !isFirstLoad);
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
- <slot />
108
+ {@render children()}
98
109
 
99
110
  <div class="infinite-intersection-target" bind:this={intersectionTarget}>
100
111
  {#if showLoading}
101
- <slot name="loading">
112
+ {#if loading}
113
+ {@render loading()}
114
+ {:else}
102
115
  <div class="infinite-loading">Loading...</div>
103
- </slot>
116
+ {/if}
104
117
  {/if}
105
118
 
106
119
  {#if showNoResults}
107
- <slot name="no-results">
120
+ {#if noResults}
121
+ {@render noResults()}
122
+ {:else}
108
123
  <div class="infinite-no-results">No results</div>
109
- </slot>
124
+ {/if}
110
125
  {/if}
111
126
 
112
- {#if showNoMore}
113
- <slot name="no-data">
127
+ {#if showNoData}
128
+ {#if noData}
129
+ {@render noData()}
130
+ {:else}
114
131
  <div class="infinite-no-data">No more data</div>
115
- </slot>
132
+ {/if}
116
133
  {/if}
117
134
 
118
135
  {#if showError}
119
- <slot name="error" {attemptLoad}>
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
- </slot>
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
- children?: ((this: void) => typeof import("svelte").SnippetReturn & {
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 };
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.1.6",
9
+ "version": "0.2.1",
10
10
  "license": "MIT",
11
11
  "homepage": "https://svelte-5-infinite.vercel.app",
12
12
  "keywords": [