Skip to content

Commit 4fd1bcc

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

File tree

5 files changed

+1032
-1
lines changed

5 files changed

+1032
-1
lines changed

src/index.js

+50-1
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+
}
25+
26+
return path.split(/[.[\]]+/).reduce((prev, key) => prev[key], state);
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+
const 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

+135
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,128 @@ 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+
// eslint-disable-next-line no-unused-vars
221+
const x = getterSetters; // Trigger getter function.
222+
});
223+
224+
test(`It should get the value of a top-level property via the \`getField()\` function.`, () => {
225+
const mockObjectField = {
226+
foo: `Foo`,
227+
bar: `Bar`,
228+
};
229+
const mockGetField = jest.fn().mockReturnValue(mockObjectField);
230+
mockGetField.mockReturnValueOnce(mockObjectField);
231+
232+
const mappedObjectFields = mapObjectFields({ objProps: `*` });
233+
234+
const getterSetters = mappedObjectFields.objProps.get.apply({
235+
$store: { getters: { getField: mockGetField } },
236+
});
237+
238+
// eslint-disable-next-line no-unused-vars
239+
const x = getterSetters.foo; // Trigger getter function.
240+
expect(mockGetField).lastCalledWith(`foo`);
241+
242+
// eslint-disable-next-line no-unused-vars
243+
const y = getterSetters.bar; // Trigger getter function.
244+
expect(mockGetField).lastCalledWith(`bar`);
245+
});
246+
247+
test(`It should get the value of nested property via the \`getField()\` function.`, () => {
248+
const mockObjectField = {
249+
obj: {
250+
foo: `Foo`,
251+
bar: `Bar`,
252+
},
253+
};
254+
const mockGetField = jest.fn().mockReturnValue(mockObjectField);
255+
mockGetField.mockReturnValueOnce(mockObjectField.obj);
256+
257+
const mappedObjectFields = mapObjectFields({ objProps: `obj.*` });
258+
259+
const getterSetters = mappedObjectFields.objProps.get.apply({
260+
$store: { getters: { getField: mockGetField } },
261+
});
262+
263+
// eslint-disable-next-line no-unused-vars
264+
const x = getterSetters.foo; // Trigger getter function.
265+
expect(mockGetField).lastCalledWith(`obj.foo`);
266+
267+
// eslint-disable-next-line no-unused-vars
268+
const y = getterSetters.bar; // Trigger getter function.
269+
expect(mockGetField).lastCalledWith(`obj.bar`);
270+
});
271+
272+
test(`It should commit new values to the store (top).`, () => {
273+
const mockObjectField = {
274+
foo: `Foo`,
275+
bar: `Bar`,
276+
};
277+
const mockCommit = jest.fn();
278+
const mockGetField = jest.fn().mockReturnValue(mockObjectField);
279+
mockGetField.mockReturnValueOnce(mockObjectField);
280+
281+
const mappedObjectFields = mapObjectFields({ objProps: `*` });
282+
283+
const getterSetters = mappedObjectFields.objProps.get.apply({
284+
$store: {
285+
getters: { getField: mockGetField },
286+
commit: mockCommit,
287+
},
288+
});
289+
290+
getterSetters.bar = `New Bar`; // Trigger setter function.
291+
expect(mockCommit).toBeCalledWith(`updateField`, { path: `bar`, value: `New Bar` });
292+
293+
getterSetters.foo = `New Foo`; // Trigger setter function.
294+
expect(mockCommit).toBeCalledWith(`updateField`, { path: `foo`, value: `New Foo` });
295+
});
296+
297+
test(`It should commit new values to the store (nested).`, () => {
298+
const mockObjectField = {
299+
obj: {
300+
foo: `Foo`,
301+
bar: `Bar`,
302+
},
303+
};
304+
const mockCommit = jest.fn();
305+
const mockGetField = jest.fn().mockReturnValue(mockObjectField);
306+
mockGetField.mockReturnValueOnce(mockObjectField.obj);
307+
308+
const mappedObjectFields = mapObjectFields({ objProps: `obj.*` });
309+
310+
const getterSetters = mappedObjectFields.objProps.get.apply({
311+
$store: {
312+
getters: { getField: mockGetField },
313+
commit: mockCommit,
314+
},
315+
});
316+
317+
getterSetters.bar = `New Bar`; // Trigger setter function.
318+
expect(mockCommit).toBeCalledWith(`updateField`, { path: `obj.bar`, value: `New Bar` });
319+
320+
getterSetters.foo = `New Foo`; // Trigger setter function.
321+
expect(mockCommit).toBeCalledWith(`updateField`, { path: `obj.foo`, value: `New Foo` });
322+
});
323+
});
324+
202325
describe(`createHelpers()`, () => {
203326
test(`It should be a function.`, () => {
204327
expect(typeof createHelpers).toBe(`function`);
@@ -211,6 +334,7 @@ describe(`index`, () => {
211334
expect(typeof helpers.updateFoo).toBe(`function`);
212335
expect(typeof helpers.mapFields).toBe(`function`);
213336
expect(typeof helpers.mapMultiRowFields).toBe(`function`);
337+
expect(typeof helpers.mapObjectFields).toBe(`function`);
214338
});
215339

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

236360
expect(helpers.mapMultiRowFields([`foo`])).toEqual(expectedResult);
237361
});
362+
363+
test(`It should call the \`mapObjectFields()\` function with the provided getter and mutation types.`, () => {
364+
const helpers = createHelpers({ getterType: `getFoo`, mutationType: `updateFoo` });
365+
const expectedResult = {
366+
foo: {
367+
get: expect.any(Function),
368+
},
369+
};
370+
371+
expect(helpers.mapObjectFields({ foo: `foo` })).toEqual(expectedResult);
372+
});
238373
});
239374
});

test/object-fields-top.test.js

+68
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

+70
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+
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 = `[email protected]`;
55+
56+
expect(wrapper.find(`#name`).element.value).toBe(`New Name`);
57+
expect(wrapper.find(`#email`).element.value).toBe(`[email protected]`);
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 = `[email protected]`;
65+
wrapper.find(`#email`).trigger(`input`);
66+
67+
expect(store.state.user.name).toBe(`New Name`);
68+
expect(store.state.user.email).toBe(`[email protected]`);
69+
});
70+
});

0 commit comments

Comments
 (0)