Skip to content

Commit

Permalink
Merge branch 'release/1.1.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
maoberlehner committed Jan 28, 2018
2 parents 1e280fd + 132e869 commit 2ab835b
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 13 deletions.
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,63 @@ export default {
</script>
```

### 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
<template>
<div id="app">
<div v-for="address in addresses">
<label>ZIP <input v-model="address.zip"></label>
<label>Town <input v-model="address.town"></label>
</div>
</div>
</template>

<script>
import { mapMultiRowFields } from 'vuex-map-fields';
export default {
computed: {
...mapMultiRowFields(['addresses']),
},
};
</script>
```

## 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.

Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
33 changes: 33 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
});
105 changes: 94 additions & 11 deletions src/index.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { createHelpers, getField, mapFields, updateField } from './';
import {
createHelpers,
getField,
mapFields,
mapMultiRowFields,
updateField,
} from './';

describe(`index`, () => {
describe(`getField()`, () => {
Expand Down Expand Up @@ -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),
},
};

Expand All @@ -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),
},
};

Expand Down Expand Up @@ -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`);
Expand All @@ -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);
});
});
});
77 changes: 77 additions & 0 deletions test/multi-row.test.js
Original file line number Diff line number Diff line change
@@ -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: `
<div>
<div v-for="user in users">
<input v-model="user.name">
<input v-model="user.email">
</div>
</div>
`,
computed: {
...mapMultiRowFields([`users`]),
},
};

store = new Vuex.Store({
state: {
users: [
{
name: `Foo`,
email: `[email protected]`,
},
{
name: `Bar`,
email: `[email protected]`,
},
],
},
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 = `[email protected]`;
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(`[email protected]`);
});

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 = `[email protected]`;
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(`[email protected]`);
});
});

0 comments on commit 2ab835b

Please sign in to comment.