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 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>import { onMount, onDestroy } from "svelte";
2
- import { STATUS, loaderState } from "./loaderState.svelte";
3
- const {
4
- triggerLoad,
5
- loopTimeout = 3e3,
6
- loopDetectionTimeout = 2e3,
7
- loopMaxCalls = 5,
8
- intersectionOptions = {},
9
- children,
10
- loading: loadingSnippet,
11
- noResults: noResultsSnippet,
12
- noData: noDataSnippet,
13
- coolingOff: coolingOffSnippet,
14
- error: errorSnippet
15
- } = $props();
16
- const ERROR_INFINITE_LOOP = `Attempted to execute load function ${loopMaxCalls} or more times within a short period. Please wait before trying again..`;
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
- loaderState.status = STATUS.LOADING;
56
- if (!loopTracker.coolingOff) {
57
- await triggerLoad();
58
- loopTracker.track();
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
- if (loaderState.status !== STATUS.ERROR && loaderState.status !== STATUS.COMPLETE) {
61
- if (loaderState.status === STATUS.LOADING) {
62
- loaderState.status = STATUS.READY;
63
- loaderState.isFirstLoad = false;
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
- if (observer || !intersectionTarget)
69
- return;
70
- const appliedIntersectionOptions = {
71
- rootMargin: "0px 0px 200px 0px",
72
- ...intersectionOptions
73
- };
74
- observer = new IntersectionObserver((entries) => {
75
- if (entries[0]?.isIntersecting) {
76
- attemptLoad();
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
- }, appliedIntersectionOptions);
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 __propDef: {
4
- props: {
5
- triggerLoad: () => Promise<void>;
6
- loopTimeout?: number | undefined;
7
- loopDetectionTimeout?: number | undefined;
8
- loopMaxCalls?: number | undefined;
9
- intersectionOptions?: IntersectionObserverInit | undefined;
10
- children: Snippet;
11
- loading?: ((this: void) => typeof import("svelte").SnippetReturn & {
12
- _: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\"";
13
- }) | undefined;
14
- noResults?: ((this: void) => typeof import("svelte").SnippetReturn & {
15
- _: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\"";
16
- }) | undefined;
17
- noData?: ((this: void) => typeof import("svelte").SnippetReturn & {
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,6 +7,7 @@ export declare const STATUS: {
7
7
  declare class LoaderState {
8
8
  isFirstLoad: boolean;
9
9
  status: "READY" | "LOADING" | "COMPLETE" | "ERROR";
10
+ mounted: boolean;
10
11
  loaded: () => void;
11
12
  complete: () => void;
12
13
  reset: () => void;
@@ -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 = false;
12
+ if (this.isFirstLoad)
13
+ this.isFirstLoad = false;
12
14
  this.status = STATUS.READY;
13
15
  };
14
16
  complete = () => {
15
- this.isFirstLoad = false;
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.3.1",
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
- "@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",
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.36.0-next.4",
53
- "prettier": "^3.2.5",
54
- "prettier-plugin-svelte": "^3.2.2",
55
- "publint": "^0.2.7",
56
- "svelte": "^5.0.0-beta.70",
57
- "svelte-check": "^3.6.6",
58
- "tslib": "^2.6.2",
59
- "typescript": "^5.3.3",
60
- "typescript-svelte-plugin": "^0.3.37",
61
- "vite": "^5.1.4",
62
- "vitest": "^1.3.1"
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",