svelte-infinite 0.5.0 → 0.5.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/README.md CHANGED
@@ -12,14 +12,15 @@
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 with **Svelte 5**
15
+ Flexible
16
+ ⏰ Infinite Loop Detection
17
+ 📣 Control Loader State
18
+ 🔎 `IntersectionObserver` based
19
+ 🔥 Using Runes and Snippets
20
+ 🧑‍🔧 **Demo**: [svelte-5-infinite.vercel.app](https://svelte-5-infinite.vercel.app)
16
21
 
17
- Flexible
18
- Infinite Loop Detection
19
- 📣 Control Loader State
20
- 🔎 `IntersectionObserver` based
21
- 🔥 Using Runes and Snippets
22
- 🧑‍🔧 **Demo**: [svelte-5-infinite.vercel.app](https://svelte-5-infinite.vercel.app)
22
+ > [!WARNING]
23
+ > `v0.5.0` contains a breaking change. See [this PR](https://github.com/ndom91/svelte-infinite/pull/12) for more details including migration steps.
23
24
 
24
25
  ## 🏗️ Getting Started
25
26
 
@@ -145,8 +146,6 @@ This is a more realistic example use-case which includes a paginated data endpoi
145
146
  {/snippet}
146
147
  </InfiniteLoader>
147
148
  </main>
148
-
149
- </script>
150
149
  ```
151
150
 
152
151
  ## ♾️ Usage
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { onMount, onDestroy, type Snippet } from "svelte"
2
+ import { type Snippet } from "svelte"
3
3
  import { STATUS, LoaderState } from "./loaderState.svelte"
4
4
 
5
5
  type InfiniteLoaderProps = {
@@ -7,14 +7,14 @@
7
7
  loopTimeout?: number
8
8
  loopDetectionTimeout?: number
9
9
  loopMaxCalls?: number
10
- intersectionOptions?: Partial<IntersectionObserver>
10
+ intersectionOptions?: IntersectionObserverInit
11
11
  loaderState: LoaderState
12
12
  children: Snippet
13
13
  loading?: Snippet
14
14
  noResults?: Snippet
15
15
  noData?: Snippet
16
16
  coolingOff?: Snippet
17
- error?: Snippet<[typeof attemptLoad]>
17
+ error?: Snippet<[() => Promise<void>]>
18
18
  }
19
19
 
20
20
  const {
@@ -34,50 +34,16 @@
34
34
 
35
35
  const ERROR_INFINITE_LOOP = `Attempted to execute load function ${loopMaxCalls} or more times within a short period. Please wait before trying again..`
36
36
 
37
- // Track load counts to avoid infinite loops
38
- class LoopTracker {
39
- coolingOff = false
40
- #coolingOffTimer: number | null = null
41
- #timer: number | null = null
42
- #count = 0
43
-
44
- // On each call, increment the count and reset the timer
45
- track() {
46
- this.#count += 1
47
-
48
- clearTimeout(this.#timer!)
49
- // Cooldown, after 2s, reset count to 0
50
- this.#timer = setTimeout(() => {
51
- this.#count = 0
52
- }, loopDetectionTimeout)
53
-
54
- // If count > loopMaxCalls, begin cool-down period
55
- // and start timer to reset loop count tracker
56
- if (this.#count >= loopMaxCalls) {
57
- console.error(ERROR_INFINITE_LOOP)
58
-
59
- this.coolingOff = true
60
- this.#coolingOffTimer = setTimeout(() => {
61
- this.coolingOff = false
62
- this.#count = 0
63
- }, loopTimeout)
64
- }
65
- }
66
-
67
- destroy() {
68
- if (this.#timer) {
69
- clearTimeout(this.#timer)
70
- }
71
- if (this.#coolingOffTimer) {
72
- clearTimeout(this.#coolingOffTimer)
73
- }
74
- }
75
- }
76
-
77
- const loopTracker = new LoopTracker()
78
-
79
37
  let intersectionTarget = $state<HTMLElement>()
80
- let observer = $state<IntersectionObserver>()
38
+ let loopTracker = $state<{
39
+ coolingOff: boolean
40
+ count: number
41
+ timers: (number | null)[]
42
+ }>({
43
+ coolingOff: false,
44
+ count: 0,
45
+ timers: []
46
+ })
81
47
 
82
48
  let showLoading = $derived(loaderState.status === STATUS.LOADING)
83
49
  let showError = $derived(loaderState.status === STATUS.ERROR)
@@ -85,10 +51,34 @@
85
51
  let showNoData = $derived(loaderState.status === STATUS.COMPLETE && !loaderState.isFirstLoad)
86
52
  let showCoolingOff = $derived(loaderState.status !== STATUS.COMPLETE && loopTracker.coolingOff)
87
53
 
54
+ function trackLoad() {
55
+ loopTracker.count += 1
56
+
57
+ loopTracker.timers.forEach((timer) => {
58
+ if (timer !== null) clearTimeout(timer)
59
+ })
60
+ loopTracker.timers = []
61
+
62
+ loopTracker.timers.push(
63
+ setTimeout(() => {
64
+ loopTracker.count = 0
65
+ }, loopDetectionTimeout)
66
+ )
67
+
68
+ if (loopTracker.count >= loopMaxCalls) {
69
+ console.error(ERROR_INFINITE_LOOP)
70
+
71
+ loopTracker.coolingOff = true
72
+ loopTracker.timers.push(
73
+ setTimeout(() => {
74
+ loopTracker.coolingOff = false
75
+ loopTracker.count = 0
76
+ }, loopTimeout)
77
+ )
78
+ }
79
+ }
80
+
88
81
  async function attemptLoad() {
89
- // If we're complete, don't attempt to load again
90
- // If we're not ready (i.e. in the middle of a fetch) don't attempt to load again
91
- // However, if we're in an error state, allow the user to retry via btn click
92
82
  if (
93
83
  loaderState.status === STATUS.COMPLETE ||
94
84
  (loaderState.status !== STATUS.READY && loaderState.status !== STATUS.ERROR)
@@ -98,47 +88,36 @@
98
88
 
99
89
  loaderState.status = STATUS.LOADING
100
90
 
101
- // Skip loading if we're in infinite loop cool-off
102
91
  if (!loopTracker.coolingOff) {
103
92
  await triggerLoad()
104
- loopTracker.track()
93
+ trackLoad()
105
94
  }
106
95
 
107
- // @ts-expect-error - client can set status to 'COMPLETE' inside the
108
- // `triggerLoad` fn above via `loaderState.complete()`, TS obviously doesn't know this.
109
- if (loaderState.status !== STATUS.ERROR && loaderState.status !== STATUS.COMPLETE) {
110
- if (loaderState.status === STATUS.LOADING) {
111
- loaderState.isFirstLoad = false
112
- loaderState.status = STATUS.READY
113
- }
96
+ if (loaderState.status === STATUS.LOADING) {
97
+ loaderState.isFirstLoad = false
98
+ loaderState.status = STATUS.READY
114
99
  }
115
100
  }
116
101
 
117
- onMount(() => {
118
- if (observer || !intersectionTarget) return
102
+ $effect(() => {
103
+ if (!intersectionTarget) return
119
104
 
120
105
  const appliedIntersectionOptions = {
121
106
  rootMargin: "0px 0px 200px 0px",
122
107
  ...intersectionOptions
123
108
  }
124
- observer = new IntersectionObserver(async (entries) => {
125
- if (entries[0]?.isIntersecting && loaderState.mounted) {
109
+ const obs = new IntersectionObserver(async (entries) => {
110
+ if (entries[0]?.isIntersecting) {
126
111
  await attemptLoad()
127
112
  }
128
113
  }, appliedIntersectionOptions)
129
- observer.observe(intersectionTarget)
114
+ obs.observe(intersectionTarget)
130
115
 
131
- loaderState.mounted = true
132
- })
133
-
134
- onDestroy(() => {
135
- if (loaderState.mounted) {
136
- if (observer) {
137
- observer.disconnect()
138
- }
139
- if (loopTracker) {
140
- loopTracker.destroy()
141
- }
116
+ return () => {
117
+ obs.disconnect()
118
+ loopTracker.timers.forEach((timer) => {
119
+ if (timer !== null) clearTimeout(timer)
120
+ })
142
121
  }
143
122
  })
144
123
  </script>
@@ -1,11 +1,11 @@
1
1
  import { type Snippet } from "svelte";
2
2
  import { LoaderState } from "./loaderState.svelte";
3
- declare const InfiniteLoader: import("svelte").Component<{
3
+ type InfiniteLoaderProps = {
4
4
  triggerLoad: () => Promise<void>;
5
5
  loopTimeout?: number;
6
6
  loopDetectionTimeout?: number;
7
7
  loopMaxCalls?: number;
8
- intersectionOptions?: Partial<IntersectionObserver>;
8
+ intersectionOptions?: IntersectionObserverInit;
9
9
  loaderState: LoaderState;
10
10
  children: Snippet;
11
11
  loading?: Snippet;
@@ -13,6 +13,7 @@ declare const InfiniteLoader: import("svelte").Component<{
13
13
  noData?: Snippet;
14
14
  coolingOff?: Snippet;
15
15
  error?: Snippet<[() => Promise<void>]>;
16
- }, {}, "">;
16
+ };
17
+ declare const InfiniteLoader: import("svelte").Component<InfiniteLoaderProps, {}, "">;
17
18
  type InfiniteLoader = ReturnType<typeof InfiniteLoader>;
18
19
  export default InfiniteLoader;
@@ -7,7 +7,6 @@ export declare const STATUS: {
7
7
  export declare class LoaderState {
8
8
  isFirstLoad: boolean;
9
9
  status: "READY" | "LOADING" | "COMPLETE" | "ERROR";
10
- mounted: boolean;
11
10
  loaded: () => void;
12
11
  complete: () => void;
13
12
  reset: () => void;
@@ -7,7 +7,6 @@ export const STATUS = {
7
7
  export class LoaderState {
8
8
  isFirstLoad = $state(true);
9
9
  status = $state(STATUS.READY);
10
- mounted = $state(false);
11
10
  loaded = () => {
12
11
  if (this.isFirstLoad)
13
12
  this.isFirstLoad = false;
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.5.0",
9
+ "version": "0.5.2",
10
10
  "license": "MIT",
11
11
  "homepage": "https://svelte-5-infinite.vercel.app",
12
12
  "keywords": [
@@ -52,30 +52,30 @@
52
52
  "svelte": "^5.0.0-0"
53
53
  },
54
54
  "devDependencies": {
55
- "@eslint/eslintrc": "^3.1.0",
56
- "@eslint/js": "^9.14.0",
57
- "@sveltejs/adapter-auto": "^3.3.1",
58
- "@sveltejs/kit": "^2.8.1",
59
- "@sveltejs/package": "^2.3.7",
60
- "@sveltejs/vite-plugin-svelte": "^4.0.0",
55
+ "@eslint/eslintrc": "^3.3.3",
56
+ "@eslint/js": "^9.39.2",
57
+ "@sveltejs/adapter-auto": "^7.0.1",
58
+ "@sveltejs/kit": "^2.51.0",
59
+ "@sveltejs/package": "^2.5.7",
60
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
61
61
  "@types/eslint": "^9.6.1",
62
- "@typescript-eslint/eslint-plugin": "^8.14.0",
63
- "@typescript-eslint/parser": "^8.14.0",
64
- "eslint": "^9.14.0",
65
- "eslint-config-prettier": "^9.1.0",
66
- "eslint-plugin-svelte": "^2.46.0",
67
- "globals": "^15.12.0",
68
- "prettier": "^3.3.3",
69
- "prettier-plugin-svelte": "^3.2.8",
70
- "publint": "^0.2.12",
71
- "svelte": "^5.1.16",
72
- "svelte-check": "^4.0.7",
62
+ "@typescript-eslint/eslint-plugin": "^8.55.0",
63
+ "@typescript-eslint/parser": "^8.55.0",
64
+ "eslint": "^9.39.2",
65
+ "eslint-config-prettier": "^10.1.8",
66
+ "eslint-plugin-svelte": "^3.15.0",
67
+ "globals": "^17.3.0",
68
+ "prettier": "^3.8.1",
69
+ "prettier-plugin-svelte": "^3.4.1",
70
+ "publint": "^0.3.17",
71
+ "svelte": "^5.51.0",
72
+ "svelte-check": "^4.4.0",
73
73
  "tslib": "^2.8.1",
74
- "typescript": "^5.6.3",
75
- "typescript-eslint": "^8.14.0",
76
- "typescript-svelte-plugin": "^0.3.43",
77
- "vite": "^5.4.11",
78
- "vitest": "^2.1.5"
74
+ "typescript": "^5.9.3",
75
+ "typescript-eslint": "^8.55.0",
76
+ "typescript-svelte-plugin": "^0.3.50",
77
+ "vite": "^7.3.1",
78
+ "vitest": "^4.0.18"
79
79
  },
80
80
  "packageManager": "pnpm@9.0.6+sha256.0624e30eff866cdeb363b15061bdb7fd9425b17bc1bb42c22f5f4efdea21f6b3"
81
81
  }