Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 21 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ The simplest and most effective way to use this library is by importing the meth
Your username is {$username}
```

the function returns a store so make sure to use it with the `$` prepended to handle auto-subscriprion. In case there's not a query parameter with the chosen name it will simply be null.
the function returns a store so make sure to use it with the `$` prepended to handle auto-subscription. In case there's not a query parameter with the chosen name it will simply be null.

### Writing to the store (single parameter)

Reading query parameters is cool but you know what is even cooler? Writing query parameters! With this library you can treat your store just like normal state in svelte. To update the state and conseguentely the url you can just do this
Reading query parameters is cool but you know what is even cooler? Writing query parameters! With this library you can treat your store just like normal state in svelte. To update the state and consequently the url you can just do this

```svelte
<script lang="ts">
Expand Down Expand Up @@ -107,7 +107,7 @@ The count is {$count}
<input bind:value={$count} type="number" />
```

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.
this time $count would be of type number and the decoding function it's what's used to update the url when you write to the store.

### Default values

Expand Down Expand Up @@ -322,7 +322,7 @@ There are six helpers all exported as functions on the object ssp. To each one o

#### object

To map from a query parameter to an object. An url like this `/?obj={"isComplex":%20true,%20"nested":%20{"field":%20"value"}}` will be mapped to
To map from a query parameter to an object. A url like this `/?obj={"isComplex":%20true,%20"nested":%20{"field":%20"value"}}` will be mapped to

```typescript
$store.obj.isComplex; //true
Expand All @@ -332,7 +332,7 @@ $store.obj.nested.value; // "value"

#### array

To map from a query parameter to an array. An url like this `/?arr=[1,2,3,4]` will be mapped to
To map from a query parameter to an array. A url like this `/?arr=[1,2,3,4]` will be mapped to

```typescript
$store.arr[0]; //1
Expand All @@ -343,27 +343,27 @@ $store.arr[3]; //4

#### number

To map from a query parameter to a number. An url like this `/?num=1` will be mapped to
To map from a query parameter to a number. A url like this `/?num=1` will be mapped to

```typescript
$store.num; //1
```

#### boolean

To map from a query parameter to a boolean. An url like this `/?bool=true` will be mapped to
To map from a query parameter to a boolean. A url like this `/?bool=true` will be mapped to

```typescript
$store.bool; //true
```

as we've seen an url like this `/?bool=false` will be mapped to
as we've seen a url like this `/?bool=false` will be mapped to

```typescript
$store.bool; //false
```

just like an url like this `/`
just like a url like this `/`

#### string

Expand All @@ -373,12 +373,22 @@ This is exported mainly for readability since all query parameters are already s

To map any JSON serializable state to his lz-string representation. This is a common way to store state in query parameters that will prevent the link to directly show the state.

An url like this `/?state=N4IgbghgNgrgpiAXCAsgTwAQGMD2OoYCO8ATpgA4QkQC2cALnCSAL5A` will map to
A url like this `/?state=N4IgbghgNgrgpiAXCAsgTwAQGMD2OoYCO8ATpgA4QkQC2cALnCSAL5A` will map to

```typescript
$store.state.value; //My cool query parameter
```

#### base64

To store more complicated strings, such as those containing unicode characters, newlines, or other special characters, you can use the base64 helper. The helper follows the "Base64 URL safe" pattern described in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Glossary/Base64).

A url like this `/?state=YSDEgCDwkICAIOaWhyDwn6aE` will map to

```typescript
$store.state.value; //a Ā 𐀀 文 🦄
```

## Store options

Both functions accept a configuration object that contains the following properties:
Expand Down Expand Up @@ -468,7 +478,7 @@ To set the configuration object you can pass it as a third parameter in case of
</script>
```

## Vite dependecies error
## Vite dependencies error

If you ran into issues with vite you need to update your `vite.config.ts` or `vite.config.js` file to include the plugin exported from `sveltekit-search-params/plugin`. It's as simple as

Expand Down
11 changes: 11 additions & 0 deletions playground/src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
sort: false,
});
const lz = queryParam('lz', ssp.lz<string>());
const base64 = queryParam('base64', ssp.base64());

let obj_changes = 0;
let arr_changes = 0;
let lz_changes = 0;
let base64_changes = 0;

$: {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
Expand All @@ -31,6 +33,11 @@
$lz;
lz_changes++;
}
$: {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
$base64;
base64_changes++;
}
</script>

<input data-testid="str-input" bind:value={$str} />
Expand Down Expand Up @@ -87,6 +94,9 @@
<input data-testid="lz-input" bind:value={$lz} />
<div data-testid="lz">{$lz}</div>

<input data-testid="base64-input" bind:value={$base64} />
<div data-testid="base64">{$base64}</div>

<button
data-testid="change-two"
on:click={() => {
Expand All @@ -98,3 +108,4 @@
<p data-testid="how-many-obj-changes">{obj_changes}</p>
<p data-testid="how-many-arr-changes">{arr_changes}</p>
<p data-testid="how-many-lz-changes">{lz_changes}</p>
<p data-testid="how-many-base64-changes">{base64_changes}</p>
4 changes: 4 additions & 0 deletions playground/src/routes/queryparameters/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
obj: ssp.object<{ str: string }>(),
arr: ssp.array<number>(),
lz: ssp.lz<string>(),
base64: ssp.base64(),
});

