Skip to content

Commit

Permalink
feat: useLocalStorage (#10)
Browse files Browse the repository at this point in the history
* WIP: localStorage

* WIP: localStorage dummy test

* WIP: implemented localstorage tests

* docs: localstorage docs
  • Loading branch information
pikax authored Nov 2, 2019
1 parent f16264f commit d0b0aeb
Show file tree
Hide file tree
Showing 13 changed files with 643 additions and 28 deletions.
148 changes: 148 additions & 0 deletions __tests__/localStorage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { useLocalStorage } from "../src/localStorage";
import { nextTick } from "./utils";
import { promisedTimeout } from "../src/utils";

describe("localStorage", () => {
const _localStorage = localStorage;
let localStore: Record<string, string> = {};
let len = 0;

const setItem = jest.fn((key: string, value: string) => {
localStore[key] = value.toString();
len = Object.keys(localStore).length;
});
const getItem = jest.fn((key: string) => localStore[key]);
const removeItem = jest.fn((key: string) => {
delete localStore[key];
len = Object.keys(localStore).length;
});

const key = jest.fn(index => {
return Object.keys(localStore)[index];
});

const clear = jest.fn(() => (localStore = {}));

let mockedLocalStorage: Storage = {
setItem,
getItem,
clear,
removeItem,
length: len,
key
};

Object.defineProperty(window, "localStorage", {
value: mockedLocalStorage
});

beforeEach(() => {
setItem.mockClear();
getItem.mockClear();
clear.mockClear();
removeItem.mockClear();
key.mockClear();
});

afterEach(async () => {
useLocalStorage("").clear();
await promisedTimeout(100);
localStore = {};
len = 0;
});

afterAll(() => {
Object.defineProperty(window, "localStorage", {
value: _localStorage
});
});

it("should store in the localStore", () => {
localStorage.setItem("test", "test");

expect(localStore["test"]).toBe("test");
});

it("should store object in localStorage if default is passed", async () => {
const obj = { a: 1 };
const { storage } = useLocalStorage("test", obj);

await promisedTimeout(100);

expect(storage.value).toMatchObject(obj);
expect(setItem).toHaveBeenCalledWith("test", JSON.stringify(obj));
});

it("should update the localstorage if value changes", async () => {
const obj = { a: 1 };

const { storage } = useLocalStorage("test", obj);
await nextTick();
await promisedTimeout(100);

expect(storage.value).toMatchObject(obj);
expect(setItem).toHaveBeenCalledWith("test", JSON.stringify(obj));

storage.value.a = 33;
await nextTick();

await promisedTimeout(100);

expect(storage.value).toMatchObject({ a: 33 });
expect(setItem).toHaveBeenCalledWith("test", JSON.stringify({ a: 33 }));

expect(localStore["test"]).toBe(JSON.stringify({ a: 33 }));
});

it("should get the same object if the same key is used", () => {
const key = "test";
const { storage: storage1 } = useLocalStorage(key, { a: 1 });
const { storage: storage2 } = useLocalStorage(key, { a: 1 });

expect(storage1).toBe(storage2);
});

it("should remove from localstorage", async () => {
const key = "test";
const { remove } = useLocalStorage(key, { a: 1 });

remove();
await nextTick();
expect(localStore[key]).toBeUndefined();
});

it("should clear all localstorage keys", async () => {
localStorage.setItem("_other_", "secret");
const s1 = useLocalStorage("key", { a: 1 });
const s2 = useLocalStorage("key2", { a: 2 });

await promisedTimeout(100);

expect(localStore).toMatchObject({
key: JSON.stringify(s1.storage.value),
key2: JSON.stringify(s2.storage.value),
_other_: "secret"
});

s1.clear();

await nextTick();
await promisedTimeout(200);

expect(s1.storage.value).toBeUndefined();
expect(s2.storage.value).toBeUndefined();
expect(localStore).toStrictEqual({
_other_: "secret"
});
});


it("should load from localStorage", () => {
const key = "hello";
localStorage.setItem(key, JSON.stringify({ k: 1 }));

const { storage } = useLocalStorage(key, { k: 10 });

expect(storage.value).toMatchObject({ k: 1 });
});
});
27 changes: 14 additions & 13 deletions dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
export * from './event/event';
export * from './pagination/arrayPagination';
export * from './debounce';
export * from './event/onMouseMove';
export * from './event/onResize';
export * from './event/onScroll';
export * from './pagination/pagination';
export * from './promise/promise';
export * from './promise/cancellablePromise';
export * from './promise/retry';
export * from './web/fetch';
export * from './web/axios';
export * from './web/webSocket';
export * from "./event/event";
export * from "./pagination/arrayPagination";
export * from "./debounce";
export * from "./event/onMouseMove";
export * from "./event/onResize";
export * from "./event/onScroll";
export * from "./pagination/pagination";
export * from "./promise/promise";
export * from "./promise/cancellablePromise";
export * from "./promise/retry";
export * from "./web/fetch";
export * from "./web/axios";
export * from "./web/webSocket";
export * from "./localStorage";
15 changes: 15 additions & 0 deletions dist/localStorage.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Ref } from "@vue/composition-api";
import { RefTyped } from "./utils";
export declare type LocalStorageTyped<T> = string;
export interface LocalStorageReturn<T> {
storage: Ref<T>;
/**
* @description Removes current item from the store
*/
remove: () => void;
/**
* @description Clears all tracked localStorage items
*/
clear: () => void;
}
export declare function useLocalStorage<T = any>(key: LocalStorageTyped<T> | string, defaultValue?: RefTyped<T>): LocalStorageReturn<T>;
61 changes: 61 additions & 0 deletions dist/vue-composable.cjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,66 @@ function useWebSocket(url, protocols) {
};
}

