svelte-infinite 0.3.1 → 0.4.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 +1 -1
- package/dist/InfiniteLoader.svelte +136 -79
- package/dist/InfiniteLoader.svelte.d.ts +15 -36
- package/dist/loaderState.svelte.d.ts +1 -0
- package/dist/loaderState.svelte.js +5 -2
- package/package.json +25 -21
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
🔥 Using Runes and Snippets
|
|
22
22
|
🧑🔧 **Demo**: [svelte-5-infinite.vercel.app](https://svelte-5-infinite.vercel.app)
|
|
23
23
|
|
|
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!
|
|
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
25
|
|
|
26
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!
|
|
27
27
|
|
|
@@ -1,87 +1,144 @@
|
|
|
1
|
-
<script
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class LoopTracker {
|
|
18
|
-
coolingOff = false;
|
|
19
|
-
#coolingOffTimer = null;
|
|
20
|
-
#timer = null;
|
|
21
|
-
#count = 0;
|
|
22
|
-
// On each call, increment the count and reset the timer
|
|
23
|
-
track() {
|
|
24
|
-
this.#count += 1;
|
|
25
|
-
clearTimeout(this.#timer);
|
|
26
|
-
this.#timer = setTimeout(() => {
|
|
27
|
-
this.#count = 0;
|
|
28
|
-
}, loopDetectionTimeout);
|
|
29
|
-
if (this.#count >= loopMaxCalls) {
|
|
30
|
-
console.error(ERROR_INFINITE_LOOP);
|
|
31
|
-
this.coolingOff = true;
|
|
32
|
-
this.#coolingOffTimer = setTimeout(() => {
|
|
33
|
-
this.coolingOff = false;
|
|
34
|
-
this.#count = 0;
|
|
35
|
-
}, loopTimeout);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
destroy() {
|
|
39
|
-
this.#timer && clearTimeout(this.#timer);
|
|
40
|
-
this.#coolingOffTimer && clearTimeout(this.#coolingOffTimer);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
const loopTracker = new LoopTracker();
|
|
44
|
-
let intersectionTarget = $state();
|
|
45
|
-
let observer = $state();
|
|
46
|
-
let showLoading = $derived(loaderState.status === STATUS.LOADING);
|
|
47
|
-
let showError = $derived(loaderState.status === STATUS.ERROR);
|
|
48
|
-
let showNoResults = $derived(loaderState.status === STATUS.COMPLETE && loaderState.isFirstLoad);
|
|
49
|
-
let showNoData = $derived(loaderState.status === STATUS.COMPLETE && !loaderState.isFirstLoad);
|
|
50
|
-
let showCoolingOff = $derived(loaderState.status !== STATUS.COMPLETE && loopTracker.coolingOff);
|
|
51
|
-
async function attemptLoad() {
|
|
52
|
-
if (loaderState.status === STATUS.COMPLETE || loaderState.status !== STATUS.READY && loaderState.status !== STATUS.ERROR) {
|
|
53
|
-
return;
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy, type Snippet } from "svelte"
|
|
3
|
+
import { STATUS, loaderState } from "./loaderState.svelte"
|
|
4
|
+
|
|
5
|
+
type InfiniteLoaderProps = {
|
|
6
|
+
triggerLoad: () => Promise<void>
|
|
7
|
+
loopTimeout?: number
|
|
8
|
+
loopDetectionTimeout?: number
|
|
9
|
+
loopMaxCalls?: number
|
|
10
|
+
intersectionOptions?: Partial<IntersectionObserver>
|
|
11
|
+
children: Snippet
|
|
12
|
+
loading?: Snippet
|
|
13
|
+
noResults?: Snippet
|
|
14
|
+
noData?: Snippet
|
|
15
|
+
coolingOff?: Snippet
|
|
16
|
+
error?: Snippet<[typeof attemptLoad]>
|
|
54
17
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
triggerLoad,
|
|
21
|
+
loopTimeout = 3000,
|
|
22
|
+
loopDetectionTimeout = 2000,
|
|
23
|
+
loopMaxCalls = 5,
|
|
24
|
+
intersectionOptions = {},
|
|
25
|
+
children,
|
|
26
|
+
loading: loadingSnippet,
|
|
27
|
+
noResults: noResultsSnippet,
|
|
28
|
+
noData: noDataSnippet,
|
|
29
|
+
coolingOff: coolingOffSnippet,
|
|
30
|
+
error: errorSnippet
|
|
31
|
+
}: InfiniteLoaderProps = $props()
|
|
32
|
+
|
|
33
|
+
const ERROR_INFINITE_LOOP = `Attempted to execute load function ${loopMaxCalls} or more times within a short period. Please wait before trying again..`
|
|
34
|
+
|
|
35
|
+
// Track load counts to avoid infinite loops
|
|
36
|
+
class LoopTracker {
|
|
37
|
+
coolingOff = false
|
|
38
|
+
#coolingOffTimer: number | null = null
|
|
39
|
+
#timer: number | null = null
|
|
40
|
+
#count = 0
|
|
41
|
+
|
|
42
|
+
// On each call, increment the count and reset the timer
|
|
43
|
+
track() {
|
|
44
|
+
this.#count += 1
|
|
45
|
+
|
|
46
|
+
clearTimeout(this.#timer!)
|
|
47
|
+
// Cooldown, after 2s, reset count to 0
|
|
48
|
+
this.#timer = setTimeout(() => {
|
|
49
|
+
this.#count = 0
|
|
50
|
+
}, loopDetectionTimeout)
|
|
51
|
+
|
|
52
|
+
// If count > loopMaxCalls, begin cool-down period
|
|
53
|
+
// and start timer to reset loop count tracker
|
|
54
|
+
if (this.#count >= loopMaxCalls) {
|
|
55
|
+
console.error(ERROR_INFINITE_LOOP)
|
|
56
|
+
|
|
57
|
+
this.coolingOff = true
|
|
58
|
+
this.#coolingOffTimer = setTimeout(() => {
|
|
59
|
+
this.coolingOff = false
|
|
60
|
+
this.#count = 0
|
|
61
|
+
}, loopTimeout)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
destroy() {
|
|
66
|
+
if (this.#timer) {
|
|
67
|
+
clearTimeout(this.#timer)
|
|
68
|
+
}
|
|
69
|
+
if (this.#coolingOffTimer) {
|
|
70
|
+
clearTimeout(this.#coolingOffTimer)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
59
73
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
|
|
75
|
+
const loopTracker = new LoopTracker()
|
|
76
|
+
|
|
77
|
+
let intersectionTarget = $state<HTMLElement>()
|
|
78
|
+
let observer = $state<IntersectionObserver>()
|
|
79
|
+
|
|
80
|
+
let showLoading = $derived(loaderState.status === STATUS.LOADING)
|
|
81
|
+
let showError = $derived(loaderState.status === STATUS.ERROR)
|
|
82
|
+
let showNoResults = $derived(loaderState.status === STATUS.COMPLETE && loaderState.isFirstLoad)
|
|
83
|
+
let showNoData = $derived(loaderState.status === STATUS.COMPLETE && !loaderState.isFirstLoad)
|
|
84
|
+
let showCoolingOff = $derived(loaderState.status !== STATUS.COMPLETE && loopTracker.coolingOff)
|
|
85
|
+
|
|
86
|
+
async function attemptLoad() {
|
|
87
|
+
// If we're complete, don't attempt to load again
|
|
88
|
+
// If we're not ready (i.e. in the middle of a fetch) don't attempt to load again
|
|
89
|
+
// However, if we're in an error state, allow the user to retry via btn click
|
|
90
|
+
if (
|
|
91
|
+
loaderState.status === STATUS.COMPLETE ||
|
|
92
|
+
(loaderState.status !== STATUS.READY && loaderState.status !== STATUS.ERROR)
|
|
93
|
+
) {
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
loaderState.status = STATUS.LOADING
|
|
98
|
+
|
|
99
|
+
// Skip loading if we're in infinite loop cool-off
|
|
100
|
+
if (!loopTracker.coolingOff) {
|
|
101
|
+
await triggerLoad()
|
|
102
|
+
loopTracker.track()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// @ts-expect-error - client can set status to 'COMPLETE' inside the
|
|
106
|
+
// `triggerLoad` fn above via `loaderState.complete()`, TS obviously doesn't know this.
|
|
107
|
+
if (loaderState.status !== STATUS.ERROR && loaderState.status !== STATUS.COMPLETE) {
|
|
108
|
+
if (loaderState.status === STATUS.LOADING) {
|
|
109
|
+
loaderState.isFirstLoad = false
|
|
110
|
+
loaderState.status = STATUS.READY
|
|
111
|
+
}
|
|
64
112
|
}
|
|
65
113
|
}
|
|
66
|
-
|
|
67
|
-
onMount(() => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
114
|
+
|
|
115
|
+
onMount(() => {
|
|
116
|
+
if (observer || !intersectionTarget) return
|
|
117
|
+
|
|
118
|
+
const appliedIntersectionOptions = {
|
|
119
|
+
rootMargin: "0px 0px 200px 0px",
|
|
120
|
+
...intersectionOptions
|
|
121
|
+
}
|
|
122
|
+
observer = new IntersectionObserver(async (entries) => {
|
|
123
|
+
if (entries[0]?.isIntersecting && loaderState.mounted) {
|
|
124
|
+
await attemptLoad()
|
|
125
|
+
}
|
|
126
|
+
}, appliedIntersectionOptions)
|
|
127
|
+
observer.observe(intersectionTarget)
|
|
128
|
+
|
|
129
|
+
loaderState.mounted = true
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
onDestroy(() => {
|
|
133
|
+
if (loaderState.mounted) {
|
|
134
|
+
if (observer) {
|
|
135
|
+
observer.disconnect()
|
|
136
|
+
}
|
|
137
|
+
if (loopTracker) {
|
|
138
|
+
loopTracker.destroy()
|
|
139
|
+
}
|
|
77
140
|
}
|
|
78
|
-
}
|
|
79
|
-
observer.observe(intersectionTarget);
|
|
80
|
-
});
|
|
81
|
-
onDestroy(() => {
|
|
82
|
-
observer && observer.disconnect();
|
|
83
|
-
loopTracker && loopTracker.destroy();
|
|
84
|
-
});
|
|
141
|
+
})
|
|
85
142
|
</script>
|
|
86
143
|
|
|
87
144
|
<div class="infinite-loader-wrapper">
|
|
@@ -1,37 +1,16 @@
|
|
|
1
|
-
import { SvelteComponent } from "svelte";
|
|
2
1
|
import { type Snippet } from "svelte";
|
|
3
|
-
declare const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
_: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\"";
|
|
19
|
-
}) | undefined;
|
|
20
|
-
coolingOff?: ((this: void) => typeof import("svelte").SnippetReturn & {
|
|
21
|
-
_: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\"";
|
|
22
|
-
}) | undefined;
|
|
23
|
-
error?: ((this: void, args_0: () => Promise<void>) => typeof import("svelte").SnippetReturn & {
|
|
24
|
-
_: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\"";
|
|
25
|
-
}) | undefined;
|
|
26
|
-
};
|
|
27
|
-
events: {
|
|
28
|
-
[evt: string]: CustomEvent<any>;
|
|
29
|
-
};
|
|
30
|
-
slots: {};
|
|
31
|
-
};
|
|
32
|
-
type InfiniteLoaderProps_ = typeof __propDef.props;
|
|
33
|
-
export { InfiniteLoaderProps_ as InfiniteLoaderProps };
|
|
34
|
-
export type InfiniteLoaderEvents = typeof __propDef.events;
|
|
35
|
-
export type InfiniteLoaderSlots = typeof __propDef.slots;
|
|
36
|
-
export default class InfiniteLoader extends SvelteComponent<InfiniteLoaderProps_, InfiniteLoaderEvents, InfiniteLoaderSlots> {
|
|
37
|
-
}
|
|
2
|
+
declare const InfiniteLoader: import("svelte").Component<{
|
|
3
|
+
triggerLoad: () => Promise<void>;
|
|
4
|
+
loopTimeout?: number;
|
|
5
|
+
loopDetectionTimeout?: number;
|
|
6
|
+
loopMaxCalls?: number;
|
|
7
|
+
intersectionOptions?: Partial<IntersectionObserver>;
|
|
8
|
+
children: Snippet;
|
|
9
|
+
loading?: Snippet;
|
|
10
|
+
noResults?: Snippet;
|
|
11
|
+
noData?: Snippet;
|
|
12
|
+
coolingOff?: Snippet;
|
|
13
|
+
error?: Snippet<[() => Promise<void>]>;
|
|
14
|
+
}, {}, "">;
|
|
15
|
+
type InfiniteLoader = ReturnType<typeof InfiniteLoader>;
|
|
16
|
+
export default InfiniteLoader;
|
|
@@ -7,12 +7,15 @@ export const STATUS = {
|
|
|
7
7
|
class LoaderState {
|
|
8
8
|
isFirstLoad = $state(true);
|
|
9
9
|
status = $state(STATUS.READY);
|
|
10
|
+
mounted = $state(false);
|
|
10
11
|
loaded = () => {
|
|
11
|
-
this.isFirstLoad
|
|
12
|
+
if (this.isFirstLoad)
|
|
13
|
+
this.isFirstLoad = false;
|
|
12
14
|
this.status = STATUS.READY;
|
|
13
15
|
};
|
|
14
16
|
complete = () => {
|
|
15
|
-
this.isFirstLoad
|
|
17
|
+
if (this.isFirstLoad)
|
|
18
|
+
this.isFirstLoad = false;
|
|
16
19
|
this.status = STATUS.COMPLETE;
|
|
17
20
|
};
|
|
18
21
|
reset = () => {
|
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.
|
|
9
|
+
"version": "0.4.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"homepage": "https://svelte-5-infinite.vercel.app",
|
|
12
12
|
"keywords": [
|
|
@@ -37,29 +37,33 @@
|
|
|
37
37
|
"!dist/**/*.spec.*"
|
|
38
38
|
],
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"svelte": "^5.0.0"
|
|
40
|
+
"svelte": "^5.0.0-0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@
|
|
44
|
-
"@
|
|
45
|
-
"@sveltejs/
|
|
46
|
-
"@sveltejs/
|
|
47
|
-
"@
|
|
48
|
-
"@
|
|
49
|
-
"@
|
|
50
|
-
"eslint": "^8.
|
|
43
|
+
"@eslint/eslintrc": "^3.1.0",
|
|
44
|
+
"@eslint/js": "^9.14.0",
|
|
45
|
+
"@sveltejs/adapter-auto": "^3.3.1",
|
|
46
|
+
"@sveltejs/kit": "^2.8.1",
|
|
47
|
+
"@sveltejs/package": "^2.3.7",
|
|
48
|
+
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
|
49
|
+
"@types/eslint": "^9.6.1",
|
|
50
|
+
"@typescript-eslint/eslint-plugin": "^8.14.0",
|
|
51
|
+
"@typescript-eslint/parser": "^8.14.0",
|
|
52
|
+
"eslint": "^9.14.0",
|
|
51
53
|
"eslint-config-prettier": "^9.1.0",
|
|
52
|
-
"eslint-plugin-svelte": "^2.
|
|
53
|
-
"
|
|
54
|
-
"prettier
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"svelte
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"typescript
|
|
61
|
-
"
|
|
62
|
-
"
|
|
54
|
+
"eslint-plugin-svelte": "^2.46.0",
|
|
55
|
+
"globals": "^15.12.0",
|
|
56
|
+
"prettier": "^3.3.3",
|
|
57
|
+
"prettier-plugin-svelte": "^3.2.8",
|
|
58
|
+
"publint": "^0.2.12",
|
|
59
|
+
"svelte": "^5.1.16",
|
|
60
|
+
"svelte-check": "^4.0.7",
|
|
61
|
+
"tslib": "^2.8.1",
|
|
62
|
+
"typescript": "^5.6.3",
|
|
63
|
+
"typescript-eslint": "^8.14.0",
|
|
64
|
+
"typescript-svelte-plugin": "^0.3.43",
|
|
65
|
+
"vite": "^5.4.11",
|
|
66
|
+
"vitest": "^2.1.5"
|
|
63
67
|
},
|
|
64
68
|
"scripts": {
|
|
65
69
|
"dev": "vite dev",
|