From 454d1ee070ffcbf6c7355c21b30f7f9de74a3d10 Mon Sep 17 00:00:00 2001 From: Dave Stewart Date: Thu, 17 Dec 2020 19:05:50 +0000 Subject: [PATCH] Support numeric keys on objects (#124) * Add check in setValue() to only change key to index if target is an Array * Fix bug when creating new objects when path specifies indices * Linting fixes * Update tests * Update version * Update changelog --- CHANGELOG.md | 5 + package-lock.json | 2 +- package.json | 4 +- src/helpers/component.js | 1 - src/helpers/modules.js | 2 +- src/helpers/store.js | 2 +- src/helpers/vuex.js | 4 +- src/services/resolver.js | 8 +- src/services/store.js | 2 +- src/services/wildcards.js | 4 +- src/utils/object.js | 46 ++++++-- tests/helpers/index.js | 16 +++ tests/helpers/pathify.js | 8 ++ tests/store-accessors.test.js | 192 +++++++++++++++++++--------------- tests/store-helpers.test.js | 18 ++-- 15 files changed, 191 insertions(+), 123 deletions(-) create mode 100644 tests/helpers/index.js create mode 100644 tests/helpers/pathify.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 62d460c..9b00ea2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [1.4.5] - 2020-12-17 +### Fixed +- Setting values on objects using numeric keys is now supported +- Setting deep properties using numeric keys now correctly creates arrays + ## [1.4.4] - 2020-12-02 ### Fixed - Fix Payload type - #101 / @VesterDe diff --git a/package-lock.json b/package-lock.json index e7b35cf..2847b5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "vuex-pathify", - "version": "1.4.2", + "version": "1.4.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index e61aa34..6b5c8b2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vuex-pathify", - "version": "1.4.4", + "version": "1.4.5", "description": "Ridiculously simple Vuex setup + wiring", "main": "dist/vuex-pathify.js", "module": "dist/vuex-pathify.esm.js", @@ -14,7 +14,7 @@ "dev": "rollup -c build/rollup.js -w", "build": "rollup -c build/rollup.js", "docs": "node-sass docs/assets/scss -o docs/assets/css -w docs/assets/scss/**/*.scss & docsify serve docs", - "test": "jest tests/", + "test": "jest tests/ --verbose", "test:types": "tsc -p types/test" }, "author": "Dave Stewart ", diff --git a/src/helpers/component.js b/src/helpers/component.js index 5f5f32f..f482b11 100644 --- a/src/helpers/component.js +++ b/src/helpers/component.js @@ -25,7 +25,6 @@ export function call (path, props) { }) } - // ------------------------------------------------------------------------------------------------------------------- // utility // ------------------------------------------------------------------------------------------------------------------- diff --git a/src/helpers/modules.js b/src/helpers/modules.js index a5fe268..e9f0a89 100644 --- a/src/helpers/modules.js +++ b/src/helpers/modules.js @@ -7,7 +7,7 @@ * @param {object} [options] Optional Vuex module registration options * @returns {object} The mixin */ -export function registerModule(path, module, callback, options) { +export function registerModule (path, module, callback, options) { return { beforeCreate () { this.$store.registerModule(path, module, options) diff --git a/src/helpers/store.js b/src/helpers/store.js index 3ecce65..98cafe8 100644 --- a/src/helpers/store.js +++ b/src/helpers/store.js @@ -58,7 +58,7 @@ export function makeActions (state) { .reduce(function (obj, key) { const action = resolveName('actions', key) const mutation = resolveName('mutations', key) - obj[action] = function ({commit}, value) { + obj[action] = function ({ commit }, value) { commit(mutation, value) } return obj diff --git a/src/helpers/vuex.js b/src/helpers/vuex.js index 9bea882..ca1fb6e 100644 --- a/src/helpers/vuex.js +++ b/src/helpers/vuex.js @@ -19,11 +19,11 @@ const vuex = { } } -export function commit(...args) { +export function commit (...args) { vuex.store.commit(...args) } -export function dispatch(...args) { +export function dispatch (...args) { return vuex.store.dispatch(...args) } diff --git a/src/services/resolver.js b/src/services/resolver.js index 808bf3b..16810c4 100644 --- a/src/services/resolver.js +++ b/src/services/resolver.js @@ -28,7 +28,7 @@ const resolvers = { * @returns {string} */ standard (type, name, formatters) { - switch(type) { + switch (type) { case 'mutations': return formatters.const('set', name) // SET_BAR case 'actions': @@ -165,11 +165,11 @@ export function resolve (store, path) { /** * Error generation function for accessors */ -export function getError(path, resolver, aName, a, bName, b) { +export function getError (path, resolver, aName, a, bName, b) { let error = `[Vuex Pathify] Unable to map path '${path}':` if (path.indexOf('!') > -1) { error += ` - - Did not find ${aName} or ${bName} named '${resolver.name}' on ${resolver.module ? `module '${resolver.module}'`: 'root store'}` + - Did not find ${aName} or ${bName} named '${resolver.name}' on ${resolver.module ? `module '${resolver.module}'` : 'root store'}` } else { const aText = a @@ -177,7 +177,7 @@ export function getError(path, resolver, aName, a, bName, b) { : '' const bText = `${bName} '${b.trgName}'` error += ` - - Did not find ${aText}${bText} on ${resolver.module ? `module '${resolver.module}'`: 'store'} + - Did not find ${aText}${bText} on ${resolver.module ? `module '${resolver.module}'` : 'store'} - Use direct syntax '${resolver.target.replace(/(@|$)/, '!$1')}' (if member exists) to target directly` } return error diff --git a/src/services/store.js b/src/services/store.js index c6dd576..4f85efa 100644 --- a/src/services/store.js +++ b/src/services/store.js @@ -103,7 +103,7 @@ export function makeGetter (store, path, stateOnly) { * @param {string} path The full dot-path on the source object * @returns {*} */ -function getValueIfEnabled(expr, source, path) { +function getValueIfEnabled (expr, source, path) { if (!options.deep && expr.indexOf('@') > -1) { console.error(`[Vuex Pathify] Unable to access sub-property for path '${expr}': - Set option 'deep' to 1 to allow it`) diff --git a/src/services/wildcards.js b/src/services/wildcards.js index f52a911..78f543d 100644 --- a/src/services/wildcards.js +++ b/src/services/wildcards.js @@ -1,4 +1,4 @@ -import { getValue} from '../utils/object' +import { getValue } from '../utils/object' // ------------------------------------------------------------------------------------------------------------------- // external @@ -50,7 +50,6 @@ export function expandCall (path, actions) { return resolveHandlers(path, actions) } - // ------------------------------------------------------------------------------------------------------------------- // internal // ------------------------------------------------------------------------------------------------------------------- @@ -102,7 +101,6 @@ export function resolveHandlers (path, hash) { return Object.keys(hash).filter(key => rx.test(key)) } - // ------------------------------------------------------------------------------------------------------------------- // utility // ------------------------------------------------------------------------------------------------------------------- diff --git a/src/utils/object.js b/src/utils/object.js index 1fca5a0..40c31e7 100644 --- a/src/utils/object.js +++ b/src/utils/object.js @@ -18,6 +18,16 @@ export function isObject (value) { return !!value && typeof value === 'object' } +/** + * Tests whether a string is numeric + * + * @param {string|number} value The value to be assessed + * @returns {boolean} + */ +export function isNumeric (value) { + return typeof value === 'number' || /^\d+$/.test(value) +} + /** * Tests whether a passed value is an Object and has the specified key * @@ -25,7 +35,7 @@ export function isObject (value) { * @param {string} key The key to check that exists * @returns {boolean} Whether the predicate is satisfied */ -export function hasKey(obj, key) { +export function hasKey (obj, key) { return isObject(obj) && key in obj } @@ -83,25 +93,38 @@ export function getValue (obj, path) { */ export function setValue (obj, path, value, create = false) { const keys = getKeys(path) - return keys.reduce((obj, key, index) => { - const isIndex = /^\d+$/.test(key) - if (isIndex) { - key = parseInt(key) - } + return keys.reduce((obj, key, index) => { + // early return if no object if (!obj) { return false } - else if (index === keys.length - 1) { + + // convert key to index if obj is an array and key is numeric + if (Array.isArray(obj) && isNumeric(key)) { + key = parseInt(key) + } + + // if we're at the end of the path, set the value + if (index === keys.length - 1) { obj[key] = value return true } + + // if the target property doesn't exist... else if (!isObject(obj[key]) || !(key in obj)) { + // ...create one, or cancel if (create) { - obj[key] = isIndex ? [] : {} - } else { + // create object or array, depending on next key + obj[key] = isNumeric(keys[index + 1]) + ? [] + : {} + } + else { return false } } + + // if we get here, return the target property return obj[key] }, obj) } @@ -113,14 +136,15 @@ export function setValue (obj, path, value, create = false) { * @param {string|Array|Object} path The path to a sub-property * @returns {boolean} Boolean true or false */ -export function hasValue(obj, path) { +export function hasValue (obj, path) { let keys = getKeys(path) if (isObject(obj)) { while (keys.length) { let key = keys.shift() if (hasKey(obj, key)) { obj = obj[key] - } else { + } + else { return false } } diff --git a/tests/helpers/index.js b/tests/helpers/index.js new file mode 100644 index 0000000..7f3664d --- /dev/null +++ b/tests/helpers/index.js @@ -0,0 +1,16 @@ +import Vue from 'vue'; +import Vuex from 'vuex' +import { make } from '../../src/main' +import pathify from './pathify' + +Vue.use(Vuex) + +export function makeStore (store) { + if (!store.mutations) { + store.mutations = make.mutations(store.state) + } + return new Vuex.Store({ + plugins: [pathify.plugin], + ...store + }) +} diff --git a/tests/helpers/pathify.js b/tests/helpers/pathify.js new file mode 100644 index 0000000..085ef8d --- /dev/null +++ b/tests/helpers/pathify.js @@ -0,0 +1,8 @@ +import pathify from '../../src/main' + +// options +pathify.options.mapping = 'simple' +pathify.options.deep = 2 + +// export +export default pathify diff --git a/tests/store-accessors.test.js b/tests/store-accessors.test.js index 2d29c5c..ae80dde 100644 --- a/tests/store-accessors.test.js +++ b/tests/store-accessors.test.js @@ -1,109 +1,133 @@ -import Vue from 'vue'; -import Vuex from 'vuex'; -import pathify, { make } from '../src/main'; +import { make } from '../src/main'; +import { makeStore } from './helpers' + +describe('top-level state', () => { + it('should get state', () => { + const state = { name: 'Jack', age: 28 } + const store = makeStore({ + state, + }) + + expect(store.get('name')).toEqual('Jack') + expect(store.get('age')).toEqual(28) + }) -Vue.use(Vuex) + it('should set state', () => { + const state = { name: 'Jack', age: 28 } + const store = makeStore({ state }) -it('can get state', () => { - const state = { name: 'Jack', age: 28 } + store.set('name', 'Jill') - const store = new Vuex.Store({ - plugins: [pathify.plugin], - state, + expect(store.state.name).toEqual('Jill') }) - - expect(store.get('name')).toEqual('Jack') - expect(store.get('age')).toEqual(28) }) -it('can get nested state', () => { - const state = { - person: { - name: 'Jack', - age: 28, - pets: [{ - animal: 'cat', - name: 'Tabby', - }], - }, - } - - const store = new Vuex.Store({ - plugins: [pathify.plugin], - state, +describe('nested state', function () { + it('should get state', () => { + const state = { + person: { + name: 'Jack', + age: 28, + pets: [{ + animal: 'cat', + name: 'Tabby', + }], + }, + } + const store = makeStore({ + state, + }) + + expect(store.get('person@name')).toEqual('Jack') + expect(store.get('person@age')).toEqual(28) + expect(store.get('person@pets@[0].animal')).toEqual('cat') + expect(store.get('person@pets@[0].name')).toEqual('Tabby') }) - expect(store.get('person@name')).toEqual('Jack') - expect(store.get('person@age')).toEqual(28) - expect(store.get('person@pets@[0].animal')).toEqual('cat') - expect(store.get('person@pets@[0].name')).toEqual('Tabby') -}) + it('should set state', () => { + const state = { + person: { + name: 'Jack', + age: 28, + pets: [{ + animal: 'cat', + name: 'Tabby', + }], + }, + } + const store = makeStore({ state }) -it('can get module state', () => { - const state = { name: 'Jack', age: 28 } + store.set('person@name', 'Jill') + store.set('person@pets[0].name', 'Spot') - const store = new Vuex.Store({ - plugins: [pathify.plugin], - modules: { - people: { namespaced: true, state } - } + expect(store.state.person.name).toEqual('Jill') + expect(store.state.person.pets[0].name).toEqual('Spot') }) - - expect(store.get('people/name')).toEqual('Jack') - expect(store.get('people/age')).toEqual(28) }) -it('can set state', () => { - const state = { name: 'Jack', age: 28 } - const mutations = make.mutations(state) - const store = new Vuex.Store({ - plugins: [pathify.plugin], - state, - mutations, +describe('module state', function () { + it('should get state', () => { + const state = { name: 'Jack', age: 28 } + const store = makeStore({ + modules: { + people: { namespaced: true, state } + } + }) + + expect(store.get('people/name')).toEqual('Jack') + expect(store.get('people/age')).toEqual(28) }) - store.set('name', 'Jill') + it('should set state', () => { + const state = { name: 'Jack', age: 28 } + const mutations = make.mutations(state) + const store = makeStore({ + modules: { + people: { namespaced: true, state, mutations } + } + }) - expect(store.state.name).toEqual('Jill') -}) + store.set('people/name', 'Jill') -it('can set nested state', () => { - const state = { - person: { - name: 'Jack', - age: 28, - pets: [{ - animal: 'cat', - name: 'Tabby', - }], - }, - } - const mutations = make.mutations(state) - const store = new Vuex.Store({ - plugins: [pathify.plugin], - state, - mutations, + expect(store.state.people.name).toEqual('Jill') }) - - store.set('person@name', 'Jill') - store.set('person@pets[0].name', 'Spot') - - expect(store.state.person.name).toEqual('Jill') - expect(store.state.person.pets[0].name).toEqual('Spot') }) -it('can set module state', () => { - const state = { name: 'Jack', age: 28 } - const mutations = make.mutations(state) - const store = new Vuex.Store({ - plugins: [pathify.plugin], - modules: { - people: { namespaced: true, state, mutations } - } +describe('special functionality', function () { + describe('key types', () => { + const state = { object: {}, array: [] } + const store = makeStore({ state }) + + it('alpha keys - should set a key on an object', function () { + store.set('object@a1', 1) + expect(store.state.object['a1']).toEqual(1) + }) + + it('numeric keys - should set a key on an object', function () { + store.set('object@1a', 1) + expect(store.state.object['1a']).toEqual(1) + }) + + it('numeric keys - should set an index on an array', function () { + store.set('array@0', 1) + expect(store.state.array[0]).toEqual(1) + }) }) - store.set('people/name', 'Jill') + describe('object creation', () => { + const state = { target: {} } + const mutations = make.mutations(state) + const store = makeStore({ state, mutations }) + + it('should create empty objects', function () { + store.set('target@value', 100) + expect(store.state.target.value).toEqual(100) + }) - expect(store.state.people.name).toEqual('Jill') + it('should create empty arrays', function () { + store.set('target@matrix.0.0', 100) + expect(store.state.target.matrix[0][0]).toEqual(100) + }) + }) }) diff --git a/tests/store-helpers.test.js b/tests/store-helpers.test.js index b6a3d84..140c62e 100644 --- a/tests/store-helpers.test.js +++ b/tests/store-helpers.test.js @@ -1,22 +1,16 @@ -import Vue from 'vue' -import Vuex from 'vuex' +import { make } from '../src/main' +import { makeStore } from './helpers' -import pathify, { make } from '../src/main' - -Vue.use(Vuex) - -it('can make mutations', () => { +it('should make mutations', () => { const state = { name: 'Jack', age: 28 } const mutations = make.mutations(state) - - const store = new Vuex.Store({ - plugins: [pathify.plugin], + const store = makeStore({ state, mutations, }) - store.commit('SET_AGE', 30) - store.commit('SET_NAME', 'Jill') + store.commit('age', 30) + store.commit('name', 'Jill') expect(store.state.name).toEqual('Jill') expect(store.state.age).toEqual(30) })