Skip to content

Commit 24c3c12

Browse files
committed
feat: add ability to cleanFalseValues from url if flag set to true in query-param-store
1 parent 0cec6d5 commit 24c3c12

File tree

4 files changed

+94
-13
lines changed

4 files changed

+94
-13
lines changed

.vscode/settings.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
"foam.edit.linkReferenceDefinitions": "withExtensions",
1212
"foam.files.ignore": [
1313
"**/node_modules/**/*",
14-
"**/package/**/*",
1514
"**/.svelte-kit/**/*",
1615
"**/dist/**/*"
1716
],

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"check": "svelte-check --tsconfig ./tsconfig.json --threshold error --diagnostic-sources js,svelte --compiler-warnings a11y-no-static-element-interactions:ignore",
5555
"check:watch": "svelte-check --tsconfig ./tsconfig.json --threshold warning --diagnostic-sources js,svelte --watch",
5656
"test": "vitest",
57+
"check-packages": "pnpm update --interactive --recursive --latest",
5758
"release": "bumpp"
5859
},
5960
"svelte": "./dist/index.js",

src/lib/stores/clean-object.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
export function cleanObject(obj, cleanFalseValues = false) {
2+
const isArray = Array.isArray(obj);
3+
const isObject = typeof obj === 'object' && obj !== null;
4+
5+
if (isArray) {
6+
const result = obj
7+
.filter(item => item !== null && item !== undefined && item !== '' && !(Array.isArray(item) && item.length === 0) && !(cleanFalseValues && item === false))
8+
.map(item => (typeof item === 'object' ? cleanObject(item, cleanFalseValues) : item));
9+
return result.length === 0 ? undefined : result;
10+
}
11+
12+
if (isObject) {
13+
const result = Object.entries(obj)
14+
.filter(([_, value]) => value !== null && value !== undefined && value !== '' && !(Array.isArray(value) && value.length === 0) && !(cleanFalseValues && value === false))
15+
.reduce((acc, [key, value]) => ({ ...acc, [key]: typeof value === 'object' ? cleanObject(value, cleanFalseValues) : value }), {});
16+
return Object.keys(result).length === 0 ? undefined : result;
17+
}
18+
19+
return obj;
20+
}
21+
22+
23+
if (import.meta.vitest) {
24+
describe(cleanObject, () => {
25+
test('should remove null, undefined, empty string, and empty array values from an object', () => {
26+
const obj = {
27+
a: 'a',
28+
b: null,
29+
c: undefined,
30+
d: '',
31+
e: [],
32+
f: {
33+
g: 'g',
34+
h: null,
35+
i: undefined,
36+
j: '',
37+
k: [],
38+
},
39+
};
40+
const expected = {
41+
a: 'a',
42+
f: {
43+
g: 'g',
44+
},
45+
};
46+
expect(cleanObject(obj)).toEqual(expected);
47+
});
48+
49+
test('should not remove false values from an object when flag not set', () => {
50+
const obj = {
51+
b: false,
52+
};
53+
expect(cleanObject(obj)).toEqual(obj);
54+
});
55+
56+
test('should remove false values from an object when flag set', () => {
57+
const obj = { b: false };
58+
expect(cleanObject(obj, true)).toBeUndefined();
59+
});
60+
61+
test('should remove null, undefined, empty string, and empty array values from an array', () => {
62+
const arr = ['a', null, undefined, '', [], { g: 'g', h: null, i: undefined, j: '', k: [] }];
63+
const expected = ['a', { g: 'g' }];
64+
expect(cleanObject(arr)).toEqual(expected);
65+
});
66+
67+
test('should return undefined for null, undefined, empty string, and empty array inputs', () => {
68+
expect(cleanObject(undefined)).toBeUndefined();
69+
expect(cleanObject([])).toBeUndefined();
70+
expect(cleanObject({a: null})).toBeUndefined();
71+
expect(cleanObject({})).toBeUndefined();
72+
});
73+
});
74+
}

