Skip to content
55 changes: 40 additions & 15 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ function objectEntries(obj) {
return Object.keys(obj).map(key => [key, obj[key]]);
}

function typeOf(obj) {
return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
}

function normalizeNamespace(fn) {
return (...params) => {
// eslint-disable-next-line prefer-const
Expand All @@ -23,11 +27,11 @@ function normalizeNamespace(fn) {
}

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

export function updateField(state, { path, value }) {
path.split(/[.[\]]+/).reduce((prev, key, index, array) => {
path.split(/[.[\]]+/).filter(x => x).reduce((prev, key, index, array) => {
if (array.length === index + 1) {
// eslint-disable-next-line no-param-reassign
prev[key] = value;
Expand All @@ -37,24 +41,44 @@ export function updateField(state, { path, value }) {
}, state);
}

export const mapFields = normalizeNamespace((namespace, fields, getterType, mutationType) => {
const fieldsObject = Array.isArray(fields) ? arrayToObject(fields) : fields;
export const mapFields = normalizeNamespace((
namespace,
paths,
getterType,
mutationType,
) => {
const pathsObject = Array.isArray(paths) ? arrayToObject(paths) : paths;
return Object.keys(pathsObject).reduce((entries, key) => {
const path = pathsObject[key];

return Object.keys(fieldsObject).reduce((prev, key) => {
const path = fieldsObject[key];
const field = {
// eslint-disable-next-line no-param-reassign
entries[key] = {
get() {
return this.$store.getters[getterType](path);
const tpath = typeof path === `function` ? path.call(null, this) : path;
const store = this.$store;
const row = store.getters[getterType](tpath);

if (!row || typeOf(row) !== `object`) return row;

return Object.keys(row).reduce((prev, fieldKey) => {
const fieldPath = `${tpath}.${fieldKey}`;
return Object.defineProperty(prev, fieldKey, {
get() {
return store.getters[getterType](fieldPath);
},
set(value) {
store.commit(mutationType, { path: fieldPath, value });
},
});
}, {});
},
set(value) {
this.$store.commit(mutationType, { path, value });
const tpath = typeof path === `function` ? path.call(null, this) : path;
this.$store.commit(mutationType, { path: tpath, value });
},
};

// eslint-disable-next-line no-param-reassign
prev[key] = field;

return prev;
return entries;
}, {});
});

Expand All @@ -72,12 +96,13 @@ export const mapMultiRowFields = normalizeNamespace((
// eslint-disable-next-line no-param-reassign
entries[key] = {
get() {
const tpath = typeof path === `function` ? path.call(null, this) : path;
const store = this.$store;
const rows = objectEntries(store.getters[getterType](path));
const rows = objectEntries(store.getters[getterType](tpath));

return rows
.map(fieldsObject => Object.keys(fieldsObject[1]).reduce((prev, fieldKey) => {
const fieldPath = `${path}[${fieldsObject[0]}].${fieldKey}`;
const fieldPath = `${tpath}[${fieldsObject[0]}].${fieldKey}`;

return Object.defineProperty(prev, fieldKey, {
get() {
Expand Down
2 changes: 2 additions & 0 deletions src/lib/array-to-object.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export default function arrayToObject(fields = []) {
return fields.reduce((prev, path) => {
if (typeof path === `object`) return { ...prev, ...path };

const key = path.split(`.`).slice(-1)[0];

if (prev[key]) {
Expand Down
81 changes: 81 additions & 0 deletions test/function-param.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';

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

const localVue = createLocalVue();

localVue.use(Vuex);

const { mapFields } = createHelpers({
getterType: `getField`,
mutationType: `updateField`,
});

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

beforeEach(() => {

Component = {
template: `
<div>
<div>
<input v-model="name">
</div>
</div>
`,
data() {
return {
ative_room: '0'
};
},
computed: {
...mapFields([{name: (c) => `users[${c.ative_room}].name` }]),
}
};

store = new Vuex.Store({
state: {
users: [
{
name: `Foo`,
email: `foo@foo.com`,
},
{
name: `Bar`,
email: `bar@bar.com`,
},
],
},
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.users[0].name = `New Name`;
// store.state.users[0].email = `new@email.com`;

expect(wrapper.find(`input`).element.value).toBe(`New Name`);
});

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

expect(store.state.users[0].name).toBe(`New Name`);
});
});
86 changes: 86 additions & 0 deletions test/map-object-properties.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';

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

const localVue = createLocalVue();

localVue.use(Vuex);

const { mapFields } = createHelpers({
getterType: `getField`,
mutationType: `updateField`,
});

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

beforeEach(() => {

Component = {
template: `
<div>
<div>
<input v-model="user.name">
<input v-model="user.email">
</div>
</div>
`,
data() {
return {
ative_room: '0'
};
},
computed: {
...mapFields({user: `users[0]` }),
}
};

store = new Vuex.Store({
state: {
users: [
{
name: `Foo`,
email: `foo@foo.com`,
},
{
name: `Bar`,
email: `bar@bar.com`,
},
],
},
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.users[0].name = `New Name`;
store.state.users[0].email = `new@email.com`;

expect(wrapper.find(`input`).element.value).toBe(`New Name`);
expect(wrapper.find(`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 = `Foo`;
wrapper.find(`input:nth-child(2)`).element.value = `foo@foo.com`;
wrapper.find(`input`).trigger(`input`);
wrapper.find(`input:nth-child(2)`).trigger(`input`);

expect(store.state.users[0].name).toBe(`Foo`);
expect(store.state.users[0].email).toBe(`foo@foo.com`);
});
});
6 changes: 5 additions & 1 deletion test/multi-row.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,14 @@ describe(`Component initialized with multi row setup.`, () => {
<input v-model="user.name">
<input v-model="user.email">
</div>
<div v-for="user in usersfun">
<input v-model="user.name">
<input v-model="user.email">
</div>
</div>
`,
computed: {
...mapMultiRowFields([`users`]),
...mapMultiRowFields([`users`, { usersfun: () => `users` }]),
},
};

Expand Down