Skip to content

Commit

Permalink
Add an initialStateFromStorage method
Browse files Browse the repository at this point in the history
  • Loading branch information
ultraq committed Jul 12, 2020
1 parent a741b52 commit 512ec07
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 28 deletions.
2 changes: 1 addition & 1 deletion .babelrc
Expand Up @@ -27,7 +27,7 @@
"presets": [
["@babel/preset-env", {
"targets": {
"node": "10.0.0"
"node": "current"
}
}]
]
Expand Down
36 changes: 36 additions & 0 deletions README.md
Expand Up @@ -20,6 +20,11 @@ npm install @ultraq/redux-utils
API
---

### backedByStorage(reducer, storage, key)

A convenience method for composing a reducer with the [`initiatedFromStorage`](#initiatedfromstoragereducer-storage-key)
and [`syncedToStorage`](#syncedtostoragereducer-storage-key) functions together.

### initialStateFromDom(selector, [slice])

Create an initial state from JSON data in a DOM element. Used for creating an
Expand All @@ -33,6 +38,24 @@ could be read.
state, then specify the name of the slice so that it can be set in the right
place.

### initiatedFromStorage(reducer, storage, key)

Wraps and returns a reducer that has an initial state from the given storage
mechanism.

This initial load is also deferred until the reducer is first used, which helps
with tests using ES6 modules that would normally have, by their static nature,
caused the reducer initial state to be run before the tests are even executed.

- **reducer**: The redux reducer to have its initial state come from a JSON
string in the given storage mechanism.
- **storage**: One of the browser's `sessionStorage` or `localStorage`, or any
object that implements the `Storage` interface.
- **key**: The key mapped to the data used to load this reducer from. It is
recommended that this key is combined with something that uniquely identifies
the current context so that the same state from different uses of the page
don't clash.

### observe(store, select, handler)

Observe the store for changes, passing the value picked out by the `select`
Expand All @@ -56,3 +79,16 @@ then given to the handler function.
that is to be observed for changes
- **handler**: the function that, when the value picked out by `select` changes,
is invoked with the changed value

### syncedToStorage(reducer, storage, key)

Wraps and returns a reducer that saves the state from the original reducer to
the browser's local storage. The storage is mapped by the given keys.

- **reducer**: The redux reducer to have its state saved to a given storage
mechanism whenever that state changes.
- **storage**: One of the browser's `sessionStorage` or `localStorage`, or any
object that implements the `Storage` interface.
- **key**: The key to use for storing this data. It is recommended that this
key is combined with something that uniquely identifies the current context
so that the same state from different uses of the page don't clash.
38 changes: 19 additions & 19 deletions __mocks__/@ultraq/dom-utils.js
Expand Up @@ -13,26 +13,26 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/

const mockDomUtils = jest.genMockFromModule('@ultraq/dom-utils');

/* eslint-env jest, node */
let mockJson = null;

/**
* Mock and return the value of future calls to the `parseJsonFromElement`
* function.
*
* @param {Object} data
* @return {Object}
* @private
*/
mockDomUtils.__setMockJsonFromElement = function(data) {
mockJson = data;
return data;
}
module.exports = {
...jest.genMockFromModule('@ultraq/dom-utils'),

mockDomUtils.parseJsonFromElement = function(selector) {
return mockJson;
}
/**
* Mock and return the value of future calls to the `parseJsonFromElement`
* function.
*
* @param {Object} data
* @return {Object}
* @private
*/
__setMockJsonFromElement(data) {
mockJson = data;
return data;
},

module.exports = mockDomUtils;
parseJsonFromElement(selector) {
return mockJson;
}
};
3 changes: 3 additions & 0 deletions jest.config.js
Expand Up @@ -7,5 +7,8 @@ module.exports = {
'html',
'lcov',
'text-summary'
],
setupFilesAfterEnv: [
'mock-local-storage'
]
};
49 changes: 48 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions package.json
@@ -1,6 +1,6 @@
{
"name": "@ultraq/redux-utils",
"version": "0.3.0",
"version": "0.4.0",
"description": "A collection of wrappers/utilities for common functions in redux",
"author": "Emanuel Rabina <emanuelrabina@gmail.com> (http://www.ultraq.net.nz/)",
"license": "Apache-2.0",
Expand All @@ -11,13 +11,17 @@
"keywords": [
"redux",
"utilities",
"observe"
"initialState",
"sessionStorage",
"localStorage",
"observe",
"observeOnce"
],
"module": "redux-utils.es.js",
"main": "redux-utils.cjs.js",
"scripts": {
"lint": "eslint \"*.js\"",
"test": "jest",
"test": "npm run lint && jest",
"coverage": "cat ./coverage/lcov.info | coveralls",
"build": "npm run build:cjs && npm run build:es",
"build:cjs": "BABEL_ENV=cjs babel redux-utils.js --out-file redux-utils.cjs.js --source-maps",
Expand All @@ -39,6 +43,7 @@
"eslint": "^6.8.0",
"eslint-config-ultraq": "^2.3.3",
"jest": "^25.5.4",
"mock-local-storage": "^1.1.12",
"redux": "^4.0.0"
},
"engines": {
Expand Down
28 changes: 25 additions & 3 deletions redux-utils.js
Expand Up @@ -33,11 +33,35 @@ import {equals} from '@ultraq/object-utils';
* be read.
*/
export function initialStateFromDom(selector, slice) {

let data = parseJsonFromElement(selector);
return data ? slice ? {[slice]: data} : data : {};
}

/**
* Create an initial state from JSON data in session or local storage. Used for
* creating an object that is suitable for the `initialState` value of Redux's
* `createStore`.
*
* @param {Storage} storage
* The storage mechanism to use, either `sessionStorage` or `localStorage`.
* @param {String} key
* The key in storage from which to get the data from.
* @param {String} [slice]
* If the JSON data only represents a slice of the entire state, then specify
* the name of the slice so that it can be set in the right place.
* @return {Object}
* The JSON data converted to an object, or an empty object if no data could
* be read.
*/
export function initialStateFromStorage(storage, key, slice) {
let item = storage.getItem(key);
if (item) {
let data = JSON.parse(item);
return slice ? { [slice]: data } : data;
}
return {};
}

/**
* Observe the store for changes, passing the value picked out by the `select`
* function to the handler.
Expand All @@ -51,7 +75,6 @@ export function initialStateFromDom(selector, slice) {
* A function that lets the observer unsubscribe from store changes.
*/
export function observe(store, select, handler) {

let currentValue = select(store.getState());
return store.subscribe(function() {
let nextValue = select(store.getState());
Expand All @@ -72,7 +95,6 @@ export function observe(store, select, handler) {
* @param {Function} handler
*/
export function observeOnce(store, select, handler) {

let unsubscribe = observe(store, select, function(value) {
if (value) {
unsubscribe();
Expand Down
35 changes: 34 additions & 1 deletion redux-utils.test.js
Expand Up @@ -18,6 +18,7 @@
import {__setMockJsonFromElement} from '@ultraq/dom-utils';
import {
initialStateFromDom,
initialStateFromStorage,
observe,
observeOnce
} from './redux-utils.js';
Expand Down Expand Up @@ -77,6 +78,39 @@ describe('redux-utils', function() {
});
});

describe('#initialStateFromStorage', function() {
const key = 'test';

afterEach(function() {
localStorage.clear();
});

test('JSON data loaded', function() {
let testData = {
message: 'Hello!'
};
localStorage.setItem(key, JSON.stringify(testData));
let result = initialStateFromStorage(localStorage, key);
expect(result).toEqual(testData);
});

test('JSON data loaded into slice', function() {
let testData = {
message: 'Hello!'
};
localStorage.setItem(key, JSON.stringify(testData));
let result = initialStateFromStorage(localStorage, key, 'slice');
expect(result).toEqual({
slice: testData
});
});

test('Empty object returned when item not present', function() {
let result = initialStateFromStorage(localStorage, key);
expect(result).toEqual({});
});
});

describe('#observe', function() {

test('All changes are passed along to the handler', function() {
Expand Down Expand Up @@ -170,5 +204,4 @@ describe('redux-utils', function() {
expect(handler).toHaveBeenCalledTimes(1);
});
});

});

0 comments on commit 512ec07

Please sign in to comment.