svelte-infinite 0.1.0 → 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,49 +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
- > [!NOTE]
13
- > Initially inspired by [jonasgeiler/svelte-infinite-loading](https://github.com/jonasgeiler/svelte-infinite-loading)
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)
14
22
 
15
23
  ## 🏗️ Getting Started
16
24
 
17
25
  1. Install `svelte-infinite`
18
26
 
19
27
  ```bash
28
+ npm install svelte-infinite
20
29
  pnpm install svelte-infinite
30
+ yarn add svelte-infinite
21
31
  ```
22
32
 
23
33
  2. Import both `InfiniteLoader` and `stateChanger` from `svelte-infinite`
24
34
 
25
- 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.
26
57
 
27
- ## 🍍 Example
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.
59
+
60
+ ## 🍍 Example
28
61
 
29
62
  ```svelte
30
63
  <script lang="ts">
31
64
  import { InfiniteLoader, stateChanger } from "svelte-infinite"
65
+ import UserCard from "$components/UserCard.svelte"
32
66
 
33
67
  const LOAD_LIMIT = 20
34
- const allItems = $state<number[]>($page.data.items)
68
+ const allItems = $state<{ id: number, body: string }[]>($page.data.items)
35
69
  let pageNumber = $state(1)
36
70
 
37
71
  // 1. You'll have to pass the InfiniteLoader component a load function
38
72
  // to its `triggerLoad` prop.
39
73
  const loadMore = async () => {
74
+ // This is a relatively straight-forward load function with support for pagination
40
75
  try {
41
76
  pageNumber += 1
42
- const limit = LOAD_LIMIT
43
- const skip = LOAD_LIMIT * (pageNumber - 1)
77
+ const limit = String(LOAD_LIMIT)
78
+ const skip = String(LOAD_LIMIT * (pageNumber - 1))
44
79
 
45
- // If there are less results on the first page than the limit,
46
- // 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.
47
82
  if (allItems.length < LOAD_LIMIT) {
48
83
  stateChanger.complete()
49
84
  return
@@ -51,20 +86,28 @@ pnpm install svelte-infinite
51
86
 
52
87
  const searchParams = new URLSearchParams({ limit, skip })
53
88
 
89
+ // Execute the API call to grab more data
54
90
  const dataResponse = await fetch(`/api/data?${searchParams}`)
55
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
+
56
96
  if (!dataResponse.ok) {
57
97
  stateChanger.error()
98
+ pageNumber -= 1
58
99
  return
59
100
  }
60
101
  const data = await dataResponse.json()
61
102
 
103
+ // If we've received data, push it to the reactive state variable
104
+ // rendering our items inside the `<InfiniteLoader />` below.
62
105
  if (data.items.length) {
63
106
  allItems.push(...data.items)
64
107
  }
65
108
 
66
- // There are less items available than fit on one page,
67
- // 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.
68
111
  if (allItems.length >= data.totalCount) {
69
112
  stateChanger.complete()
70
113
  } else {
@@ -73,6 +116,7 @@ pnpm install svelte-infinite
73
116
  } catch (error) {
74
117
  console.error(error)
75
118
  stateChanger.error()
119
+ pageNumber -= 1
76
120
  }
77
121
  }
78
122
  </script>
@@ -85,6 +129,11 @@ pnpm install svelte-infinite
85
129
  {#each allItems as user (user.id)}
86
130
  <UserCard {user} />
87
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>
88
137
  </InfiniteLoader>
89
138
  </main>
90
139
 
@@ -101,23 +150,42 @@ However, there is also a `stateChanger` export which you should use to interact
101
150
 
102
151
  The `stateChanger` import is an object with 4 methods on it:
103
152
 
104
- 1. `stateChanger.loaded()` - Designed to be called after a successful fetch.
105
- 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.
106
- 3. `stateChanger.complete()` - Designed to be called when you've reached the end of your list and there are no more items to fetch.
107
- 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.
108
161
 
109
162
  ### Props
110
163
 
111
- - `triggerLoad: () => Promise<void>` - **required** - The function to call when the user scrolls to the bottom of the list.
112
- - `loopTimeout: number = 1000` - optional - If the `loopMaxCalls` is reached within this duration (in milliseconds), a cool down period is triggered.
113
- - `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.
114
173
 
