Skip to content

Commit 4865f84

Browse files
committed
Add mapObjectFields() to add object properties using template e.g. 'property.*'
1 parent 65842ca commit 4865f84

File tree

5 files changed

+1023
-1
lines changed

5 files changed

+1023
-1
lines changed

src/index.js

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ function normalizeNamespace(fn) {
1818
}
1919

2020
export function getField(state) {
21-
return path => path.split(/[.[\]]+/).reduce((prev, key) => prev[key], state);
21+
return path => {
22+
if (path == '') {
23+
return state;
24+
} else {
25+
return path.split(/[.[\]]+/).reduce((prev, key) => prev[key], state);
26+
}
27+
}
2228
}
2329

2430
export function updateField(state, { path, value }) {
@@ -90,11 +96,54 @@ export const mapMultiRowFields = normalizeNamespace((
9096
}, {});
9197
});
9298

99+
export const mapObjectFields = normalizeNamespace((
100+
namespace,
101+
paths,
102+
getterType,
103+
mutationType,
104+
) => {
105+
const pathsObject = paths;
106+
107+
return Object.keys(pathsObject).reduce((entries, key) => {
108+
let path = pathsObject[key].replace(/\.?\*/g, '');
109+
110+
// eslint-disable-next-line no-param-reassign
111+
entries[key] = {
112+
get() {
113+
const store = this.$store;
114+
115+
const fieldsObject = store.getters[getterType](path);
116+
if (!fieldsObject) {
117+
return {}
118+
}
119+
120+
return Object.keys(fieldsObject).reduce((prev, fieldKey) => {
121+
const fieldPath = path ? `${path}.${fieldKey}`: fieldKey;
122+
123+
return Object.defineProperty(prev, fieldKey, {
124+
enumerable: true,
125+
get() {
126+
return store.getters[getterType](fieldPath);
127+
},
128+
set(value) {
129+
store.commit(mutationType, { path: fieldPath, value });
130+
},
131+
});
132+
}, {});
133+
},
134+
};
135+
136+
return entries;
137+
}, {});
138+
});
139+
93140
export const createHelpers = ({ getterType, mutationType }) => ({
94141
[getterType]: getField,
95142
[mutationType]: updateField,
96143
mapFields: normalizeNamespace((namespace, fields) =>
97144
mapFields(namespace, fields, getterType, mutationType)),
98145
mapMultiRowFields: normalizeNamespace((namespace, paths) =>
99146
mapMultiRowFields(namespace, paths, getterType, mutationType)),
147+
mapObjectFields: normalizeNamespace((namespace, paths) =>
148+
mapObjectFields(namespace, paths, getterType, mutationType)),
100149
});

src/index.spec.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
getField,
44
mapFields,
55
mapMultiRowFields,
6+
mapObjectFields,
67
updateField,
78
} from './';
89

@@ -199,6 +200,119 @@ describe(`index`, () => {
199200
});
200201
});
201202