const unordered_store = queryParameters(
Expand Down Expand Up @@ -84,6 +85,9 @@
<input data-testid="lz-input" bind:value={$store.lz} />
<div data-testid="lz">{$store.lz}</div>

<input data-testid="base64-input" bind:value={$store.base64} />
<div data-testid="base64">{$store.base64}</div>

<button
data-testid="change-two"
on:click={() => {
Expand Down
37 changes: 37 additions & 0 deletions src/lib/ssp/base64.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { EncodeAndDecodeOptions } from '$lib/types';

/**
* Encodes and decodes a Unicode string as utf-8 Base64 URL safe.
*
* See: https://developer.mozilla.org/en-US/docs/Glossary/Base64
*/
function sspBase64(
defaultValue: string,
): EncodeAndDecodeOptions<string> & { defaultValue: string };
function sspBase64(): EncodeAndDecodeOptions<string> & {
defaultValue: undefined;
};
function sspBase64(defaultValue?: string): EncodeAndDecodeOptions<string> {
return {
encode: (value: string) => {
if (value === '') return undefined;
const bytes = new TextEncoder().encode(value);
const binString = Array.from(bytes, (byte) =>
String.fromCodePoint(byte),
).join('');
return btoa(binString)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
},
Comment on lines +16 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider optimizing the encode method for readability and performance.

The encode method correctly handles UTF-8 encoding and produces a URL-safe Base64 string. However, it could be optimized for better readability and potentially improved performance.

Consider the following optimizations:

  1. Use encodeURIComponent to handle UTF-8 encoding, which is simpler and potentially faster than using TextEncoder.
  2. Use btoa directly on the encoded string, eliminating the need for the intermediate binary string conversion.
  3. Use a single replace call with a regular expression for URL-safe character replacements.

Here's a suggested implementation:

encode: (value: string) => {
  if (value === '') return undefined;
  return btoa(encodeURIComponent(value))
    .replace(/[+/=]/g, (m) => ({ '+': '-', '/': '_', '=': '' }[m] || m));
},

This version is more concise and potentially more efficient, while maintaining the same functionality.

decode: (value: string | null) => {
if (value === null) return '';
const binString = atob(value.replace(/-/g, '+').replace(/_/g, '/'));
const bytes = Uint8Array.from(binString, (m) => m.codePointAt(0)!);
return new TextDecoder().decode(bytes);
},
defaultValue,
};
Comment on lines +27 to +34
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider optimizing the decode method for readability and performance.

The decode method correctly reverses the encoding process, including handling of null input and URL-safe character replacements. However, similar to the encode method, it could be optimized for better readability and potentially improved performance.

Consider the following optimizations:

  1. Use decodeURIComponent to handle UTF-8 decoding, which is simpler and potentially faster than using TextDecoder.
  2. Combine the URL-safe character replacements and Base64 decoding into a single step.

Here's a suggested implementation:

decode: (value: string | null) => {
  if (value === null) return '';
  return decodeURIComponent(
    atob(value.replace(/-/g, '+').replace(/_/g, '/'))
      .split('')
      .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
      .join('')
  );
},

This version is more concise and potentially more efficient, while maintaining the same functionality. It also handles potential decoding errors more gracefully by using decodeURIComponent.

}

export { sspBase64 };
2 changes: 2 additions & 0 deletions src/lib/ssp/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { sspBase64 } from './base64';
import {
arrayEncodeAndDecodeOptions,
objectEncodeAndDecodeOptions,
Expand All @@ -21,4 +22,5 @@ export default {
object: objectEncodeAndDecodeOptions,
array: arrayEncodeAndDecodeOptions,
lz: lzEncodeAndDecodeOptions,
base64: sspBase64,
} as const;
30 changes: 30 additions & 0 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,26 @@ test.describe('queryParam', () => {
await expect(lz_changes).toHaveText('2');
});

test('works as expected with base64', async ({ page }) => {
await page.goto('/');
const input = page.getByTestId('base64-input');
await input.fill('a Ā 𐀀 文 🦄');
const str = page.getByTestId('base64');
await expect(str).toHaveText('a Ā 𐀀 文 🦄');
const url = new URL(page.url());
expect(url.searchParams.get('base64')).toBe('YSDEgCDwkICAIOaWhyDwn6aE');
});

test("changing a base64 doesn't trigger reactivity multiple times", async ({
page,
}) => {
await page.goto('/');
const input = page.getByTestId('base64-input');
await input.fill('a');
const base64_changes = page.getByTestId('how-many-base64-changes');
await expect(base64_changes).toHaveText('2');
});

test("changing one parameter doesn't interfere with the rest", async ({
page,
}) => {
Expand Down Expand Up @@ -262,6 +282,16 @@ test.describe('queryParameters', () => {
expect(url.searchParams.get('lz')).toBe('EQGwXsQ');
});

test('works as expected with base64', async ({ page }) => {
await page.goto('/queryparameters');
const input = page.getByTestId('base64-input');
await input.fill('a Ā 𐀀 文 🦄');
const str = page.getByTestId('base64');
await expect(str).toHaveText('a Ā 𐀀 文 🦄');
const url = new URL(page.url());
expect(url.searchParams.get('base64')).toBe('YSDEgCDwkICAIOaWhyDwn6aE');
});

test("changes to the store doesn't trigger reactivity multiple times", async ({
page,
}) => {
Expand Down
Loading