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/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",
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 38811244..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()`, () => {
@@ -38,12 +44,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 +63,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),
},
};
@@ -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,18 +175,30 @@ 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.`, () => {
const helpers = createHelpers({ getterType: `getFoo`, mutationType: `updateFoo` });
const expectedResult = {
foo: {
- get: expect.anything(),
- set: expect.anything(),
+ get: expect.any(Function),
+ set: expect.any(Function),
},
};
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`);
+ });
+});