115
174
  ### Slots
116
175
 
117
- - `loading` - Shown while calling `triggerLoad` and waiting on a response.
118
- - `no-results` - Shown when there are no results to display.
119
- - `no-data` - Shown when `stateChanger.complete()` is called, indicating the end of scroll.
120
- - `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.
184
+
185
+ ## 📦 Contributing
186
+
187
+ - Initially inspired by [jonasgeiler/svelte-infinite-loading](https://github.com/jonasgeiler/svelte-infinite-loading)
188
+ - Open to contributions, issues, and feedback 🙏
121
189
 
122
190
  ## 📝 License
123
191
 
@@ -25,7 +25,13 @@ export const stateChanger = {
25
25
  };
26
26
  </script>
27
27
 
28
- <script>const { triggerLoad, loopTimeout = 1e3, loopMaxCalls = 5 } = $props();
28
+ <script>import { onDestroy } from "svelte";
29
+ const {
30
+ triggerLoad,
31
+ loopTimeout = 1e3,
32
+ loopMaxCalls = 5,
33
+ intersectionOptions = {}
34
+ } = $props();
29
35
  const ERROR_INFINITE_LOOP = `Executed load function ${loopMaxCalls} or more times within a short period. Cooling off..`;
30
36
  class LoopTracker {
31
37
  coolingOff = false;
@@ -67,105 +73,116 @@ async function attemptLoad() {
67
73
  }
68
74
  }
69
75
  $effect(() => {
70
- if (!observer) {
71
- if (intersectionTarget) {
72
- observer = new IntersectionObserver(
73
- (entries) => {
74
- if (entries[0]?.isIntersecting) {
75
- attemptLoad();
76
- }
77
- },
78
- { rootMargin: "100px 0px 0px 0px" }
79
- );
80
- observer.observe(intersectionTarget);
81
- return observer;
76
+ if (observer || !intersectionTarget)
77
+ return;
78
+ const appliedIntersectionOptions = {
79
+ rootMargin: "0px 0px 200px 0px",
80
+ ...intersectionOptions
81
+ };
82
+ observer = new IntersectionObserver((entries) => {
83
+ if (entries[0]?.isIntersecting) {
84
+ attemptLoad();
82
85
  }
86
+ }, appliedIntersectionOptions);
87
+ observer.observe(intersectionTarget);
88
+ });
89
+ onDestroy(() => {
90
+ if (observer) {
91
+ observer.disconnect();
83
92
  }
84
- return () => observer?.disconnect();
85
93
  });
86
94
  </script>
87
95
 
88
- <div class="loader-wrapper">
96
+ <div class="infinite-loader-wrapper">
89
97
  <slot />
90
98
 
91
- {#if showLoading}
92
- <slot name="loading">
93
- <div class="loading">Loading...</div>
94
- </slot>
95
- {/if}
96
-
97
- {#if showNoResults}
98
- <slot name="no-results">
99
- <div class="no-results">No results</div>
100
- </slot>
101
- {/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}
102
105
 
103
- {#if showNoMore}
104
- <slot name="no-data">
105
- <div class="no-data">No more data</div>
106
- </slot>
107
- {/if}
106
+ {#if showNoResults}
107
+ <slot name="no-results">
108
+ <div class="infinite-no-results">No results</div>
109
+ </slot>
110
+ {/if}
108
111
 
109
- {#if showError}
110
- <slot name="error" {attemptLoad}>
111
- <div class="error">
112
- <div class="label">Oops, something went wrong</div>
113
- <button class="btn" on:click={attemptLoad}> Retry </button>
114
- </div>
115
- </slot>
116
- {/if}
112
+ {#if showNoMore}
113
+ <slot name="no-data">
114
+ <div class="infinite-no-data">No more data</div>
115
+ </slot>
116
+ {/if}
117
117
 
118
- <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>
119
129
  </div>
120
130
 
121
131
  <style>
122
- .loader-wrapper {
132
+ .infinite-loader-wrapper {
123
133
  display: grid;
124
134
  width: 100%;
125
135
  place-items: center;
126
- margin-block: 2rem;
127
136
 
128
- .loading {
137
+ .infinite-loading {
129
138
  margin-top: 1rem;
130
139
  font-size: 1.5rem;
131
140
  }
132
141
 
133
- .no-results {
142
+ .infinite-no-results {
134
143
  margin-top: 1rem;
135
144
  font-size: 1.5rem;
136
145
  }
137
146
 
138
- .no-data {
147
+ .infinite-no-data {
139
148
  margin-top: 1rem;
140
149
  font-size: 1.5rem;
141
150
  }
142
151
 
143
- .error {
152
+ .infinite-error {
144
153
  display: flex;
145
154
  flex-direction: column;
146
155
  gap: 1rem;
147
156
  font-size: 1.5rem;
148
157
  margin-block: 1rem;
149
158
 
150
- .label {
151
- color: firebrick;
159
+ .infinite-label {
160
+ color: crimson;
152
161
  }
153
162
 
154
- .btn {
163
+ .infinite-btn {
155
164
  color: white;
156
165
  background-color: #333;
157
166
  padding-inline: 1.5rem;
158
167
  padding-block: 0.75rem;
159
168
  border-radius: 0.25rem;
160
169
  border: none;
170
+ transition: background-color 0.3s;
171
+ line-height: normal;
161
172
  }
162
- .btn:hover {
173
+ .infinite-btn:hover {
163
174
  cursor: pointer;
175
+ background-color: #222;
164
176
  }
165
177
  }
166
178
 
167
- .target {
168
- height: 4rem;
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;
169
186
  }
170
187
  }
171
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.0",
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
- "homepage": "https://github.com/ndom91/svelte-infinite",
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",
@@ -45,27 +40,35 @@
45
40
  "svelte": "^5.0.0"
46
41
  },
47
42
  "devDependencies": {
48
- "@sveltejs/adapter-auto": "^3.0.0",
49
- "@sveltejs/kit": "^2.0.0",
50
- "@sveltejs/package": "^2.0.0",
51
- "@sveltejs/vite-plugin-svelte": "^3.0.0",
52
- "@types/eslint": "^8.56.0",
53
- "@typescript-eslint/eslint-plugin": "^7.0.0",
54
- "@typescript-eslint/parser": "^7.0.0",
55
- "eslint": "^8.56.0",
43
+ "@sveltejs/adapter-auto": "^3.1.1",
44
+ "@sveltejs/kit": "^2.5.2",
45
+ "@sveltejs/package": "^2.2.7",
46
+ "@sveltejs/vite-plugin-svelte": "^3.0.2",
47
+ "@types/eslint": "^8.56.5",
48
+ "@typescript-eslint/eslint-plugin": "^7.1.0",
49
+ "@typescript-eslint/parser": "^7.1.0",
50
+ "eslint": "^8.57.0",
56
51
  "eslint-config-prettier": "^9.1.0",
57
52
  "eslint-plugin-svelte": "^2.36.0-next.4",
58
- "prettier": "^3.1.1",
59
- "prettier-plugin-svelte": "^3.1.2",
60
- "publint": "^0.1.9",
53
+ "prettier": "^3.2.5",
54
+ "prettier-plugin-svelte": "^3.2.2",
55
+ "publint": "^0.2.7",
61
56
  "svelte": "^5.0.0-beta.70",
62
- "svelte-check": "^3.6.0",
63
- "tslib": "^2.4.1",
64
- "typescript": "^5.0.0",
65
- "vite": "^5.0.11",
66
- "vitest": "^1.2.0"
57
+ "svelte-check": "^3.6.6",
58
+ "tslib": "^2.6.2",
59
+ "typescript": "^5.3.3",
60
+ "vite": "^5.1.4",
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
+ }