Skip to content

Commit

Permalink
feat(useStorage): init (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
2nthony authored Sep 17, 2021
1 parent 30e0c7d commit c5aba7f
Show file tree
Hide file tree
Showing 8 changed files with 283 additions and 5 deletions.
5 changes: 4 additions & 1 deletion indexes.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"Browser",
"Component",
"Sensors",
"State",
"Utilities"
],
"functions": [
Expand Down Expand Up @@ -98,7 +99,9 @@
{
"name": "useStorage",
"package": "core",
"internal": true
"docs": "/core/useStorage/",
"category": "State",
"description": "reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)/[SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage)"
},
{
"name": "useTitle",
Expand Down
5 changes: 4 additions & 1 deletion packages/core/indexes.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"Browser",
"Component",
"Sensors",
"State",
"Utilities"
],
"functions": [
Expand Down Expand Up @@ -98,7 +99,9 @@
{
"name": "useStorage",
"package": "core",
"internal": true
"docs": "/core/useStorage/",
"category": "State",
"description": "reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)/[SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage)"
},
{
"name": "useTitle",
Expand Down
16 changes: 16 additions & 0 deletions packages/core/useStorage/demo.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script lang="ts">
import { dump } from 'js-yaml'
import { useStorage } from '.'
const state = useStorage('svelte-use-local-storage', {
name: 'Banana',
color: 'Yellow',
size: 'Medium',
})
</script>

<input bind:value={$state.name} type="text">
<input bind:value={$state.color} type="text">
<input bind:value={$state.size} type="text">

<pre lang="json">{ dump($state, { skipInvalid: true }) }</pre>
66 changes: 66 additions & 0 deletions packages/core/useStorage/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
category: State
---

# useStorage

Reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)/[SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage)

## Usage

```html
<script>
import { useStorage } from '@svelte-use/core'
// bind object
const state = useStorage('my-store', { hello: 'hi', greeting: 'Hello' }) // returns Writable<object>
// bind boolean
const flag = useStorage('my-flag', true) // returns Writable<boolean>
// bind number
const count = useStorage('my-count', 0) // returns Writable<number>
// bind string with SessionStorage
const id = useStorage('my-id', 'some-string-id', sessionStorage) // returns Writable<string>
// delete data from storage
$state = null
</script>
```

## Custom Serialization

By default, `useStorage` will smartly use the corresponding serializer based on the data type of provided default value. For example, `JSON.stringify` / `JSON.parse` will be used for objects, `Number.toString` / `parseFloat` for numbers, etc.

You can also provide your own serialization function to `useStorage`:

```html
<script>
import { useStorage } from '@svelte-use/core'
useStorage(
'key',
{},
{
serializer: {
read: (v: any) => v ? JSON.parse(v) : null,
write: (v: any) => JSON.stringify(v),
}
}
})
</script>
```

Please note when you provide `null` as the default value, `useStorage` can't assume the data type from it. In this case, you can provide a custom serializer or reuse the built-in ones explicitly.

```html
<script>
import { useStorage, StorageSerializers } from '@svelte-use/core'
const objectLike = useStorage('key', null, {
serializer: StorageSerializers.object,
})
$objectLike = { foo: 'bar' }
</script>
```
181 changes: 180 additions & 1 deletion packages/core/useStorage/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,180 @@
export function useStorage() {}
import {
MaybeWritable,
RemovableWritable,
toWritable,
unstore,
} from '@svelte-use/shared'
import { useEventListener } from '../useEventListener'
import { ConfigurableWindow, defaultWindow } from '../_configurable'

export type Serializer<T> = {
read(raw: string): T
write(value: T): string
}

export const StorageSerializers: Record<
'boolean' | 'object' | 'number' | 'string' | 'any',
Serializer<any>
> = {
boolean: {
read: (v: any) => v === 'true',
write: (v: any) => String(v),
},
object: {
read: (v: any) => JSON.parse(v),
write: (v: any) => JSON.stringify(v),
},
number: {
read: (v: any) => Number.parseFloat(v),
write: (v: any) => String(v),
},
string: {
read: (v: any) => v,
write: (v: any) => String(v),
},
any: {
read: (v: any) => v,
write: (v: any) => String(v),
},
}

export type StorageLike = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>

export interface StorageOptions<T> extends ConfigurableWindow {
/**
* Listen to storage changes, useful for multiple tabs application
*
* @default true
*/
listenToStorageChanges?: boolean

/**
* Custom data serialization
*/
serializer?: Serializer<T>

/**
* On error callback
*
* Default lo error to `console.error`
*/
onError?: (error: unknown) => void
}

export function useStorage(
key: string,
initialValue: MaybeWritable<string>,
storage?: StorageLike,
options?: StorageOptions<string>,
): RemovableWritable<string>
export function useStorage(
key: string,
initialValue: MaybeWritable<boolean>,
storage?: StorageLike,
options?: StorageOptions<boolean>,
): RemovableWritable<boolean>
export function useStorage(
key: string,
initialValue: MaybeWritable<number>,
storage?: StorageLike,
options?: StorageOptions<number>,
): RemovableWritable<number>
export function useStorage<T>(
key: string,
initialValue: MaybeWritable<T>,
storage?: StorageLike,
options?: StorageOptions<T>,
): RemovableWritable<T>
export function useStorage<T = unknown>(
key: string,
initialValue: MaybeWritable<null>,
storage?: StorageLike,
options?: StorageOptions<T>,
): RemovableWritable<T>

/**
* Reactive LocalStorage/SessionStorage
*
* @see https://svelte-use.vercel.app/core/useStorage
* @param key
* @param initialValue
* @param storage
* @param options
*/
export function useStorage<T extends boolean | object | number | string | null>(
key: string,
initialValue: MaybeWritable<T>,
storage: StorageLike | undefined = defaultWindow?.localStorage,
options: StorageOptions<T> = {},
): RemovableWritable<T> {
const {
listenToStorageChanges = true,
window = defaultWindow,
onError = (e) => {
console.error(e)
},
} = options

const rawInit: T = unstore(initialValue)

// https://github.com/vueuse/vueuse/blob/706a04921c6bb81a5fc3687c2b2748a81f5e9a21/packages/core/useStorage/index.ts#L98
const type =
rawInit == null
? 'any'
: typeof rawInit === 'boolean'
? 'boolean'
: typeof rawInit === 'string'
? 'string'
: typeof rawInit === 'object'
? 'object'
: Array.isArray(rawInit)
? 'object'
: !Number.isNaN(rawInit)
? 'number'
: 'any'

const data = toWritable(initialValue) as RemovableWritable<T>
const serializer = options.serializer ?? StorageSerializers[type]

function read(event?: StorageEvent) {
if (!storage || (event && event.key !== key)) {
return
}

try {
const rawValue = event ? event.newValue : storage.getItem(key)
if (rawValue == null) {
data.set(rawInit)
if (rawInit !== null) {
storage.setItem(key, serializer.write(rawInit))
}
} else {
data.set(serializer.read(rawValue))
}
} catch (e) {
onError(e)
}
}

read()

if (window && listenToStorageChanges) {
useEventListener(window, 'storage', read)
}

if (storage) {
data.subscribe((value) => {
try {
if (value == null) {
storage.removeItem(key)
} else {
storage.setItem(key, serializer.write(value))
}
} catch (e) {
onError(e)
}
})
}

return data
}
3 changes: 3 additions & 0 deletions packages/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
### Sensors
- [`useMutationObserver`](/core/useMutationObserver/) — watch for changes being made to the DOM tree

### State
- [`useStorage`](/core/useStorage/) — reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)/[SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage)

### Utilities
- [`whenever`](/shared/whenever/) — shorthand for watching value to be truthy

Expand Down
2 changes: 1 addition & 1 deletion packages/shared/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function promiseTimeout(
})
}

export function unReadable<T>(val: MaybeReadable<T>): T {
export function unstore<T>(val: MaybeReadable<T>): T {
return isReadable(val) ? get(val) : val
}

Expand Down
10 changes: 9 additions & 1 deletion packages/shared/utils/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Readable, Writable } from 'svelte/store'
import { Readable, Updater, Writable } from 'svelte/store'
/**
* Any function
*/
Expand Down Expand Up @@ -27,6 +27,14 @@ export type MaybeReadable<T> = T | Readable<T>
*/
export type MaybeWritable<T> = T | Writable<T>

/**
* A writable that allow to set/update `null` or `undefined`
*/
export type RemovableWritable<T> = Writable<T> & {
set(this: void, value: T | null | undefined): void
update(this: void, updater: Updater<T | null | undefined>): void
}

export interface Stopable {
/**
* A writable indicate whether a stopable instance is executing
Expand Down

0 comments on commit c5aba7f

Please sign in to comment.