├── .eslintrc ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── package.json └── src ├── __tests__ └── filter-test.js └── filter.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb/base", 3 | "env": { 4 | "mocha": true 5 | }, 6 | "globals": { 7 | "assert": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Elger Lambert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN=node_modules/.bin 2 | 3 | MOCHA_ARGS= --compilers js:babel/register \ 4 | --recursive 5 | 6 | MOCHA_TARGET=src/__tests__ 7 | 8 | build: 9 | $(BIN)/babel src --ignore __tests__ --out-dir lib 10 | 11 | clean: 12 | rm -rf lib 13 | 14 | test: lint 15 | NODE_ENV=test $(BIN)/mocha $(MOCHA_ARGS) $(MOCHA_TARGET) 16 | 17 | test-watch: lint 18 | NODE_ENV=test $(BIN)/mocha $(MOCHA_ARGS) -w $(MOCHA_TARGET) 19 | 20 | lint: 21 | $(BIN)/eslint src 22 | 23 | PHONY: build clean test test-watch lint 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux-localstorage-filter 2 | Storage enhancer to persist a subset of your state. 3 | 4 | [![license](https://img.shields.io/npm/l/redux-localstorage-filter.svg?style=flat-square)](https://www.npmjs.com/package/redux-localstorage-filter) 5 | [![npm version](https://img.shields.io/npm/v/redux-localstorage-filter.svg?style=flat-square)](https://www.npmjs.com/package/redux-localstorage-filter) 6 | [![npm downloads](https://img.shields.io/npm/dm/redux-localstorage-filter.svg?style=flat-square)](https://www.npmjs.com/package/redux-localstorage-filter) 7 | 8 | ## Installation 9 | ```js 10 | npm install --save redux-localstorage-filter 11 | ``` 12 | 13 | ## Usage 14 | ```js 15 | const storage = compose( 16 | filter('nested.key'), 17 | )(adapter(localStorage)); 18 | ``` 19 | For more information on using storage enhancers check out [redux-localstorage](elgerlambert/redux-localstorage) 20 | 21 | ## API 22 | ### filter(paths) 23 | ```js 24 | type paths = Array | String 25 | ``` 26 | A path is a `string` that points to the property key of your redux store that you would like to persist. In order to specify a nested key separate the keys that make up the full path with fullstops (`.`). Specify multiple paths by passing in an `Array`: 27 | 28 | ```js 29 | filter(['key', 'another.key']), 30 | ``` 31 | 32 | ## License 33 | MIT 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-localstorage-filter", 3 | "version": "0.1.1", 4 | "description": "Storage enhancer to persist a subset of your state.", 5 | "main": "lib/filter.js", 6 | "scripts": { 7 | "prepublish": "make test clean build", 8 | "test": "make test" 9 | }, 10 | "files": [ 11 | "lib/", 12 | "package.json", 13 | "README.md" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/elgerlambert/redux-localstorage-filter.git" 18 | }, 19 | "keywords": [ 20 | "redux", 21 | "redux-localstorage", 22 | "storage enhancer", 23 | "filter" 24 | ], 25 | "author": "Elger Lambert ", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/elgerlambert/redux-localstorage-filter/issues" 29 | }, 30 | "homepage": "https://github.com/elgerlambert/redux-localstorage-filter#readme", 31 | "devDependencies": { 32 | "babel": "^5.8.23", 33 | "babel-core": "^5.8.23", 34 | "babel-eslint": "^4.1.0", 35 | "chai": "^3.2.0", 36 | "eslint": "^1.3.1", 37 | "eslint-config-airbnb": "0.0.8", 38 | "mocha": "^2.2.5" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/__tests__/filter-test.js: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {getSubset} from '../filter.js'; 3 | 4 | describe('filter', () => { 5 | const obj = { 6 | a: 1, 7 | b: 2, 8 | nested: { 9 | x: 3, 10 | deeply: { 11 | y: 4, 12 | z: 5, 13 | }, 14 | }, 15 | c: 0, 16 | d: false, 17 | }; 18 | 19 | it('should return the original object if no paths are defined', () => { 20 | const subset = getSubset(obj); 21 | assert.deepEqual(subset, obj); 22 | }); 23 | 24 | it('should return only the keys defined by paths', () => { 25 | const paths = ['a', 'b']; 26 | const subset = getSubset(obj, paths); 27 | assert.deepEqual(subset, {a: 1, b: 2}); 28 | }); 29 | 30 | it('should ignore paths with no value', () => { 31 | const paths = ['a', 'undefinedKey']; 32 | const subset = getSubset(obj, paths); 33 | assert.deepEqual(subset, {a: 1}); 34 | }); 35 | 36 | it('should not ignore values false or 0', () => { 37 | const paths = ['c', 'd']; 38 | const subset = getSubset(obj, paths); 39 | assert.deepEqual(subset, {c: 0, d: false}); 40 | }); 41 | 42 | describe('works with nested objects', () => { 43 | it('should return only the nested keys defined by paths', () => { 44 | const paths = ['nested.deeply.y']; 45 | const subset = getSubset(obj, paths); 46 | assert.deepEqual(subset, {nested: {deeply: {y: 4}}}); 47 | }); 48 | 49 | it('should ignore deeply nested paths with no value', () => { 50 | const paths = ['nested.x', 'nested.deeply.undefinedKey']; 51 | const subset = getSubset(obj, paths); 52 | assert.deepEqual(subset, {nested: {x: 3}}); 53 | }); 54 | 55 | it('should merge paths that share a parent', () => { 56 | const paths = ['nested.x', 'nested.deeply.y', 'b']; 57 | const subset = getSubset(obj, paths); 58 | assert.deepEqual(subset, {nested: {x: 3, deeply: {y: 4}}, b: 2}); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/filter.js: -------------------------------------------------------------------------------- 1 | function hasValue(value) { 2 | return !!value 3 | || value === 0 4 | || value === false; 5 | } 6 | 7 | /** 8 | * @description 9 | * getSubset returns an object with the same structure as the original object passed in, but contains 10 | * only the specified paths and only if those paths have "value" (truth-y values, 0 or false). 11 | * 12 | * @param {Object} obj The object from which to create a subset. 13 | * @param {String[]} paths An array of paths e.g. ['deeply.nested.key'], that should be included in the subset. 14 | * 15 | * @returns {Object} An object that contains only the specified paths if they hold something of value. 16 | */ 17 | export function getSubset(obj, paths) { 18 | if (!paths) return obj; 19 | 20 | const subset = {}; 21 | 22 | paths.forEach(path => { 23 | const keys = path.split('.'); 24 | const length = keys.length; 25 | const lastIndex = length - 1; 26 | 27 | let index = 0; 28 | let value = obj; 29 | let nested = subset; 30 | 31 | // Retrieve value specified by path 32 | while (value && index < length) { 33 | value = value[keys[index++]]; 34 | } 35 | 36 | // Add to subset if the specified path is defined and hasValue 37 | if (index === length && hasValue(value)) { 38 | keys.forEach((key, i) => { 39 | if (i === lastIndex) { 40 | nested[key] = value; 41 | } else if (!nested[key]) { 42 | nested[key] = {}; 43 | } 44 | nested = nested[key]; 45 | }); 46 | } 47 | }); 48 | 49 | return subset; 50 | } 51 | 52 | export default function filter(paths) { 53 | const finalPaths = typeof paths === 'string' 54 | ? [paths] 55 | : paths; 56 | 57 | return (storage) => ({ 58 | ...storage, 59 | put: (key, state, callback) => { 60 | storage.put(key, getSubset(state, finalPaths), callback); 61 | }, 62 | }); 63 | } 64 | --------------------------------------------------------------------------------