From b8485577d9d160e22df2de6fb17af9a59e1b53bb Mon Sep 17 00:00:00 2001 From: Salvatore Tedde Date: Sat, 29 Feb 2020 01:06:39 +0000 Subject: [PATCH 1/4] improvement(useCookie): Adding ability to define serializer and deserializer BREAKING CHANGE: The options object has changed radically --- README.md | 2 + .../useCookie/stories/UseCookieDemo.vue | 4 +- src/components/useCookie/stories/useCookie.md | 16 +++- src/components/useCookie/useCookie.spec.ts | 89 ++++++++++++++++++- src/components/useCookie/useCookie.ts | 38 ++++++-- .../useGeolocation/useGeolocation.ts | 2 +- src/components/useLocalStorage/index.ts | 1 + .../stories/UseLocalStorageDemo.vue | 28 ++++++ .../stories/useLocalStorage.md | 23 +++++ .../stories/useLocalStorage.story.ts | 28 ++++++ .../useLocalStorage/useLocalStorage.spec.ts | 12 +++ .../useLocalStorage/useLocalStorage.ts | 15 ++++ src/utils.ts | 32 +++++++ src/vue-use-kit.ts | 1 + 14 files changed, 278 insertions(+), 13 deletions(-) create mode 100755 src/components/useLocalStorage/index.ts create mode 100755 src/components/useLocalStorage/stories/UseLocalStorageDemo.vue create mode 100755 src/components/useLocalStorage/stories/useLocalStorage.md create mode 100755 src/components/useLocalStorage/stories/useLocalStorage.story.ts create mode 100755 src/components/useLocalStorage/useLocalStorage.spec.ts create mode 100755 src/components/useLocalStorage/useLocalStorage.ts diff --git a/README.md b/README.md index bd41ae1..7df2547 100755 --- a/README.md +++ b/README.md @@ -107,6 +107,8 @@ Vue.use(VueCompositionAPI); [![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/side-effects-usebeforeunload--demo) - [`useCookie`](./src/components/useCookie/stories/useCookie.md) — provides way to read, update and delete a cookie. [![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/side-effects-usecookie--demo) + - [`useLocalStorage`](./src/components/useLocalStorage/stories/useLocalStorage.md) — provides way to read, update and delete a localStorage key. + [![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/side-effects-uselocalstorage--demo) - UI - [`useClickAway`](./src/components/useClickAway/stories/useClickAway.md) — triggers callback when user clicks outside target area. [![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/ui-useclickaway--demo) diff --git a/src/components/useCookie/stories/UseCookieDemo.vue b/src/components/useCookie/stories/UseCookieDemo.vue index 5b46943..ed0113b 100755 --- a/src/components/useCookie/stories/UseCookieDemo.vue +++ b/src/components/useCookie/stories/UseCookieDemo.vue @@ -52,7 +52,9 @@ export default Vue.extend({ cookie: jsonCookie, setCookie: jsonSetCookie, removeCookie: jsonRemoveCookie - } = useCookie('jsonCookie') + } = useCookie('jsonCookie', { + isParsing: true + }) let counter = 0 const handleSetCookie = () => { diff --git a/src/components/useCookie/stories/useCookie.md b/src/components/useCookie/stories/useCookie.md index 86ec705..28cd7da 100755 --- a/src/components/useCookie/stories/useCookie.md +++ b/src/components/useCookie/stories/useCookie.md @@ -4,10 +4,18 @@ Vue function that provides way to read, set and remove a cookie. ## Reference +```typescript +interface UseCookieOptions { + isParsing: boolean; + serializer?: (value: any) => string; + deserializer?: (value: string) => any; +} +``` + ```typescript function useCookie( cookieName: string, - enableParseJSON?: boolean, + options?: UseCookieOptions, runOnMount?: boolean ): { cookie: Ref @@ -20,7 +28,10 @@ function useCookie( ### Parameters - `cookieName: string` the cookie name you wish to get/set/remove -- `enableParseJSON: boolean` whether to enable JSON parsing or not, `false` by default +- `options: UseCookieOptions` + - `isParsing: boolean` whether to enable parsing the cookie value or not, `false` by default + - `serializer: Function` a custom serializer, `JSON.stringify` by default + - `deserializer: Function` a custom deserializer, `JSON.parse` by default - `runOnMount: boolean` whether to get the cookie on mount or not, `true` by default ### Returns @@ -65,4 +76,5 @@ export default Vue.extend({ } } }) + ``` diff --git a/src/components/useCookie/useCookie.spec.ts b/src/components/useCookie/useCookie.spec.ts index b0c06aa..8afe13e 100755 --- a/src/components/useCookie/useCookie.spec.ts +++ b/src/components/useCookie/useCookie.spec.ts @@ -9,7 +9,7 @@ afterEach(() => { const testComponent = ( cookieName = 'cookieName', cookieValue: any = '', - parseJson = false, + opts = { isParsing: false } as any, onMount = true ) => ({ template: ` @@ -26,7 +26,7 @@ const testComponent = ( setup() { const { cookie, getCookie, setCookie, removeCookie } = useCookie( cookieName, - parseJson, + opts, onMount ) @@ -75,7 +75,9 @@ describe('useCookie', () => { it('should correctly get and set the parseToJson object', async () => { const cookieName = 'cookieName' const cookieValue = { value1: 'testValue1', value2: 'testValue2' } - const wrapper = mount(testComponent(cookieName, cookieValue, true)) + const wrapper = mount( + testComponent(cookieName, cookieValue, { isParsing: true }) + ) wrapper.find('#setCookie').trigger('click') await wrapper.vm.$nextTick() expect(wrapper.find('#cookieJson').html()).toContain(cookieValue.value1) @@ -87,4 +89,85 @@ describe('useCookie', () => { expect(wrapper.find('#cookieJson').html()).toContain(cookieValue.value1) expect(wrapper.find('#cookieJson').html()).toContain(cookieValue.value2) }) + + it('should correctly get using the deserializer', async () => { + const cookieName = 'cookieName' + const cookieValue = { value1: 'testValue1', value2: 'testValue2' } + const deserializerVal = { value1: 'gatto', value2: 'topo' } + const wrapper = mount( + testComponent(cookieName, cookieValue, { + isParsing: true, + deserializer: () => deserializerVal + }) + ) + await wrapper.vm.$nextTick() + expect(wrapper.find('#cookieJson').html()).toContain(deserializerVal.value1) + expect(wrapper.find('#cookieJson').html()).toContain(deserializerVal.value2) + }) + + it('should ignore deserializer when isParsing is false', async () => { + const cookieName = 'cookieName' + const cookieValue = { value1: 'testValue1', value2: 'testValue2' } + const deserializerVal = { value1: 'gatto', value2: 'topo' } + const wrapper = mount( + testComponent(cookieName, cookieValue, { + isParsing: false, + deserializer: () => deserializerVal + }) + ) + await wrapper.vm.$nextTick() + expect(wrapper.find('#cookie').html()).toContain(cookieValue.value1) + expect(wrapper.find('#cookie').html()).toContain(cookieValue.value2) + expect(wrapper.find('#cookieJson').html()).not.toContain( + deserializerVal.value1 + ) + expect(wrapper.find('#cookieJson').html()).not.toContain( + deserializerVal.value2 + ) + }) + + it('should correctly set using the serializer', async () => { + const cookieName = 'cookieName' + const cookieValue = { value1: 'testValue1', value2: 'testValue2' } + const serializerVal = { value1: 'testValue1+1', value2: 'testValue2+1' } + const wrapper = mount( + testComponent(cookieName, cookieValue, { + isParsing: true, + serializer: (obj: any) => ({ + value1: `${obj.value1}+1`, + value2: `${obj.value2}+1` + }) + }) + ) + wrapper.find('#setCookie').trigger('click') + wrapper.find('#getCookie').trigger('click') + await wrapper.vm.$nextTick() + expect(wrapper.find('#cookieJson').html()).toContain(serializerVal.value1) + expect(wrapper.find('#cookieJson').html()).toContain(serializerVal.value2) + }) + + it('should ignore serializer when isParsing is false', async () => { + const cookieName = 'cookieName' + const cookieValue = { value1: 'testValue1', value2: 'testValue2' } + const serializerVal = { value1: 'testValue1+1', value2: 'testValue2+1' } + const wrapper = mount( + testComponent(cookieName, cookieValue, { + isParsing: false, + serializer: (obj: any) => ({ + value1: `${obj.value1}+1`, + value2: `${obj.value2}+1` + }) + }) + ) + wrapper.find('#setCookie').trigger('click') + wrapper.find('#getCookie').trigger('click') + await wrapper.vm.$nextTick() + expect(wrapper.find('#cookie').html()).toContain('[object Object]') + expect(wrapper.find('#cookieJson').html()).not.toContain( + serializerVal.value1 + ) + expect(wrapper.find('#cookieJson').html()).not.toContain( + serializerVal.value2 + ) + }) }) diff --git a/src/components/useCookie/useCookie.ts b/src/components/useCookie/useCookie.ts index 702e7f6..0b25c22 100755 --- a/src/components/useCookie/useCookie.ts +++ b/src/components/useCookie/useCookie.ts @@ -1,28 +1,54 @@ import Cookies from 'cookie-universal' import { CookieSerializeOptions } from 'cookie' +import { + createSerializer, + createDeserializer, + trySerialize, + tryDeserialize +} from '@src/utils' import { ref, onMounted, Ref } from '@src/api' +export interface UseCookieOptions { + isParsing: boolean + serializer?: (value: any) => string + deserializer?: (value: string) => any +} + +const defaultOptions = { + isParsing: false +} + export function useCookie( cookieName: string, - enableParseJSON = false, + options?: UseCookieOptions, runOnMount = true ) { - const cookieLib = Cookies(undefined, undefined, enableParseJSON) + const { isParsing, ...opts } = Object.assign({}, defaultOptions, options) + const cookieLib = Cookies(undefined, undefined, false) const cookie: Ref = ref(null) + const serializer = createSerializer(opts.serializer) + const deserializer = createDeserializer(opts.deserializer) + const getCookie = () => { - const cookieVal = cookieLib.get(cookieName) + const cookieVal = tryDeserialize( + cookieLib.get(cookieName), + deserializer, + isParsing + ) if (typeof cookieVal !== 'undefined') cookie.value = cookieVal } const setCookie = ( - // The user may pass a 'string', a 'number', or a valid JSON object/array + // The user may pass a 'string', a 'number', a valid JSON object/array + // or even a custom object when serializer/deserializer are defined // so it is better to set allowed cookie value as 'any' newVal: any, options?: CookieSerializeOptions ) => { - cookieLib.set(cookieName, newVal, options) - cookie.value = newVal + const newCookieVal = trySerialize(newVal, serializer, isParsing) + cookieLib.set(cookieName, newCookieVal, options) + cookie.value = tryDeserialize(newCookieVal, deserializer, isParsing) } const removeCookie = (options?: CookieSerializeOptions) => { diff --git a/src/components/useGeolocation/useGeolocation.ts b/src/components/useGeolocation/useGeolocation.ts index 4a283db..6ebec03 100755 --- a/src/components/useGeolocation/useGeolocation.ts +++ b/src/components/useGeolocation/useGeolocation.ts @@ -23,7 +23,7 @@ export function useGeolocation( options: PositionOptions = {}, runOnMount = true ) { - options = Object.assign(defaultOpts, options) + options = Object.assign({}, defaultOpts, options) // Note: surprisingly the watchId can be 0 (not positive) so // we have to check if watchId !== null every time diff --git a/src/components/useLocalStorage/index.ts b/src/components/useLocalStorage/index.ts new file mode 100755 index 0000000..f089557 --- /dev/null +++ b/src/components/useLocalStorage/index.ts @@ -0,0 +1 @@ +export * from './useLocalStorage' diff --git a/src/components/useLocalStorage/stories/UseLocalStorageDemo.vue b/src/components/useLocalStorage/stories/UseLocalStorageDemo.vue new file mode 100755 index 0000000..c89532f --- /dev/null +++ b/src/components/useLocalStorage/stories/UseLocalStorageDemo.vue @@ -0,0 +1,28 @@ + + + diff --git a/src/components/useLocalStorage/stories/useLocalStorage.md b/src/components/useLocalStorage/stories/useLocalStorage.md new file mode 100755 index 0000000..71730e0 --- /dev/null +++ b/src/components/useLocalStorage/stories/useLocalStorage.md @@ -0,0 +1,23 @@ +# useLocalStorage + +Vue function that... + +## Reference + +```typescript +// function useLocalStorage() +``` + +### Parameters + +- `value: string` lorem ipsa + +### Returns + +- `value: Ref` lorem ipsa + +## Usage + +```html + +``` diff --git a/src/components/useLocalStorage/stories/useLocalStorage.story.ts b/src/components/useLocalStorage/stories/useLocalStorage.story.ts new file mode 100755 index 0000000..7f59732 --- /dev/null +++ b/src/components/useLocalStorage/stories/useLocalStorage.story.ts @@ -0,0 +1,28 @@ +import { storiesOf } from '@storybook/vue' +import path from 'path' +import StoryTitle from '@src/helpers/StoryTitle.vue' +import UseLocalStorageDemo from './UseLocalStorageDemo.vue' + +const functionName = 'useLocalStorage' +const functionPath = path.resolve(__dirname, '..') +const notes = require(`./${functionName}.md`).default + +const basicDemo = () => ({ + components: { StoryTitle, demo: UseLocalStorageDemo }, + template: ` +
+ + + + + +
` +}) + +storiesOf('side effects|useLocalStorage', module) + .addParameters({ notes }) + .add('Demo', basicDemo) diff --git a/src/components/useLocalStorage/useLocalStorage.spec.ts b/src/components/useLocalStorage/useLocalStorage.spec.ts new file mode 100755 index 0000000..f23b2d6 --- /dev/null +++ b/src/components/useLocalStorage/useLocalStorage.spec.ts @@ -0,0 +1,12 @@ +// import { mount } from '@src/helpers/test' +// import { useLocalStorage } from '@src/vue-use-kit' + +afterEach(() => { + jest.clearAllMocks() +}) + +describe('useLocalStorage', () => { + it('should do something', () => { + // Add test here + }) +}) diff --git a/src/components/useLocalStorage/useLocalStorage.ts b/src/components/useLocalStorage/useLocalStorage.ts new file mode 100755 index 0000000..0213f8c --- /dev/null +++ b/src/components/useLocalStorage/useLocalStorage.ts @@ -0,0 +1,15 @@ +import { ref, onMounted, onUnmounted, Ref } from '@src/api' + +export function useLocalStorage() { + const isMounted = ref(false) + + onMounted(() => { + isMounted.value = true + }) + + onUnmounted(() => { + isMounted.value = false + }) + + return isMounted +} diff --git a/src/utils.ts b/src/utils.ts index 61cd5fe..acbabad 100755 --- a/src/utils.ts +++ b/src/utils.ts @@ -25,3 +25,35 @@ export const normalizeEntriesData = (data: [any, any][]) => acc[key] = val return acc }, {} as { [key: string]: any }) + +export const createSerializer = (serializer?: Function) => + serializer || JSON.stringify + +export const createDeserializer = (deserializer?: Function) => + deserializer || JSON.parse + +export const trySerialize = ( + val: any, + serializer: Function, + shouldSerialize?: boolean +) => { + if (!shouldSerialize) return String(val) + try { + return serializer(val) + } catch (error) { + return val + } +} + +export const tryDeserialize = ( + val: string, + deserializer: Function, + shouldDeserialize?: boolean +) => { + if (!shouldDeserialize) return val + try { + return deserializer(val) + } catch (error) { + return val + } +} diff --git a/src/vue-use-kit.ts b/src/vue-use-kit.ts index abe589e..98aac17 100755 --- a/src/vue-use-kit.ts +++ b/src/vue-use-kit.ts @@ -25,3 +25,4 @@ export * from './components/useTimeout' // Site Effects export * from './components/useBeforeUnload' export * from './components/useCookie' +export * from './components/useLocalStorage' From 4f66f19e58ca8c2abeb18c50b9d7d787064cac82 Mon Sep 17 00:00:00 2001 From: Salvatore Tedde Date: Sat, 29 Feb 2020 02:53:04 +0000 Subject: [PATCH 2/4] feat(useLocalStorage): Adding useLocalStorage function --- src/components/useCookie/useCookie.spec.ts | 20 +- src/components/useCookie/useCookie.ts | 11 +- .../stories/UseLocalStorageDemo.vue | 66 ++++++- .../useLocalStorage/useLocalStorage.spec.ts | 187 +++++++++++++++++- .../useLocalStorage/useLocalStorage.ts | 82 +++++++- src/helpers/utils.ts | 5 - src/utils.ts | 17 +- 7 files changed, 348 insertions(+), 40 deletions(-) delete mode 100755 src/helpers/utils.ts diff --git a/src/components/useCookie/useCookie.spec.ts b/src/components/useCookie/useCookie.spec.ts index 8afe13e..a752a0e 100755 --- a/src/components/useCookie/useCookie.spec.ts +++ b/src/components/useCookie/useCookie.spec.ts @@ -72,7 +72,7 @@ describe('useCookie', () => { expect(document.cookie).not.toContain(cookieValue) }) - it('should correctly get and set the parseToJson object', async () => { + it('should correctly get and set the isParsing object', async () => { const cookieName = 'cookieName' const cookieValue = { value1: 'testValue1', value2: 'testValue2' } const wrapper = mount( @@ -133,10 +133,11 @@ describe('useCookie', () => { const wrapper = mount( testComponent(cookieName, cookieValue, { isParsing: true, - serializer: (obj: any) => ({ - value1: `${obj.value1}+1`, - value2: `${obj.value2}+1` - }) + serializer: (obj: any) => + JSON.stringify({ + value1: `${obj.value1}+1`, + value2: `${obj.value2}+1` + }) }) ) wrapper.find('#setCookie').trigger('click') @@ -153,10 +154,11 @@ describe('useCookie', () => { const wrapper = mount( testComponent(cookieName, cookieValue, { isParsing: false, - serializer: (obj: any) => ({ - value1: `${obj.value1}+1`, - value2: `${obj.value2}+1` - }) + serializer: (obj: any) => + JSON.stringify({ + value1: `${obj.value1}+1`, + value2: `${obj.value2}+1` + }) }) ) wrapper.find('#setCookie').trigger('click') diff --git a/src/components/useCookie/useCookie.ts b/src/components/useCookie/useCookie.ts index 0b25c22..0897cd8 100755 --- a/src/components/useCookie/useCookie.ts +++ b/src/components/useCookie/useCookie.ts @@ -4,7 +4,8 @@ import { createSerializer, createDeserializer, trySerialize, - tryDeserialize + tryDeserialize, + isNullOrUndefined } from '@src/utils' import { ref, onMounted, Ref } from '@src/api' @@ -24,19 +25,19 @@ export function useCookie( runOnMount = true ) { const { isParsing, ...opts } = Object.assign({}, defaultOptions, options) - const cookieLib = Cookies(undefined, undefined, false) - const cookie: Ref = ref(null) - const serializer = createSerializer(opts.serializer) const deserializer = createDeserializer(opts.deserializer) + const cookieLib = Cookies(undefined, undefined, false) + const cookie: Ref = ref(null) + const getCookie = () => { const cookieVal = tryDeserialize( cookieLib.get(cookieName), deserializer, isParsing ) - if (typeof cookieVal !== 'undefined') cookie.value = cookieVal + if (!isNullOrUndefined(cookieVal)) cookie.value = cookieVal } const setCookie = ( diff --git a/src/components/useLocalStorage/stories/UseLocalStorageDemo.vue b/src/components/useLocalStorage/stories/UseLocalStorageDemo.vue index c89532f..cd411b8 100755 --- a/src/components/useLocalStorage/stories/UseLocalStorageDemo.vue +++ b/src/components/useLocalStorage/stories/UseLocalStorageDemo.vue @@ -8,8 +8,32 @@ - Sample - {{ sample }} + item + {{ item }} + + + + + + + + + jsonItem + {{ jsonItem }} + + + + + + @@ -17,12 +41,44 @@ diff --git a/src/components/useLocalStorage/useLocalStorage.spec.ts b/src/components/useLocalStorage/useLocalStorage.spec.ts index f23b2d6..a9b109e 100755 --- a/src/components/useLocalStorage/useLocalStorage.spec.ts +++ b/src/components/useLocalStorage/useLocalStorage.spec.ts @@ -1,12 +1,189 @@ -// import { mount } from '@src/helpers/test' -// import { useLocalStorage } from '@src/vue-use-kit' +import { mount } from '@src/helpers/test' +import { useLocalStorage } from '@src/vue-use-kit' + +const localStorageMock = () => { + let store = {} as any + return { + getItem(key: any) { + return store[key] + }, + setItem(key: any, value: any) { + store[key] = String(value) + }, + clear() { + store = {} + }, + removeItem(key: any) { + delete store[key] + } + } +} + +beforeEach(() => { + ;(window as any).localStorage = localStorageMock() +}) afterEach(() => { jest.clearAllMocks() }) -describe('useLocalStorage', () => { - it('should do something', () => { - // Add test here +const testComponent = ( + key = 'key', + itemValue: any = '', + opts = { isParsing: false } as any, + onMount = true +) => ({ + template: ` +
+
{{ item }}
+
+ {{ item && item.value1 }} - {{ item && item.value2 }} +
+ + + +
+ `, + setup() { + const { item, getItem, setItem, removeItem } = useLocalStorage( + key, + opts, + onMount + ) + + return { + item, + getItem, + setItem, + removeItem, + itemValue + } + } +}) + +describe('useItem', () => { + it('should get a item with the given value', async () => { + const itemKey = 'itemKey' + const itemValue = 'itemValue' + ;(window as any).localStorage.setItem(itemKey, itemValue) + const wrapper = mount(testComponent(itemKey)) + await wrapper.vm.$nextTick() + expect(wrapper.html()).toContain(itemValue) + }) + + it('should set a item with the given value', async () => { + const itemKey = 'itemKey' + const itemValue = 'itemValue' + const wrapper = mount(testComponent(itemKey, itemValue)) + await wrapper.vm.$nextTick() + expect(localStorage.getItem(itemKey)).toContain(itemValue) + }) + + it('should remove a item with the given value', async () => { + const itemKey = 'itemKey' + const itemValue = 'itemValue' + const wrapper = mount(testComponent(itemKey, itemValue)) + await wrapper.vm.$nextTick() + expect(localStorage.getItem(itemKey)).toContain(itemValue) + wrapper.find('#removeItem').trigger('click') + await wrapper.vm.$nextTick() + expect(localStorage.getItem(itemKey)).toBeFalsy() + }) + + it('should correctly get and set the isParsing object', async () => { + const itemKey = 'itemKey' + const itemValue = { value1: 'testValue1', value2: 'testValue2' } + const wrapper = mount( + testComponent(itemKey, itemValue, { isParsing: true }) + ) + wrapper.find('#setItem').trigger('click') + await wrapper.vm.$nextTick() + expect(wrapper.find('#itemJson').html()).toContain(itemValue.value1) + expect(wrapper.find('#itemJson').html()).toContain(itemValue.value2) + + // Also check that when clicking #getItem we still get a proper JSON object + wrapper.find('#getItem').trigger('click') + await wrapper.vm.$nextTick() + expect(wrapper.find('#itemJson').html()).toContain(itemValue.value1) + expect(wrapper.find('#itemJson').html()).toContain(itemValue.value2) + }) + + it('should correctly get using the deserializer', async () => { + const itemKey = 'itemKey' + const itemValue = { value1: 'testValue1', value2: 'testValue2' } + const deserializerVal = { value1: 'gatto', value2: 'topo' } + const wrapper = mount( + testComponent(itemKey, itemValue, { + isParsing: true, + deserializer: () => deserializerVal + }) + ) + await wrapper.vm.$nextTick() + expect(wrapper.find('#itemJson').html()).toContain(deserializerVal.value1) + expect(wrapper.find('#itemJson').html()).toContain(deserializerVal.value2) + }) + + it('should ignore deserializer when isParsing is false', async () => { + const itemKey = 'itemKey' + const itemValue = { value1: 'testValue1', value2: 'testValue2' } + const deserializerVal = { value1: 'gatto', value2: 'topo' } + const wrapper = mount( + testComponent(itemKey, itemValue, { + isParsing: false, + deserializer: () => deserializerVal + }) + ) + await wrapper.vm.$nextTick() + expect(wrapper.find('#item').html()).toContain(itemValue.value1) + expect(wrapper.find('#item').html()).toContain(itemValue.value2) + expect(wrapper.find('#itemJson').html()).not.toContain( + deserializerVal.value1 + ) + expect(wrapper.find('#itemJson').html()).not.toContain( + deserializerVal.value2 + ) + }) + + it('should correctly set using the serializer', async () => { + const itemKey = 'itemKey' + const itemValue = { value1: 'testValue1', value2: 'testValue2' } + const serializerVal = { value1: 'testValue1+1', value2: 'testValue2+1' } + const wrapper = mount( + testComponent(itemKey, itemValue, { + isParsing: true, + serializer: (obj: any) => + JSON.stringify({ + value1: `${obj.value1}+1`, + value2: `${obj.value2}+1` + }) + }) + ) + wrapper.find('#setItem').trigger('click') + wrapper.find('#getItem').trigger('click') + await wrapper.vm.$nextTick() + expect(wrapper.find('#itemJson').html()).toContain(serializerVal.value1) + expect(wrapper.find('#itemJson').html()).toContain(serializerVal.value2) + }) + + it('should ignore serializer when isParsing is false', async () => { + const itemKey = 'itemKey' + const itemValue = { value1: 'testValue1', value2: 'testValue2' } + const serializerVal = { value1: 'testValue1+1', value2: 'testValue2+1' } + const wrapper = mount( + testComponent(itemKey, itemValue, { + isParsing: false, + serializer: (obj: any) => + JSON.stringify({ + value1: `${obj.value1}+1`, + value2: `${obj.value2}+1` + }) + }) + ) + wrapper.find('#setItem').trigger('click') + wrapper.find('#getItem').trigger('click') + await wrapper.vm.$nextTick() + expect(wrapper.find('#item').html()).toContain('[object Object]') + expect(wrapper.find('#itemJson').html()).not.toContain(serializerVal.value1) + expect(wrapper.find('#itemJson').html()).not.toContain(serializerVal.value2) }) }) diff --git a/src/components/useLocalStorage/useLocalStorage.ts b/src/components/useLocalStorage/useLocalStorage.ts index 0213f8c..414a1a6 100755 --- a/src/components/useLocalStorage/useLocalStorage.ts +++ b/src/components/useLocalStorage/useLocalStorage.ts @@ -1,15 +1,77 @@ -import { ref, onMounted, onUnmounted, Ref } from '@src/api' +import { + createSerializer, + createDeserializer, + trySerialize, + tryDeserialize, + isNullOrUndefined +} from '@src/utils' +import { ref, onMounted, Ref } from '@src/api' -export function useLocalStorage() { - const isMounted = ref(false) +export interface UseLocalStorageOptions { + isParsing: boolean + serializer?: (value: any) => string + deserializer?: (value: string) => any +} + +const defaultOptions = { + isParsing: false +} + +export function useLocalStorage( + key: string, + options?: UseLocalStorageOptions, + runOnMount = true +) { + const { isParsing, ...opts } = Object.assign({}, defaultOptions, options) + const serializer = createSerializer(opts.serializer) + const deserializer = createDeserializer(opts.deserializer) + + const item: Ref = ref(null) + + const getItem = () => { + try { + const itemVal = tryDeserialize( + localStorage.getItem(key), + deserializer, + isParsing + ) + if (!isNullOrUndefined(itemVal)) item.value = itemVal + } catch (error) { + // If user is in private mode or has storage restriction localStorage can throw + } + } + + const setItem = ( + // The user may pass a 'string', a 'number', a valid JSON object/array + // or even a custom object when serializer/deserializer are defined + // so it is better to set allowed newItemVal as 'any' + newVal: any + ) => { + try { + const newItemVal = trySerialize(newVal, serializer, isParsing) + localStorage.setItem(key, newItemVal) + item.value = tryDeserialize(newItemVal, deserializer, isParsing) + } catch (error) { + // If user is in private mode or has storage restriction localStorage can throw + } + } - onMounted(() => { - isMounted.value = true - }) + const removeItem = () => { + try { + localStorage.removeItem(key) + item.value = null + } catch (error) { + // If user is in private mode or has storage restriction localStorage can throw + item.value = null + } + } - onUnmounted(() => { - isMounted.value = false - }) + onMounted(() => runOnMount && getItem()) - return isMounted + return { + item, + getItem, + setItem, + removeItem + } } diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts deleted file mode 100755 index 4f14c2c..0000000 --- a/src/helpers/utils.ts +++ /dev/null @@ -1,5 +0,0 @@ -const checkType = (typeToCheck: any) => - Object.prototype.toString.call(typeToCheck) - -export const isObj = (objToCheck: any) => - checkType(objToCheck) === '[object Object]' diff --git a/src/utils.ts b/src/utils.ts index acbabad..f629755 100755 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,18 @@ +const checkType = (typeToCheck: any) => + Object.prototype.toString.call(typeToCheck) + +export const isObj = (varToCheck: any) => + checkType(varToCheck) === '[object Object]' + +export const isNull = (varToCheck: any) => + checkType(varToCheck) === '[object Null]' + +export const isUndefined = (varToCheck: any) => + checkType(varToCheck) === '[object Undefined]' + +export const isNullOrUndefined = (varToCheck: any) => + isNull(varToCheck) || isUndefined(varToCheck) + // The history methods 'pushState' and 'replaceState' by default do not fire an event // unless it is coming from user interaction with the browser navigation bar, // so we are adding a patch to make them detectable @@ -46,7 +61,7 @@ export const trySerialize = ( } export const tryDeserialize = ( - val: string, + val: any, deserializer: Function, shouldDeserialize?: boolean ) => { From f8a8dc883c86ae2afff90f783eb0364f25e566e6 Mon Sep 17 00:00:00 2001 From: Salvatore Tedde Date: Sat, 29 Feb 2020 10:59:35 +0000 Subject: [PATCH 3/4] improvement(useLocalStorage): Always return string from useLocalStorage serializer --- src/components/useCookie/useCookie.spec.ts | 11 +++++------ .../useLocalStorage/useLocalStorage.spec.ts | 11 +++++------ src/utils.ts | 10 +++++++--- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/components/useCookie/useCookie.spec.ts b/src/components/useCookie/useCookie.spec.ts index a752a0e..1464603 100755 --- a/src/components/useCookie/useCookie.spec.ts +++ b/src/components/useCookie/useCookie.spec.ts @@ -126,7 +126,7 @@ describe('useCookie', () => { ) }) - it('should correctly set using the serializer', async () => { + it('should correctly set the object using the serializer', async () => { const cookieName = 'cookieName' const cookieValue = { value1: 'testValue1', value2: 'testValue2' } const serializerVal = { value1: 'testValue1+1', value2: 'testValue2+1' } @@ -154,11 +154,10 @@ describe('useCookie', () => { const wrapper = mount( testComponent(cookieName, cookieValue, { isParsing: false, - serializer: (obj: any) => - JSON.stringify({ - value1: `${obj.value1}+1`, - value2: `${obj.value2}+1` - }) + serializer: (obj: any) => ({ + value1: `${obj.value1}+1`, + value2: `${obj.value2}+1` + }) }) ) wrapper.find('#setCookie').trigger('click') diff --git a/src/components/useLocalStorage/useLocalStorage.spec.ts b/src/components/useLocalStorage/useLocalStorage.spec.ts index a9b109e..ebf3c42 100755 --- a/src/components/useLocalStorage/useLocalStorage.spec.ts +++ b/src/components/useLocalStorage/useLocalStorage.spec.ts @@ -144,7 +144,7 @@ describe('useItem', () => { ) }) - it('should correctly set using the serializer', async () => { + it('should correctly set the object using the serializer', async () => { const itemKey = 'itemKey' const itemValue = { value1: 'testValue1', value2: 'testValue2' } const serializerVal = { value1: 'testValue1+1', value2: 'testValue2+1' } @@ -172,11 +172,10 @@ describe('useItem', () => { const wrapper = mount( testComponent(itemKey, itemValue, { isParsing: false, - serializer: (obj: any) => - JSON.stringify({ - value1: `${obj.value1}+1`, - value2: `${obj.value2}+1` - }) + serializer: (obj: any) => ({ + value1: `${obj.value1}+1`, + value2: `${obj.value2}+1` + }) }) ) wrapper.find('#setItem').trigger('click') diff --git a/src/utils.ts b/src/utils.ts index f629755..536a179 100755 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,9 @@ const checkType = (typeToCheck: any) => Object.prototype.toString.call(typeToCheck) +export const isString = (varToCheck: any) => + checkType(varToCheck) === '[object String]' + export const isObj = (varToCheck: any) => checkType(varToCheck) === '[object Object]' @@ -47,16 +50,17 @@ export const createSerializer = (serializer?: Function) => export const createDeserializer = (deserializer?: Function) => deserializer || JSON.parse +const fallbackToString = (val: any) => (isString(val) ? val : String(val)) export const trySerialize = ( val: any, serializer: Function, shouldSerialize?: boolean ) => { - if (!shouldSerialize) return String(val) + if (!shouldSerialize) return fallbackToString(val) try { - return serializer(val) + return fallbackToString(serializer(val)) } catch (error) { - return val + return fallbackToString(val) } } From c03d6c5ab1b2d392d690a24da4be731383ab5634 Mon Sep 17 00:00:00 2001 From: Salvatore Tedde Date: Sat, 29 Feb 2020 11:34:29 +0000 Subject: [PATCH 4/4] improvement(useLocalStorage): Improve useLocalStorage typescript types --- src/components/useCookie/stories/useCookie.md | 43 ++++++------- src/components/useCookie/useCookie.ts | 6 +- .../stories/useLocalStorage.md | 64 +++++++++++++++++-- .../useLocalStorage/useLocalStorage.ts | 6 +- src/utils.ts | 12 ++-- 5 files changed, 96 insertions(+), 35 deletions(-) diff --git a/src/components/useCookie/stories/useCookie.md b/src/components/useCookie/stories/useCookie.md index 28cd7da..5466028 100755 --- a/src/components/useCookie/stories/useCookie.md +++ b/src/components/useCookie/stories/useCookie.md @@ -6,9 +6,9 @@ Vue function that provides way to read, set and remove a cookie. ```typescript interface UseCookieOptions { - isParsing: boolean; - serializer?: (value: any) => string; - deserializer?: (value: string) => any; + isParsing: boolean + serializer?: SerializerFunction + deserializer?: DeserializerFunction } ``` @@ -30,8 +30,8 @@ function useCookie( - `cookieName: string` the cookie name you wish to get/set/remove - `options: UseCookieOptions` - `isParsing: boolean` whether to enable parsing the cookie value or not, `false` by default - - `serializer: Function` a custom serializer, `JSON.stringify` by default - - `deserializer: Function` a custom deserializer, `JSON.parse` by default + - `serializer: SerializerFunction` a custom serializer, `JSON.stringify` by default + - `deserializer: DeserializerFunction` a custom deserializer, `JSON.parse` by default - `runOnMount: boolean` whether to get the cookie on mount or not, `true` by default ### Returns @@ -49,8 +49,7 @@ function useCookie( ```html ``` diff --git a/src/components/useCookie/useCookie.ts b/src/components/useCookie/useCookie.ts index 0897cd8..9d667d7 100755 --- a/src/components/useCookie/useCookie.ts +++ b/src/components/useCookie/useCookie.ts @@ -3,6 +3,8 @@ import { CookieSerializeOptions } from 'cookie' import { createSerializer, createDeserializer, + SerializerFunction, + DeserializerFunction, trySerialize, tryDeserialize, isNullOrUndefined @@ -11,8 +13,8 @@ import { ref, onMounted, Ref } from '@src/api' export interface UseCookieOptions { isParsing: boolean - serializer?: (value: any) => string - deserializer?: (value: string) => any + serializer?: SerializerFunction + deserializer?: DeserializerFunction } const defaultOptions = { diff --git a/src/components/useLocalStorage/stories/useLocalStorage.md b/src/components/useLocalStorage/stories/useLocalStorage.md index 71730e0..901f865 100755 --- a/src/components/useLocalStorage/stories/useLocalStorage.md +++ b/src/components/useLocalStorage/stories/useLocalStorage.md @@ -1,23 +1,77 @@ # useLocalStorage -Vue function that... +Vue function that provides way to read, update and delete a localStorage key ## Reference ```typescript -// function useLocalStorage() +interface UseLocalStorageOptions { + isParsing: boolean + serializer?: SerializerFunction + deserializer?: DeserializerFunction +} +``` + +```typescript +function useLocalStorage( + key: string, + options?: UseLocalStorageOptions, + runOnMount?: boolean +): { + item: Ref + getItem: () => void + setItem: (newVal: any) => void + removeItem: () => void +} ``` ### Parameters -- `value: string` lorem ipsa +- `key: string` the localstorage key you wish to get/set/remove +- `options: UseLocalStorageOptions` + - `isParsing: boolean` whether to enable parsing the localstorage key value or not, `false` by default + - `serializer: SerializerFunction` a custom serializer, `JSON.stringify` by default + - `deserializer: DeserializerFunction` a custom deserializer, `JSON.parse` by default +- `runOnMount: boolean` whether to get the localstorage key on mount or not, `true` by default ### Returns -- `value: Ref` lorem ipsa +- `item: Ref` the localstorage key value, it can be null, a string or a JSON object/array +- `getItem: Function` get the localstorage key value +- `setItem: Function` set the localstorage key value + - `newVal: any`: the value to set, can be a string or an object/array +- `removeItem: Function` delete the localstorage key ## Usage ```html - + + + ``` diff --git a/src/components/useLocalStorage/useLocalStorage.ts b/src/components/useLocalStorage/useLocalStorage.ts index 414a1a6..3631122 100755 --- a/src/components/useLocalStorage/useLocalStorage.ts +++ b/src/components/useLocalStorage/useLocalStorage.ts @@ -1,6 +1,8 @@ import { createSerializer, createDeserializer, + SerializerFunction, + DeserializerFunction, trySerialize, tryDeserialize, isNullOrUndefined @@ -9,8 +11,8 @@ import { ref, onMounted, Ref } from '@src/api' export interface UseLocalStorageOptions { isParsing: boolean - serializer?: (value: any) => string - deserializer?: (value: string) => any + serializer?: SerializerFunction + deserializer?: DeserializerFunction } const defaultOptions = { diff --git a/src/utils.ts b/src/utils.ts index 536a179..0ba62fb 100755 --- a/src/utils.ts +++ b/src/utils.ts @@ -44,16 +44,20 @@ export const normalizeEntriesData = (data: [any, any][]) => return acc }, {} as { [key: string]: any }) -export const createSerializer = (serializer?: Function) => +export type SerializerFunction = (value: any) => string + +export type DeserializerFunction = (value: string) => any + +export const createSerializer = (serializer?: SerializerFunction) => serializer || JSON.stringify -export const createDeserializer = (deserializer?: Function) => +export const createDeserializer = (deserializer?: DeserializerFunction) => deserializer || JSON.parse const fallbackToString = (val: any) => (isString(val) ? val : String(val)) export const trySerialize = ( val: any, - serializer: Function, + serializer: SerializerFunction, shouldSerialize?: boolean ) => { if (!shouldSerialize) return fallbackToString(val) @@ -66,7 +70,7 @@ export const trySerialize = ( export const tryDeserialize = ( val: any, - deserializer: Function, + deserializer: DeserializerFunction, shouldDeserialize?: boolean ) => { if (!shouldDeserialize) return val