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 CHANGED
File without changes
package/README.md CHANGED
@@ -1,48 +1,84 @@
1
- <img align="right" src="https://raw.githubusercontent.com/ndom91/svelte-infinite/51683d459ae954a99e7c5c25817ed667678a0840/src/assets/SvelteLogo.svg" alt="Svelte Logo" width="128px" />
2
-
3
- # Svelte Infinite
4
-
5
- Svelte Infinite Scroller designed for use in **Svelte 5** with runes
6
-
7
- - ⏰ Infinite Loop Detection
8
- - 📣 Control Loader State
9
- - 🔎 `IntersectionObservor` based
10
- - ✨ Flexible
11
-
12
- **Demo**: [svelte-5-infinite.vercel.app](https://svelte-5-infinite.vercel.app)
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
+ ![](https://img.shields.io/badge/typescript-black?style=for-the-badge&logo=typescript&logoColor=white)
9
+ ![](https://img.shields.io/badge/only-svelte5?style=for-the-badge&logo=svelte&logoColor=white&label=svelte5&labelColor=black&color=black)
10
+ [![](https://img.shields.io/npm/v/svelte-infinite?style=for-the-badge&labelColor=black&color=black)](https://npmjs.org/packages/svelte-infinite)
11
+ [![](https://img.shields.io/badge/13kb-size?style=for-the-badge&label=size&labelColor=black&color=black)](https://npmjs.org/packages/svelte-infinite)
12
+ [![](https://img.shields.io/npm/dm/svelte-infinite?style=for-the-badge&labelColor=black&color=black)](https://npmjs.org/packages/svelte-infinite)
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
+
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
- 3. The component should wrap your list of items, and `stateChanger` should be used in your `triggerLoad` function to interact with the internal state of the component, telling it whether you're out of data, ran into an error, etc. 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.
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 than the limit,
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
- // There are less items available than fit on one page,
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
- 1. `stateChanger.loaded()` - Designed to be called after a successful fetch.
104
- 2. `stateChanger.error()` - Designed to be called after a failed fetch or any other error. This will cause the `InfiniteLoader` to render a "Retry" button.
105
- 3. `stateChanger.complete()` - Designed to be called when you've reached the end of your list and there are no more items to fetch.
106
- 4. `stateChanger.reset()` - 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.
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** - The function to call when the user scrolls to the bottom of the list.
111
- - `loopTimeout: number = 1000` - optional - If the `loopMaxCalls` is reached within this duration (in milliseconds), a cool down period is triggered.
112
- - `loopMaxCalls: number = 5` - optional - The number of calls to the `triggerLoad` function within timeout which should trigger cool down period.
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` - Shown while calling `triggerLoad` and waiting on a response.
117
- - `no-results` - Shown when there are no results to display.
118
- - `no-data` - Shown when `stateChanger.complete()` is called, indicating the end of scroll.
119
- - `error` - Shown when there is an error. Slot has an `attemptLoad` prop passed to it which is the `triggerLoad` function, designed for a "Retry" button or similar.
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 { triggerLoad, loopTimeout = 1e3, loopMaxCalls = 5 } = $props();
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
- observer = new IntersectionObserver(
74
- (entries) => {
75
- if (entries[0]?.isIntersecting) {
76
- attemptLoad();
77
- }
78
- },
79
- { rootMargin: "100px 0px 0px 0px" }
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
- {#if showLoading}
94
- <slot name="loading">
95
- <div class="loading">Loading...</div>
96
- </slot>
97
- {/if}
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
- {#if showNoMore}
106
- <slot name="no-data">
107
- <div class="no-data">No more data</div>
108
- </slot>
109
- {/if}
106
+ {#if showNoResults}
107
+ <slot name="no-results">
108
+ <div class="infinite-no-results">No results</div>
109
+ </slot>
110
+ {/if}
110
111
 
111
- {#if showError}
112
- <slot name="error" {attemptLoad}>
113
- <div class="error">
114
- <div class="label">Oops, something went wrong</div>
115
- <button class="btn" on:click={attemptLoad}> Retry </button>
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
- <div class="target" bind:this={intersectionTarget} />
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: firebrick;
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
- height: 1rem;
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": "ndom91 <yo@ndo.dev>",
5
- "version": "0.1.1",
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
- "scripts": {
16
- "dev": "vite dev",
17
- "build": "vite build && npm run package",
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
- "svelte": "./dist/index.js",
69
- "types": "./dist/index.d.ts",
70
- "type": "module"
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
+ }