From a00750f1174c19dbc6b02e287668dd037734cceb Mon Sep 17 00:00:00 2001 From: Markus Oberlehner Date: Sun, 28 Jan 2018 09:14:31 +0100 Subject: [PATCH 1/3] Be more specific when checking return values Check if the given value is a function instead of anything. --- src/index.spec.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/index.spec.js b/src/index.spec.js index 38811244..9c1133b1 100644 --- a/src/index.spec.js +++ b/src/index.spec.js @@ -38,12 +38,12 @@ describe(`index`, () => { }; const expectedResult = { foo: { - get: expect.anything(), - set: expect.anything(), + get: expect.any(Function), + set: expect.any(Function), }, bar: { - get: expect.anything(), - set: expect.anything(), + get: expect.any(Function), + set: expect.any(Function), }, }; @@ -57,12 +57,12 @@ describe(`index`, () => { ]; const expectedResult = { foo: { - get: expect.anything(), - set: expect.anything(), + get: expect.any(Function), + set: expect.any(Function), }, bar: { - get: expect.anything(), - set: expect.anything(), + get: expect.any(Function), + set: expect.any(Function), }, }; @@ -110,8 +110,8 @@ describe(`index`, () => { const helpers = createHelpers({ getterType: `getFoo`, mutationType: `updateFoo` }); const expectedResult = { foo: { - get: expect.anything(), - set: expect.anything(), + get: expect.any(Function), + set: expect.any(Function), }, }; From eb7945e57bc1e2bb5b33e4671f7c238dbf4fdb90 Mon Sep 17 00:00:00 2001 From: Markus Oberlehner Date: Sun, 28 Jan 2018 09:38:25 +0100 Subject: [PATCH 2/3] Implement multi-row field mapping Multi-row field mapping makes it possible to map an array of field objects to vuex compatible getters and setters. See #7 Fixes #8 --- README.md | 57 ++++++++++++++++++++++++++++ src/index.js | 33 ++++++++++++++++ src/index.spec.js | 85 +++++++++++++++++++++++++++++++++++++++++- test/multi-row.test.js | 77 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 test/multi-row.test.js diff --git a/README.md b/README.md index a9f7e2a7..5d919b8c 100644 --- a/README.md +++ b/README.md @@ -350,6 +350,63 @@ export default { ``` +### Multi-row fields +If you want to build a form which allows the user to enter multiple rows of a specific data type with multiple fields (e.g. multiple addresses) you can use the multi-row field mapping function. + +#### Store +```js +import Vue from 'vue'; +import Vuex from 'vuex'; + +import { getField, updateField } from 'vuex-map-fields'; + +Vue.use(Vuex); + +export default new Vuex.Store({ + // ... + state: { + addresses: [ + { + zip: `12345`, + town: `Foo Town`, + }, + { + zip: `54321`, + town: `Bar Town`, + }, + ], + }, + getters: { + getField, + }, + mutations: { + updateField, + }, +}); +``` + +#### Component +```html + + + +``` + ## Upgrade from 0.x.x to 1.x.x Instead of accessing the state directly, since the 1.0.0 release, in order to enable the ability to implement custom getters and mutations, `vuex-map-fields` is using a getter function to access the state. This makes it necessary to add a getter function to your Vuex store. diff --git a/src/index.js b/src/index.js index e7938a5d..859509f9 100644 --- a/src/index.js +++ b/src/index.js @@ -36,8 +36,41 @@ export function mapFields(fields, getterType = `getField`, mutationType = `updat }, {}); } +export function mapMultiRowFields(paths, getterType = `getField`, mutationType = `updateField`) { + const pathsObject = Array.isArray(paths) ? arrayToObject(paths) : paths; + + return Object.keys(pathsObject).reduce((entries, key) => { + const path = pathsObject[key]; + + // eslint-disable-next-line no-param-reassign + entries[key] = { + get() { + const store = this.$store; + const rows = store.getters[getterType](path); + + return rows.map((fieldsObject, index) => + Object.keys(fieldsObject).reduce((prev, fieldKey) => { + const fieldPath = `${path}[${index}].${fieldKey}`; + + return Object.defineProperty(prev, fieldKey, { + get() { + return store.getters[getterType](fieldPath); + }, + set(value) { + store.commit(mutationType, { path: fieldPath, value }); + }, + }); + }, {})); + }, + }; + + return entries; + }, {}); +} + export const createHelpers = ({ getterType, mutationType }) => ({ [getterType]: getField, [mutationType]: updateField, mapFields: fields => mapFields(fields, getterType, mutationType), + mapMultiRowFields: paths => mapMultiRowFields(paths, getterType, mutationType), }); diff --git a/src/index.spec.js b/src/index.spec.js index 9c1133b1..afd536c6 100644 --- a/src/index.spec.js +++ b/src/index.spec.js @@ -1,4 +1,10 @@ -import { createHelpers, getField, mapFields, updateField } from './'; +import { + createHelpers, + getField, + mapFields, + mapMultiRowFields, + updateField, +} from './'; describe(`index`, () => { describe(`getField()`, () => { @@ -93,6 +99,71 @@ describe(`index`, () => { }); }); + describe(`mapMultiRowFields()`, () => { + test(`It should be possible to re-map the initial path.`, () => { + const expectedResult = { + otherFieldRows: { get: expect.any(Function) }, + }; + + expect(mapMultiRowFields({ otherFieldRows: `fieldRows` })).toEqual(expectedResult); + }); + + test(`It should get the value of a property via the \`getField()\` function.`, () => { + const mockFieldRows = [ + { + foo: `Foo`, + bar: `Bar`, + }, + { + foo: `Foo`, + bar: `Bar`, + }, + ]; + const mockGetField = jest.fn().mockReturnValue(mockFieldRows); + const mappedFieldRows = mapMultiRowFields([`fieldRows`]); + + const getterSetters = mappedFieldRows.fieldRows.get.apply({ + $store: { getters: { getField: mockGetField } }, + }); + + // eslint-disable-next-line no-unused-vars + const x = getterSetters[0].bar; // Trigger getter function. + expect(mockGetField).lastCalledWith(`fieldRows[0].bar`); + + // eslint-disable-next-line no-unused-vars + const y = getterSetters[1].foo; // Trigger getter function. + expect(mockGetField).lastCalledWith(`fieldRows[1].foo`); + }); + + test(`It should commit new values to the store.`, () => { + const mockFieldRows = [ + { + foo: `Foo`, + bar: `Bar`, + }, + { + foo: `Foo`, + bar: `Bar`, + }, + ]; + const mockCommit = jest.fn(); + const mappedFieldRows = mapMultiRowFields([`fieldRows`]); + + const getterSetters = mappedFieldRows.fieldRows.get.apply({ + $store: { + getters: { getField: () => mockFieldRows }, + commit: mockCommit, + }, + }); + + getterSetters[0].bar = `New Bar`; // Trigger setter function. + expect(mockCommit).toBeCalledWith(`updateField`, { path: `fieldRows[0].bar`, value: `New Bar` }); + + getterSetters[1].foo = `New Foo`; // Trigger setter function. + expect(mockCommit).toBeCalledWith(`updateField`, { path: `fieldRows[1].foo`, value: `New Foo` }); + }); + }); + describe(`createHelpers()`, () => { test(`It should be a function.`, () => { expect(typeof createHelpers).toBe(`function`); @@ -104,6 +175,7 @@ describe(`index`, () => { expect(typeof helpers.getFoo).toBe(`function`); expect(typeof helpers.updateFoo).toBe(`function`); expect(typeof helpers.mapFields).toBe(`function`); + expect(typeof helpers.mapMultiRowFields).toBe(`function`); }); test(`It should call the \`mapFields()\` function with the provided getter and mutation types.`, () => { @@ -117,5 +189,16 @@ describe(`index`, () => { expect(helpers.mapFields([`foo`])).toEqual(expectedResult); }); + + test(`It should call the \`mapMultiRowFields()\` function with the provided getter and mutation types.`, () => { + const helpers = createHelpers({ getterType: `getFoo`, mutationType: `updateFoo` }); + const expectedResult = { + foo: { + get: expect.any(Function), + }, + }; + + expect(helpers.mapMultiRowFields([`foo`])).toEqual(expectedResult); + }); }); }); diff --git a/test/multi-row.test.js b/test/multi-row.test.js new file mode 100644 index 00000000..dffd6347 --- /dev/null +++ b/test/multi-row.test.js @@ -0,0 +1,77 @@ +import Vuex from 'vuex'; +import { createLocalVue, shallow } from 'vue-test-utils'; + +import { mapMultiRowFields, getField, updateField } from '../src'; + +const localVue = createLocalVue(); + +localVue.use(Vuex); + +describe(`Component initialized with multi row setup.`, () => { + let Component; + let store; + let wrapper; + + beforeEach(() => { + Component = { + template: ` +
+
+ + +
+
+ `, + computed: { + ...mapMultiRowFields([`users`]), + }, + }; + + store = new Vuex.Store({ + state: { + users: [ + { + name: `Foo`, + email: `foo@foo.com`, + }, + { + name: `Bar`, + email: `bar@bar.com`, + }, + ], + }, + getters: { + getField, + }, + mutations: { + updateField, + }, + }); + + wrapper = shallow(Component, { localVue, store }); + }); + + test(`It should render the component.`, () => { + expect(wrapper.exists()).toBe(true); + }); + + test(`It should update field values when the store is updated.`, () => { + store.state.users[0].name = `New Name`; + store.state.users[1].email = `new@email.com`; + wrapper.update(); + + expect(wrapper.find(`input`).element.value).toBe(`New Name`); + expect(wrapper.find(`div:nth-child(2) input:nth-child(2)`).element.value).toBe(`new@email.com`); + }); + + test(`It should update the store when the field values are updated.`, () => { + wrapper.find(`input`).element.value = `New Name`; + wrapper.find(`input`).trigger(`input`); + + wrapper.find(`div:nth-child(2) input:nth-child(2)`).element.value = `new@email.com`; + wrapper.find(`div:nth-child(2) input:nth-child(2)`).trigger(`input`); + + expect(store.state.users[0].name).toBe(`New Name`); + expect(store.state.users[1].email).toBe(`new@email.com`); + }); +}); From 132e869c655d3840eedd94523d4105641d9d677d Mon Sep 17 00:00:00 2001 From: Markus Oberlehner Date: Sun, 28 Jan 2018 09:41:44 +0100 Subject: [PATCH 3/3] Bump version number to 1.1.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7f116229..2703c5d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "vuex-map-fields", - "version": "1.0.2", + "version": "1.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a1411259..dae25d53 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vuex-map-fields", - "version": "1.0.2", + "version": "1.1.0", "description": "Enable two-way data binding for form fields saved in a Vuex store", "keywords": [ "vue",