203+
describe(`mapObjectFields()`, () => {
204+
test(`It should be possible to re-map the initial path.`, () => {
205+
const expectedResult = {
206+
otherFieldRows: { get: expect.any(Function) },
207+
};
208+
209+
expect(mapObjectFields({ otherFieldRows: `*` })).toEqual(expectedResult);
210+
});
211+
212+
test(`It should do nothing if path doesn't exist.`, () => {
213+
const mockGetField = jest.fn().mockReturnValue(undefined);
214+
const mappedObjectFields = mapObjectFields({objProps: `obj.*`});
215+
216+
const getterSetters = mappedObjectFields.objProps.get.apply({
217+
$store: { getters: { getField: mockGetField } },
218+
});
219+
220+
const x = getterSetters; // Trigger getter function.
221+
});
222+
223+
test(`It should get the value of a top-level property via the \`getField()\` function.`, () => {
224+
const mockObjectField = {
225+
foo: `Foo`,
226+
bar: `Bar`,
227+
};
228+
const mockGetField = jest.fn().mockReturnValue(mockObjectField).mockReturnValueOnce(mockObjectField);
229+
const mappedObjectFields = mapObjectFields({objProps: `*`});
230+
231+
const getterSetters = mappedObjectFields.objProps.get.apply({
232+
$store: { getters: { getField: mockGetField } },
233+
});
234+
235+
// eslint-disable-next-line no-unused-vars
236+
const x = getterSetters.foo; // Trigger getter function.
237+
expect(mockGetField).lastCalledWith(`foo`);
238+
239+
// eslint-disable-next-line no-unused-vars
240+
const y = getterSetters.bar; // Trigger getter function.
241+
expect(mockGetField).lastCalledWith(`bar`);
242+
});
243+
244+
test(`It should get the value of nested property via the \`getField()\` function.`, () => {
245+
const mockObjectField = {
246+
obj: {
247+
foo: `Foo`,
248+
bar: `Bar`,
249+
},
250+
};
251+
const mockGetField = jest.fn().mockReturnValue(mockObjectField).mockReturnValueOnce(mockObjectField.obj);
252+
const mappedObjectFields = mapObjectFields({objProps: `obj.*`});
253+
254+
const getterSetters = mappedObjectFields.objProps.get.apply({
255+
$store: { getters: { getField: mockGetField } },
256+
});
257+
258+
// eslint-disable-next-line no-unused-vars
259+
const x = getterSetters.foo; // Trigger getter function.
260+
expect(mockGetField).lastCalledWith(`obj.foo`);
261+
262+
// eslint-disable-next-line no-unused-vars
263+
const y = getterSetters.bar; // Trigger getter function.
264+
expect(mockGetField).lastCalledWith(`obj.bar`);
265+
});
266+
267+
test(`It should commit new values to the store (top).`, () => {
268+
const mockObjectField = {
269+
foo: `Foo`,
270+
bar: `Bar`,
271+
};
272+
const mockCommit = jest.fn();
273+
const mockGetField = jest.fn().mockReturnValue(mockObjectField).mockReturnValueOnce(mockObjectField);
274+
const mappedObjectFields = mapObjectFields({objProps: `*`});
275+
276+
const getterSetters = mappedObjectFields.objProps.get.apply({
277+
$store: {
278+
getters: { getField: mockGetField },
279+
commit: mockCommit,
280+
},
281+
});
282+
283+
getterSetters.bar = `New Bar`; // Trigger setter function.
284+
expect(mockCommit).toBeCalledWith(`updateField`, { path: `bar`, value: `New Bar` });
285+
286+
getterSetters.foo = `New Foo`; // Trigger setter function.
287+
expect(mockCommit).toBeCalledWith(`updateField`, { path: `foo`, value: `New Foo` });
288+
});
289+
290+
test(`It should commit new values to the store (nested).`, () => {
291+
const mockObjectField = {
292+
obj: {
293+
foo: `Foo`,
294+
bar: `Bar`,
295+
}
296+
};
297+
const mockCommit = jest.fn();
298+
const mockGetField = jest.fn().mockReturnValue(mockObjectField).mockReturnValueOnce(mockObjectField.obj);
299+
const mappedObjectFields = mapObjectFields({objProps: `obj.*`});
300+
301+
const getterSetters = mappedObjectFields.objProps.get.apply({
302+
$store: {
303+
getters: { getField: mockGetField },
304+
commit: mockCommit,
305+
},
306+
});
307+
308+
getterSetters.bar = `New Bar`; // Trigger setter function.
309+
expect(mockCommit).toBeCalledWith(`updateField`, { path: `obj.bar`, value: `New Bar` });
310+
311+
getterSetters.foo = `New Foo`; // Trigger setter function.
312+
expect(mockCommit).toBeCalledWith(`updateField`, { path: `obj.foo`, value: `New Foo` });
313+
});
314+
});
315+
202316
describe(`createHelpers()`, () => {
203317
test(`It should be a function.`, () => {
204318
expect(typeof createHelpers).toBe(`function`);
@@ -211,6 +325,7 @@ describe(`index`, () => {
211325
expect(typeof helpers.updateFoo).toBe(`function`);
212326
expect(typeof helpers.mapFields).toBe(`function`);
213327
expect(typeof helpers.mapMultiRowFields).toBe(`function`);
328+
expect(typeof helpers.mapObjectFields).toBe(`function`);
214329
});
215330

216331
test(`It should call the \`mapFields()\` function with the provided getter and mutation types.`, () => {
@@ -235,5 +350,16 @@ describe(`index`, () => {
235350

236351
expect(helpers.mapMultiRowFields([`foo`])).toEqual(expectedResult);
237352
});
353+
354+
test(`It should call the \`mapObjectFields()\` function with the provided getter and mutation types.`, () => {
355+
const helpers = createHelpers({ getterType: `getFoo`, mutationType: `updateFoo` });
356+
const expectedResult = {
357+
foo: {
358+
get: expect.any(Function),
359+
},
360+
};
361+
362+
expect(helpers.mapObjectFields({foo:`foo`})).toEqual(expectedResult);
363+
});
238364
});
239365
});

test/object-fields-top.test.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import Vuex from 'vuex';
2+
import { createLocalVue, shallowMount } from '@vue/test-utils';
3+
4+
import { mapObjectFields, getField, updateField } from '../src';
5+
6+
const localVue = createLocalVue();
7+
8+
localVue.use(Vuex);
9+
10+
describe(`Component initialized with object fields setup (top).`, () => {
11+
let Component;
12+
let store;
13+
let wrapper;
14+
15+
beforeEach(() => {
16+
Component = {
17+
template: `
18+
<div>
19+
<input id="city" v-model="address.city">
20+
<input id="country" v-model="address.country">
21+
</div>
22+
`,
23+
computed: {
24+
...mapObjectFields({
25+
address: `*`,
26+
}),
27+
},
28+
};
29+
30+
store = new Vuex.Store({
31+
state: {
32+
city: `New York`,
33+
country: `USA`,
34+
},
35+
getters: {
36+
getField,
37+
},
38+
mutations: {
39+
updateField,
40+
},
41+
});
42+
43+
wrapper = shallowMount(Component, { localVue, store });
44+
});
45+
46+
test(`It should render the component.`, () => {
47+
expect(wrapper.exists()).toBe(true);
48+
});
49+
50+
test(`It should update field values when the store is updated.`, () => {
51+
store.state.city = `New City`;
52+
store.state.country = `New Country`;
53+
54+
expect(wrapper.find(`#city`).element.value).toBe(`New City`);
55+
expect(wrapper.find(`#country`).element.value).toBe(`New Country`);
56+
});
57+
58+
test(`It should update the store when the field values are updated.`, () => {
59+
wrapper.find(`#city`).element.value = `New City`;
60+
wrapper.find(`#city`).trigger(`input`);
61+
62+
wrapper.find(`#country`).element.value = `New Country`;
63+
wrapper.find(`#country`).trigger(`input`);
64+
65+
expect(store.state.city).toBe(`New City`);
66+
expect(store.state.country).toBe(`New Country`);
67+
});
68+
});

test/object-fields.test.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import Vuex from 'vuex';
2+
import { createLocalVue, shallowMount } from '@vue/test-utils';
3+
4+
import { mapObjectFields, getField, updateField } from '../src';
5+
6+
const localVue = createLocalVue();
7+
8+
localVue.use(Vuex);
9+
10+
describe(`Component initialized with object fields setup.`, () => {
11+
let Component;
12+
let store;
13+
let wrapper;
14+
15+
beforeEach(() => {
16+
Component = {
17+
template: `
18+
<div>
19+
<input id="name" v-model="user.name">
20+
<input id="email" v-model="user.email">
21+
</div>
22+
`,
23+
computed: {
24+
...mapObjectFields({
25+
user: `user.*`,
26+
}),
27+
},
28+
};
29+
30+
store = new Vuex.Store({
31+
state: {
32+
user: {
33+
name: `Foo`,
34+
email: `foo@foo.com`
35+
},
36+
},
37+
getters: {
38+
getField,
39+
},
40+
mutations: {
41+
updateField,
42+
},
43+
});
44+
45+
wrapper = shallowMount(Component, { localVue, store });
46+
});
47+
48+
test(`It should render the component.`, () => {
49+
expect(wrapper.exists()).toBe(true);
50+
});
51+
52+
test(`It should update field values when the store is updated.`, () => {
53+
store.state.user.name = `New Name`;
54+
store.state.user.email = `new@email.com`;
55+
56+
expect(wrapper.find(`#name`).element.value).toBe(`New Name`);
57+
expect(wrapper.find(`#email`).element.value).toBe(`new@email.com`);
58+
});
59+
60+
test(`It should update the store when the field values are updated.`, () => {
61+
wrapper.find(`#name`).element.value = `New Name`;
62+
wrapper.find(`#name`).trigger(`input`);
63+
64+
wrapper.find(`#email`).element.value = `new@email.com`;
65+
wrapper.find(`#email`).trigger(`input`);
66+
67+
expect(store.state.user.name).toBe(`New Name`);
68+
expect(store.state.user.email).toBe(`new@email.com`);
69+
});
70+
});

0 commit comments

Comments
 (0)