diff --git a/.gitignore b/.gitignore index 17b1dc0..e08aae2 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ node_modules # Local Netlify folder .netlify +test-results \ No newline at end of file diff --git a/README.md b/README.md index d40bfb6..91c7453 100644 --- a/README.md +++ b/README.md @@ -46,9 +46,9 @@ The simplest and most effective way to use this library is by importing the meth ```svelte Your username is {$username} @@ -62,9 +62,9 @@ Reading query parameters is cool but you know what is even cooler? Writing query ```svelte Your username is {$username} @@ -75,16 +75,18 @@ or if you prefer ```svelte - Your username is {$username} -{ - $username = e.target.value; -}} /> + { + $username = e.target.value; + }} +/> ``` ### Encoding and decoding @@ -93,16 +95,16 @@ By default query parameters are strings but more often than not tho we are not w ```svelte The count is {$count} - + ``` this time $count would be of type number and the deconding function it's what's used to update the url when you write to the store. @@ -113,17 +115,17 @@ Sometimes when we want to create a new variable we like to pass a default value. ```svelte The count is {$count} - + ``` this will make the query parameter change as soon as the page is rendered on the browser (the query parameter will change only if it's not already present and only the first time the application render). @@ -138,13 +140,13 @@ Write an encode and decode function may seem trivial but it's tedious for sure. ```svelte The count is {$count} - + ``` this code will produce the same output as the code written above but far more readable and easier to read. You can find all the exports documented in the section [ssp - Helpers](#ssp---helpers). @@ -153,13 +155,13 @@ You can also pass a default value to the function that will be the defaultValue ```svelte The count is {$count} - + ``` ### Simple case (all parameters) @@ -168,9 +170,9 @@ You can use the function queryParameters to get an object containing all the pre ```svelte
@@ -195,17 +197,20 @@ Just like with the single parameter case you can just update the store and the U
 
 ```svelte
 
 
 
     {JSON.stringify($store, null, 2)}
 
-{ - $store.username = e.target.value; -}} /> + { + $store.username = e.target.value; + }} +/> ``` writing in the input will update the state and the URL at the same time. @@ -216,11 +221,11 @@ Most of the times if you need to read from query parameters you are expecting so ```svelte
@@ -254,16 +259,17 @@ The parameter passed to `queryParameters` can aslo be used to specify the encodi
 
 ```svelte
 
 
 
@@ -297,12 +303,12 @@ Obviously also in this case you can use the helpers functions provided inside `s
 
 ```svelte
 
 
 
@@ -385,25 +391,32 @@ The number of milliseconds to delay the writing of the history when the state ch
 
 A boolean defining if the history have to be written at all. If set to false no new history entries will be written to the history stack (the URL will still update but the user will not be able to go back with the browser).
 
+### sort
+
+Whenever you interacts with a store it navigates for you. By default the search params are sorted to allow for better cache-ability. You can disable this behavior by passing `false` to this option. Keep in mind that this is a per-store settings. This mean that if you interact with a store that has this option set to `false` and than interact with one that has this option set to `true` (the default) the resulting URL will still have the search params sorted.
+
 ### How to use it
 
 To set the configuration object you can pass it as a third parameter in case of `queryParam` or the second in case of `queryParameters`.
 
 ```svelte
 
 ```
 
diff --git a/package.json b/package.json
index 7d78a6f..3132f51 100644
--- a/package.json
+++ b/package.json
@@ -34,8 +34,10 @@
 		"format": "prettier --write .",
 		"publish": "pnpm run build && changeset publish",
 		"test": "npm run test:integration && npm run test:unit",
+		"test:integration:ui": "playwright test --ui",
 		"test:integration": "playwright test",