// used to store all the instances of weakMap
const keyedMap = new Map();
const weakMap = new WeakMap();
function useLocalStorage(key, defaultValue) {
let lazy = false;
let k = keyedMap.get(key);
const json = localStorage.getItem(key);
const storage = (k && weakMap.get(k)) ||
(!!defaultValue && wrap(defaultValue)) ||
compositionApi.ref(null);
if (json && !k) {
try {
storage.value = JSON.parse(json);
lazy = false;
}
catch (e) {
/* istanbul ignore next */
console.warn("[useLocalStorage] error parsing value from localStorage", key, e);
}
}
// do not watch if we already created the instance
if (!k) {
k = {};
keyedMap.set(key, k);
weakMap.set(k, storage);
compositionApi.watch(storage, storage => {
if (storage === undefined) {
localStorage.removeItem(key);
return;
}
// do not overflow localStorage with updates nor keep doing stringify
debounce(() => localStorage.setItem(key, JSON.stringify(storage)), 100)();
}, {
deep: true,
lazy
});
}
const clear = () => {
keyedMap.forEach((v) => {
const obj = weakMap.get(v);
/* istanbul ignore else */
if (obj) {
obj.value = undefined;
}
weakMap.delete(v);
});
keyedMap.clear();
};
const remove = () => {
keyedMap.delete(key);
weakMap.delete(k);
storage.value = undefined;
};
return {
storage,
clear,
remove
};
}

exports.debounce = debounce;
exports.exponentialDelay = exponentialDelay;
exports.noDelay = noDelay;
Expand All @@ -571,6 +631,7 @@ exports.useCancellablePromise = useCancellablePromise;
exports.useDebounce = useDebounce;
exports.useEvent = useEvent;
exports.useFetch = useFetch;
exports.useLocalStorage = useLocalStorage;
exports.useOnMouseMove = useOnMouseMove;
exports.useOnResize = useOnResize;
exports.useOnScroll = useOnScroll;
Expand Down
62 changes: 61 additions & 1 deletion dist/vue-composable.es.js
Original file line number Diff line number Diff line change
Expand Up @@ -556,4 +556,64 @@ function useWebSocket(url, protocols) {
};
}

export { debounce, exponentialDelay, noDelay, useArrayPagination, useAxios, useCancellablePromise, useDebounce, useEvent, useFetch, useOnMouseMove, useOnResize, useOnScroll, usePagination, usePromise, useRetry, useWebSocket };
// used to store all the instances of weakMap
const keyedMap = new Map();
const weakMap = new WeakMap();
function useLocalStorage(key, defaultValue) {
let lazy = false;
let k = keyedMap.get(key);
const json = localStorage.getItem(key);
const storage = (k && weakMap.get(k)) ||
(!!defaultValue && wrap(defaultValue)) ||
ref(null);
if (json && !k) {
try {
storage.value = JSON.parse(json);
lazy = false;
}
catch (e) {
/* istanbul ignore next */
console.warn("[useLocalStorage] error parsing value from localStorage", key, e);
}
}
// do not watch if we already created the instance
if (!k) {
k = {};
keyedMap.set(key, k);
weakMap.set(k, storage);
watch(storage, storage => {
if (storage === undefined) {
localStorage.removeItem(key);
return;
}
// do not overflow localStorage with updates nor keep doing stringify
debounce(() => localStorage.setItem(key, JSON.stringify(storage)), 100)();
}, {
deep: true,
lazy
});
}
const clear = () => {
keyedMap.forEach((v) => {
const obj = weakMap.get(v);
/* istanbul ignore else */
if (obj) {
obj.value = undefined;
}
weakMap.delete(v);
});
keyedMap.clear();
};
const remove = () => {
keyedMap.delete(key);
weakMap.delete(k);
storage.value = undefined;
};
return {
storage,
clear,
remove
};
}

export { debounce, exponentialDelay, noDelay, useArrayPagination, useAxios, useCancellablePromise, useDebounce, useEvent, useFetch, useLocalStorage, useOnMouseMove, useOnResize, useOnScroll, usePagination, usePromise, useRetry, useWebSocket };
Loading

0 comments on commit d0b0aeb

Please sign in to comment.