Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changes for issue #43 (Shortcut for multiple object properties) #45

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ function normalizeNamespace(fn) {
}

export function getField(state) {
return path => path.split(/[.[\]]+/).reduce((prev, key) => prev[key], state);
return (path) => {
if (path === ``) {
return state;
}

return path.split(/[.[\]]+/).reduce((prev, key) => prev[key], state);
};
}

export function updateField(state, { path, value }) {
Expand Down Expand Up @@ -95,6 +101,47 @@ export const mapMultiRowFields = normalizeNamespace((
}, {});
});

export const mapObjectFields = normalizeNamespace((
namespace,
paths,
getterType,
mutationType,
) => {
const pathsObject = paths;

return Object.keys(pathsObject).reduce((entries, key) => {
const path = pathsObject[key].replace(/\.?\*/g, ``);

// eslint-disable-next-line no-param-reassign
entries[key] = {
get() {
const store = this.$store;

const fieldsObject = store.getters[getterType](path);
if (!fieldsObject) {
return {};
}

return Object.keys(fieldsObject).reduce((prev, fieldKey) => {
const fieldPath = path ? `${path}.${fieldKey}` : fieldKey;

return Object.defineProperty(prev, fieldKey, {
enumerable: true,
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,
Expand Down
135 changes: 135 additions & 0 deletions src/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
getField,
mapFields,
mapMultiRowFields,
mapObjectFields,
updateField,
} from '.';

Expand Down Expand Up @@ -199,6 +200,128 @@ describe(`index`, () => {
});
});

describe(`mapObjectFields()`, () => {
test(`It should be possible to re-map the initial path.`, () => {
const expectedResult = {
otherFieldRows: { get: expect.any(Function) },
};

expect(mapObjectFields({ otherFieldRows: `*` })).toEqual(expectedResult);
});

test(`It should do nothing if path doesn't exist.`, () => {
const mockGetField = jest.fn().mockReturnValue(undefined);
const mappedObjectFields = mapObjectFields({ objProps: `obj.*` });

const getterSetters = mappedObjectFields.objProps.get.apply({
$store: { getters: { getField: mockGetField } },
});

// eslint-disable-next-line no-unused-vars
const x = getterSetters; // Trigger getter function.
});

test(`It should get the value of a top-level property via the \`getField()\` function.`, () => {
const mockObjectField = {
foo: `Foo`,
bar: `Bar`,
};
const mockGetField = jest.fn().mockReturnValue(mockObjectField);
mockGetField.mockReturnValueOnce(mockObjectField);

const mappedObjectFields = mapObjectFields({ objProps: `*` });

const getterSetters = mappedObjectFields.objProps.get.apply({
$store: { getters: { getField: mockGetField } },
});

// eslint-disable-next-line no-unused-vars
const x = getterSetters.foo; // Trigger getter function.
expect(mockGetField).lastCalledWith(`foo`);

// eslint-disable-next-line no-unused-vars
const y = getterSetters.bar; // Trigger getter function.
expect(mockGetField).lastCalledWith(`bar`);
});

test(`It should get the value of nested property via the \`getField()\` function.`, () => {
const mockObjectField = {
obj: {
foo: `Foo`,
bar: `Bar`,
},
};
const mockGetField = jest.fn().mockReturnValue(mockObjectField);
mockGetField.mockReturnValueOnce(mockObjectField.obj);

const mappedObjectFields = mapObjectFields({ objProps: `obj.*` });

const getterSetters = mappedObjectFields.objProps.get.apply({
$store: { getters: { getField: mockGetField } },
});

// eslint-disable-next-line no-unused-vars
const x = getterSetters.foo; // Trigger getter function.
expect(mockGetField).lastCalledWith(`obj.foo`);

// eslint-disable-next-line no-unused-vars
const y = getterSetters.bar; // Trigger getter function.
expect(mockGetField).lastCalledWith(`obj.bar`);
});

test(`It should commit new values to the store (top).`, () => {
const mockObjectField = {
foo: `Foo`,
bar: `Bar`,
};
const mockCommit = jest.fn();
const mockGetField = jest.fn().mockReturnValue(mockObjectField);
mockGetField.mockReturnValueOnce(mockObjectField);

const mappedObjectFields = mapObjectFields({ objProps: `*` });

const getterSetters = mappedObjectFields.objProps.get.apply({
$store: {
getters: { getField: mockGetField },
commit: mockCommit,
},
});

getterSetters.bar = `New Bar`; // Trigger setter function.
expect(mockCommit).toBeCalledWith(`updateField`, { path: `bar`, value: `New Bar` });

getterSetters.foo = `New Foo`; // Trigger setter function.
expect(mockCommit).toBeCalledWith(`updateField`, { path: `foo`, value: `New Foo` });
});

test(`It should commit new values to the store (nested).`, () => {
const mockObjectField = {
obj: {
foo: `Foo`,
bar: `Bar`,
},
};
const mockCommit = jest.fn();
const mockGetField = jest.fn().mockReturnValue(mockObjectField);
mockGetField.mockReturnValueOnce(mockObjectField.obj);

const mappedObjectFields = mapObjectFields({ objProps: `obj.*` });

const getterSetters = mappedObjectFields.objProps.get.apply({
$store: {
getters: { getField: mockGetField },
commit: mockCommit,
},
});

getterSetters.bar = `New Bar`; // Trigger setter function.
expect(mockCommit).toBeCalledWith(`updateField`, { path: `obj.bar`, value: `New Bar` });

getterSetters.foo = `New Foo`; // Trigger setter function.
expect(mockCommit).toBeCalledWith(`updateField`, { path: `obj.foo`, value: `New Foo` });
});
});

describe(`createHelpers()`, () => {
test(`It should be a function.`, () => {
expect(typeof createHelpers).toBe(`function`);
Expand All @@ -211,6 +334,7 @@ describe(`index`, () => {
expect(typeof helpers.updateFoo).toBe(`function`);
expect(typeof helpers.mapFields).toBe(`function`);
expect(typeof helpers.mapMultiRowFields).toBe(`function`);
expect(typeof helpers.mapObjectFields).toBe(`function`);
});

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

expect(helpers.mapMultiRowFields([`foo`])).toEqual(expectedResult);
});

test(`It should call the \`mapObjectFields()\` function with the provided getter and mutation types.`, () => {
const helpers = createHelpers({ getterType: `getFoo`, mutationType: `updateFoo` });
const expectedResult = {
foo: {
get: expect.any(Function),
},
};

expect(helpers.mapObjectFields({ foo: `foo` })).toEqual(expectedResult);
});
});
});
68 changes: 68 additions & 0 deletions test/object-fields-top.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';

import { mapObjectFields, getField, updateField } from '../src';

const localVue = createLocalVue();

localVue.use(Vuex);

describe(`Component initialized with object fields setup (top).`, () => {
let Component;
let store;
let wrapper;

beforeEach(() => {
Component = {
template: `
<div>
<input id="city" v-model="address.city">
<input id="country" v-model="address.country">
</div>
`,
computed: {
...mapObjectFields({
address: `*`,
}),
},
};

store = new Vuex.Store({
state: {
city: `New York`,
country: `USA`,
},
getters: {
getField,
},
mutations: {
updateField,
},
});

wrapper = shallowMount(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.city = `New City`;
store.state.country = `New Country`;

expect(wrapper.find(`#city`).element.value).toBe(`New City`);
expect(wrapper.find(`#country`).element.value).toBe(`New Country`);
});

test(`It should update the store when the field values are updated.`, () => {
wrapper.find(`#city`).element.value = `New City`;
wrapper.find(`#city`).trigger(`input`);

wrapper.find(`#country`).element.value = `New Country`;
wrapper.find(`#country`).trigger(`input`);

expect(store.state.city).toBe(`New City`);
expect(store.state.country).toBe(`New Country`);
});
});
70 changes: 70 additions & 0 deletions test/object-fields.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';

import { mapObjectFields, getField, updateField } from '../src';

const localVue = createLocalVue();

localVue.use(Vuex);

describe(`Component initialized with object fields setup.`, () => {
let Component;
let store;
let wrapper;

beforeEach(() => {
Component = {
template: `
<div>
<input id="name" v-model="user.name">
<input id="email" v-model="user.email">
</div>
`,
computed: {
...mapObjectFields({
user: `user.*`,
}),
},
};

store = new Vuex.Store({
state: {
user: {
name: `Foo`,
email: `[email protected]`,
},
},
getters: {
getField,
},
mutations: {
updateField,
},
});

wrapper = shallowMount(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.user.name = `New Name`;
store.state.user.email = `[email protected]`;

expect(wrapper.find(`#name`).element.value).toBe(`New Name`);
expect(wrapper.find(`#email`).element.value).toBe(`[email protected]`);
});

test(`It should update the store when the field values are updated.`, () => {
wrapper.find(`#name`).element.value = `New Name`;
wrapper.find(`#name`).trigger(`input`);

wrapper.find(`#email`).element.value = `[email protected]`;
wrapper.find(`#email`).trigger(`input`);

expect(store.state.user.name).toBe(`New Name`);
expect(store.state.user.email).toBe(`[email protected]`);
});
});