Skip to content

Commit

Permalink
feat: add ability to cleanFalseValues from url if flag set to true in…
Browse files Browse the repository at this point in the history
… query-param-store
  • Loading branch information
jacob-8 committed Mar 19, 2024
1 parent 0cec6d5 commit 24c3c12
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 13 deletions.
1 change: 0 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"foam.edit.linkReferenceDefinitions": "withExtensions",
"foam.files.ignore": [
"**/node_modules/**/*",
"**/package/**/*",
"**/.svelte-kit/**/*",
"**/dist/**/*"
],
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"check": "svelte-check --tsconfig ./tsconfig.json --threshold error --diagnostic-sources js,svelte --compiler-warnings a11y-no-static-element-interactions:ignore",
"check:watch": "svelte-check --tsconfig ./tsconfig.json --threshold warning --diagnostic-sources js,svelte --watch",
"test": "vitest",
"check-packages": "pnpm update --interactive --recursive --latest",
"release": "bumpp"
},
"svelte": "./dist/index.js",
Expand Down
74 changes: 74 additions & 0 deletions src/lib/stores/clean-object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
export function cleanObject(obj, cleanFalseValues = false) {
const isArray = Array.isArray(obj);
const isObject = typeof obj === 'object' && obj !== null;

if (isArray) {
const result = obj
.filter(item => item !== null && item !== undefined && item !== '' && !(Array.isArray(item) && item.length === 0) && !(cleanFalseValues && item === false))
.map(item => (typeof item === 'object' ? cleanObject(item, cleanFalseValues) : item));
return result.length === 0 ? undefined : result;
}

if (isObject) {
const result = Object.entries(obj)
.filter(([_, value]) => value !== null && value !== undefined && value !== '' && !(Array.isArray(value) && value.length === 0) && !(cleanFalseValues && value === false))
.reduce((acc, [key, value]) => ({ ...acc, [key]: typeof value === 'object' ? cleanObject(value, cleanFalseValues) : value }), {});
return Object.keys(result).length === 0 ? undefined : result;
}

return obj;
}


if (import.meta.vitest) {
describe(cleanObject, () => {
test('should remove null, undefined, empty string, and empty array values from an object', () => {
const obj = {
a: 'a',
b: null,
c: undefined,
d: '',
e: [],
f: {
g: 'g',
h: null,
i: undefined,
j: '',
k: [],
},
};
const expected = {
a: 'a',
f: {
g: 'g',
},
};
expect(cleanObject(obj)).toEqual(expected);
});

test('should not remove false values from an object when flag not set', () => {
const obj = {
b: false,
};
expect(cleanObject(obj)).toEqual(obj);
});

test('should remove false values from an object when flag set', () => {
const obj = { b: false };
expect(cleanObject(obj, true)).toBeUndefined();
});

test('should remove null, undefined, empty string, and empty array values from an array', () => {
const arr = ['a', null, undefined, '', [], { g: 'g', h: null, i: undefined, j: '', k: [] }];
const expected = ['a', { g: 'g' }];
expect(cleanObject(arr)).toEqual(expected);
});

test('should return undefined for null, undefined, empty string, and empty array inputs', () => {
expect(cleanObject(undefined)).toBeUndefined();
expect(cleanObject([])).toBeUndefined();
expect(cleanObject({a: null})).toBeUndefined();
expect(cleanObject({})).toBeUndefined();
});
});
}
31 changes: 19 additions & 12 deletions src/lib/stores/query-param-store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { writable, type Writable } from 'svelte/store';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { cleanObject } from './clean-object';

export interface QueryParamStore<T> extends Writable<T> {
remove: () => void;
Expand All @@ -12,26 +13,29 @@ export interface QueryParamStoreOptions<T> {
replaceState?: boolean;
persist?: 'localStorage' | 'sessionStorage';
storagePrefix?: string;
cleanFalseValues?: boolean;
log?: boolean;
}

const stringify = (value) => {
if (typeof value === 'undefined' || value === null) return undefined;
function stringify(value, cleanFalseValues: boolean): string | undefined {
if (typeof value === 'undefined' || value === null || value === '') return undefined;
if (typeof value === 'string') return value;
return JSON.stringify(value);
};

const parse = (value: string) => {
const cleanedValue = cleanObject(value, cleanFalseValues);
return cleanedValue === undefined ? undefined : JSON.stringify(cleanedValue);
}

function parse(value: string) {
if (typeof value === 'undefined') return undefined;
try {
return JSON.parse(value);
} catch {
return value; // if the original input was just a string (and never JSON stringified), it will throw an error so just return the string
}
};
}

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

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

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

const setStoreValue = (value: string) => {
const parsed_value = parse(value) as T;
if (log) console.info(`URL set ${key} to ${value}`);
let parsed_value = parse(value) as T;
if (!parsed_value && typeof startWith === 'object')
parsed_value = {} as T;
set(parsed_value);
if (log) console.info(`URL set ${key} to ${parsed_value}`);
storage?.setItem(storageKey, JSON.stringify(parsed_value));
if (log && storage) console.info({[storageKey + '_to_cache']: parsed_value});
};
Expand Down Expand Up @@ -104,7 +111,7 @@ export function createQueryParamStore<T>(opts: QueryParamStoreOptions<T>) {
};

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

return {
Expand Down

0 comments on commit 24c3c12

Please sign in to comment.