src/lib/stores/query-param-store.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { writable, type Writable } from 'svelte/store';
22
import { goto } from '$app/navigation';
33
import { page } from '$app/stores';
4+
import { cleanObject } from './clean-object';
45

56
export interface QueryParamStore<T> extends Writable<T> {
67
remove: () => void;
@@ -12,26 +13,29 @@ export interface QueryParamStoreOptions<T> {
1213
replaceState?: boolean;
1314
persist?: 'localStorage' | 'sessionStorage';
1415
storagePrefix?: string;
16+
cleanFalseValues?: boolean;
1517
log?: boolean;
1618
}
1719

18-
const stringify = (value) => {
19-
if (typeof value === 'undefined' || value === null) return undefined;
20+
function stringify(value, cleanFalseValues: boolean): string | undefined {
21+
if (typeof value === 'undefined' || value === null || value === '') return undefined;
2022
if (typeof value === 'string') return value;
21-
return JSON.stringify(value);
22-
};
2323

24-
const parse = (value: string) => {
24+
const cleanedValue = cleanObject(value, cleanFalseValues);
25+
return cleanedValue === undefined ? undefined : JSON.stringify(cleanedValue);
26+
}
27+
28+
function parse(value: string) {
2529
if (typeof value === 'undefined') return undefined;
2630
try {
2731
return JSON.parse(value);
2832
} catch {
2933
return value; // if the original input was just a string (and never JSON stringified), it will throw an error so just return the string
3034
}
31-
};
35+
}
3236

3337
export function createQueryParamStore<T>(opts: QueryParamStoreOptions<T>) {
34-
const { key, log, persist } = opts;
38+
const { key, log, persist, startWith, cleanFalseValues } = opts;
3539
const replaceState = typeof opts.replaceState === 'undefined' ? true : opts.replaceState;
3640
const storageKey = `${opts.storagePrefix || ''}${key}`
3741

@@ -45,10 +49,11 @@ export function createQueryParamStore<T>(opts: QueryParamStoreOptions<T>) {
4549

4650
const setQueryParam = (value) => {
4751
if (typeof window === 'undefined') return; // safety check in case store value is assigned via $: call server side
48-
if (value === undefined || value === null) return removeQueryParam();
52+
const stringified_value = stringify(value, cleanFalseValues);
53+
if (stringified_value === undefined) return removeQueryParam();
4954
const {hash} = window.location
5055
const searchParams = new URLSearchParams(window.location.search)
51-
searchParams.set(key, stringify(value));
56+
searchParams.set(key, stringify(value, cleanFalseValues));
5257
goto(`?${searchParams}${hash}`, { keepFocus: true, noScroll: true, replaceState });
5358
if (log) console.info(`user action changed: ${key} to ${value}`);
5459
};
@@ -69,9 +74,11 @@ export function createQueryParamStore<T>(opts: QueryParamStoreOptions<T>) {
6974
};
7075

7176
const setStoreValue = (value: string) => {
72-
const parsed_value = parse(value) as T;
77+
if (log) console.info(`URL set ${key} to ${value}`);
78+
let parsed_value = parse(value) as T;
79+
if (!parsed_value && typeof startWith === 'object')
80+
parsed_value = {} as T;
7381
set(parsed_value);
74-
if (log) console.info(`URL set ${key} to ${parsed_value}`);
7582
storage?.setItem(storageKey, JSON.stringify(parsed_value));
7683
if (log && storage) console.info({[storageKey + '_to_cache']: parsed_value});
7784
};
@@ -104,7 +111,7 @@ export function createQueryParamStore<T>(opts: QueryParamStoreOptions<T>) {
104111
};
105112

106113
// 3rd Priority: use startWith if no query param in url nor storage value found
107-
const store = writable<T>(opts.startWith, start);
114+
const store = writable<T>(startWith, start);
108115
const { subscribe, set } = store;
109116

110117
return {

0 commit comments

Comments
 (0)