-		"test:unit": "vitest"
+		"test:unit": "vitest",
+		"changeset": "changeset"
 	},
 	"devDependencies": {
 		"@changesets/cli": "^2.26.0",
diff --git a/playground/src/routes/+page.svelte b/playground/src/routes/+page.svelte
index e4b02e2..d6b558d 100644
--- a/playground/src/routes/+page.svelte
+++ b/playground/src/routes/+page.svelte
@@ -6,6 +6,9 @@
 	const bools = queryParam('bools', ssp.boolean());
 	const obj = queryParam('obj', ssp.object<{ str: string }>());
 	const arr = queryParam('arr', ssp.array());
+	const arr_unordered = queryParam('arr-unordered', ssp.array(), {
+		sort: false,
+	});
 	const lz = queryParam('lz', ssp.lz());
 
 
@@ -44,6 +47,22 @@
 	{/each}
 
 
+
+
    + {#each $arr_unordered ?? [] as num} +
  • {num}
  • + {/each} +
+
{$lz}
diff --git a/playground/src/routes/queryparameters/+page.svelte b/playground/src/routes/queryparameters/+page.svelte index cd4bd88..ede425e 100644 --- a/playground/src/routes/queryparameters/+page.svelte +++ b/playground/src/routes/queryparameters/+page.svelte @@ -9,6 +9,15 @@ arr: ssp.array(), lz: ssp.lz(), }); + + const unordered_store = queryParameters( + { + 'arr-unordered': ssp.array(), + }, + { + sort: false, + }, + ); @@ -46,6 +55,24 @@ {/each} + +
    + {#each $unordered_store['arr-unordered'] ?? [] as num} +
  • {num}
  • + {/each} +
+
{$store.lz}
diff --git a/src/lib/sveltekit-search-params.ts b/src/lib/sveltekit-search-params.ts index d72d16c..d0b2825 100644 --- a/src/lib/sveltekit-search-params.ts +++ b/src/lib/sveltekit-search-params.ts @@ -30,6 +30,7 @@ export type EncodeAndDecodeOptions = { export type StoreOptions = { debounceHistory?: number; pushHistory?: boolean; + sort?: boolean; }; type LooseAutocomplete = { @@ -156,7 +157,7 @@ const debouncedTimeouts = new Map(); export function queryParameters( options?: Options, - { debounceHistory = 0, pushHistory = true }: StoreOptions = {}, + { debounceHistory = 0, pushHistory = true, sort = true }: StoreOptions = {}, ): Writable> { function set(value: T) { if (!browser) return; @@ -192,11 +193,19 @@ export function queryParameters( batched(query); }); clearTimeout(debouncedTimeouts.get('queryParameters')); - if (browser) await goto(`?${query}${hash}`, GOTO_OPTIONS); + if (browser) { + if (sort) { + query.sort(); + } + await goto(`?${query}${hash}`, GOTO_OPTIONS); + } if (pushHistory && browser) { debouncedTimeouts.set( 'queryParameters', setTimeout(() => { + if (sort) { + query.sort(); + } goto(hash, GOTO_OPTIONS_PUSH); }, debounceHistory), ); @@ -237,7 +246,7 @@ export function queryParam( decode: decode = DEFAULT_ENCODER_DECODER.decode, defaultValue, }: EncodeAndDecodeOptions = DEFAULT_ENCODER_DECODER, - { debounceHistory = 0, pushHistory = true }: StoreOptions = {}, + { debounceHistory = 0, pushHistory = true, sort = true }: StoreOptions = {}, ): Writable { function set(value: T | null) { if (!browser) return; @@ -262,11 +271,19 @@ export function queryParam( batched(query); }); clearTimeout(debouncedTimeouts.get(name)); - if (browser) await goto(`?${query}${hash}`, GOTO_OPTIONS); + if (browser) { + if (sort) { + query.sort(); + } + await goto(`?${query}${hash}`, GOTO_OPTIONS); + } if (pushHistory && browser) { debouncedTimeouts.set( name, setTimeout(() => { + if (sort) { + query.sort(); + } goto(hash, GOTO_OPTIONS_PUSH); }, debounceHistory), ); diff --git a/tests/index.test.ts b/tests/index.test.ts index f1c3c96..48922e8 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -116,6 +116,41 @@ test.describe('queryParam', () => { expect(url.searchParams.get('str')).toBe('one'); expect(url.searchParams.get('num')).toBe('42'); }); + + test('parameters are kept in alphabetical order by default', async ({ + page, + }) => { + await page.goto('/?num=0'); + const arr_btn = page.getByTestId('arr-input'); + const btn = page.getByTestId('num'); + await btn.click(); + await arr_btn.click(); + const url = new URL(page.url()); + expect(url.search).toBe('?arr=%5B0%5D&num=1'); + }); + + test('parameters are not ordered if updated through a store that has specifically set sort to false', async ({ + page, + }) => { + await page.goto('/'); + const input = page.getByTestId('str-input'); + await input.fill('str'); + const btn = page.getByTestId('arr-unordered-input'); + await btn.click(); + const arr = page.getByTestId('arr-unordered'); + expect(await arr.count()).toBe(1); + let url = new URL(page.url()); + expect(url.searchParams.get('arr-unordered')).toBe('[0]'); + expect(url.searchParams.get('str')).toBe('str'); + expect(url.search).toBe('?str=str&arr-unordered=%5B0%5D'); + + // expect them to be ordered if you access an ordered store + await input.fill('string'); + url = new URL(page.url()); + expect(url.searchParams.get('arr-unordered')).toBe('[0]'); + expect(url.searchParams.get('str')).toBe('string'); + expect(url.search).toBe('?arr-unordered=%5B0%5D&str=string'); + }); }); test.describe('queryParameters', () => { @@ -233,4 +268,41 @@ test.describe('queryParameters', () => { expect(url.searchParams.get('str')).toBe('one'); expect(url.searchParams.get('num')).toBe('42'); }); + + test('parameters are kept in alphabetical order by default', async ({ + page, + }) => { + await page.goto('/queryparameters?num=0'); + const arr_btn = page.getByTestId('arr-input'); + const btn = page.getByTestId('num'); + await btn.click(); + await arr_btn.click(); + const url = new URL(page.url()); + expect(url.search).toBe('?arr=%5B0%5D&bools=false&num=1'); + }); + + test('parameters are not ordered if updated through a store that has specifically set sort to false', async ({ + page, + }) => { + await page.goto('/queryparameters'); + const input = page.getByTestId('str-input'); + await input.fill('str'); + const btn = page.getByTestId('arr-unordered-input'); + await btn.click(); + const arr = page.getByTestId('arr-unordered'); + expect(await arr.count()).toBe(1); + let url = new URL(page.url()); + expect(url.searchParams.get('arr-unordered')).toBe('[0]'); + expect(url.searchParams.get('str')).toBe('str'); + expect(url.search).toBe('?bools=false&str=str&arr-unordered=%5B0%5D'); + + // expect them to be ordered if you access an ordered store + await input.fill('string'); + url = new URL(page.url()); + expect(url.searchParams.get('arr-unordered')).toBe('[0]'); + expect(url.searchParams.get('str')).toBe('string'); + expect(url.search).toBe( + '?arr-unordered=%5B0%5D&bools=false&str=string', + ); + }); });