Skip to content

Commit

Permalink
Added nested values proposal prototype (#202) (#207)
Browse files Browse the repository at this point in the history
* Added nested values proposal prototype (#202)

* Fixed default response in setNestedObjectValues

* Fixed isObject check in setNestedObjectValues

* Refactored to remove lodash set and get dependencies

* Inlined dlv in utils.js

* Fixed mutation bug in setDeep and added setDeep tests

* Added array support to setDeep

* Fixed tsconfig.json

* Fix dlv

* Fix generic in touchAllFields

* Fix typo
  • Loading branch information
klis87 authored and jaredpalmer committed Nov 27, 2017
1 parent e40dd9e commit 3a8e620
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 45 deletions.
3 changes: 2 additions & 1 deletion src/Field.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as PropTypes from 'prop-types';
import * as React from 'react';
import { dlv } from './utils';

import { FormikProps } from './formik';
import { isFunction, isEmptyChildren } from './utils';
Expand Down Expand Up @@ -121,7 +122,7 @@ export class Field<Props extends FieldAttributes = any> extends React.Component<
value:
props.type === 'radio' || props.type === 'checkbox'
? props.value
: formik.values[name],
: dlv(formik.values, name),
name,
onChange: formik.handleChange,
onBlur: formik.handleBlur,
Expand Down
75 changes: 31 additions & 44 deletions src/formik.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import * as PropTypes from 'prop-types';
import * as React from 'react';
import isEqual from 'lodash.isequal';
import {
isEmptyChildren,
isFunction,
isObject,
isPromise,
isReactNative,
isEmptyChildren,
values,
setDeep
} from './utils';

import warning from 'warning';
Expand Down Expand Up @@ -398,17 +400,11 @@ export class Formik<
// Set form fields by name
this.setState(prevState => ({
...prevState,
values: {
...(prevState.values as object),
[field]: val,
},
values: setDeep(field, val, prevState.values),
}));

if (this.props.validateOnChange) {
this.runValidations({
...(this.state.values as object),
[field]: val,
} as Object);
this.runValidations(setDeep(field, value, this.state.values));
}
};

Expand All @@ -421,38 +417,23 @@ export class Formik<
// Set touched and form fields by name
this.setState(prevState => ({
...prevState,
values: {
...(prevState.values as object),
[field]: value,
},
touched: {
...(prevState.touched as object),
[field]: true,
},
values: setDeep(field, value, prevState.values),
touched: setDeep(field, true, prevState.touched),
}));

this.runValidationSchema({
...(this.state.values as object),
[field]: value,
} as object);
this.runValidationSchema(setDeep(field, value, this.state.values));
};

setFieldValue = (field: string, value: any) => {
// Set form field by name
this.setState(
prevState => ({
...prevState,
values: {
...(prevState.values as object),
[field]: value,
},
values: setDeep(field, value, prevState.values),
}),
() => {
if (this.props.validateOnChange) {
this.runValidations({
...(this.state.values as object),
[field]: value,
} as object);
this.runValidations(this.state.values);
}
}
);
Expand Down Expand Up @@ -538,7 +519,7 @@ export class Formik<
}

this.setState(prevState => ({
touched: { ...(prevState.touched as object), [field]: true },
touched: setDeep(field, true, prevState.touched),
}));

if (this.props.validateOnBlur) {
Expand All @@ -551,10 +532,7 @@ export class Formik<
this.setState(
prevState => ({
...prevState,
touched: {
...(prevState.touched as object),
[field]: touched,
},
touched: setDeep(field, touched, prevState.touched),
}),
() => {
if (this.props.validateOnBlur) {
Expand All @@ -568,10 +546,7 @@ export class Formik<
// Set form field by name
this.setState(prevState => ({
...prevState,
errors: {
...(prevState.errors as object),
[field]: message,
},
errors: setDeep(field, message, prevState.errors),
}));
};

Expand Down Expand Up @@ -667,7 +642,7 @@ export function yupToFormErrors<Values>(yupError: any): FormikErrors<Values> {
let errors = {} as FormikErrors<Values>;
for (let err of yupError.inner) {
if (!errors[err.path]) {
errors[err.path] = err.message;
errors = setDeep(err.path, err.message, errors);
}
}
return errors;
Expand All @@ -692,12 +667,24 @@ export function validateYupSchema<T>(
return schema.validate(validateData, { abortEarly: false, context: context });
}

export function touchAllFields<Values>(fields: Values): FormikTouched<Values> {
const touched = {} as FormikTouched<Values>;
for (let k of Object.keys(fields)) {
touched[k] = true;
function setNestedObjectValues(object: any, value: any, response: any = null) {
response = response === null ? {} : response;

for (let k of Object.keys(object)) {
const val = object[k];
if (isObject(val)) {
response[k] = {};
setNestedObjectValues(val, value, response[k]);
} else {
response[k] = value;
}
}
return touched;

return response;
}

export function touchAllFields<T>(fields: T): FormikTouched<T> {
return setNestedObjectValues(fields, true);
}

export * from './Field';
Expand Down
58 changes: 58 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,66 @@ export function values<T>(obj: any): T[] {
return vals;
}

/**
* @private Deeply get a value from an object via it's dot path.
* See https://github.com/developit/dlv/blob/master/index.js
*/
export function dlv(
obj: any,
key: string | string[],
def?: any,
p: number = 0
) {
key = (key as string).split ? (key as string).split('.') : key;
while (obj && p < key.length) {
obj = obj[key[p++]];
}
return obj === undefined ? def : obj;
}

/**
* @private Deeply set a value from in object via it's dot path.
* See https://github.com/developit/linkstate
*/
export function setDeep(path: string, value: any, obj: any): any {
let res: any = {};
let resVal: any = res;
let i = 0;
let pathArray = path.replace(/\]/g, '').split(/\.|\[/);

for (; i < pathArray.length - 1; i++) {
const currentPath: string = pathArray[i];
let currentObj: any = obj[currentPath];

if (resVal[currentPath]) {
resVal = resVal[currentPath];
} else if (currentObj) {
resVal = resVal[currentPath] = Array.isArray(currentObj)
? [...currentObj]
: { ...currentObj };
} else {
const nextPath: string = pathArray[i + 1];
resVal = resVal[currentPath] =
isInteger(nextPath) && Number(nextPath) >= 0 ? [] : {};
}
}

resVal[pathArray[i]] = value;
return { ...obj, ...res };
}

/** @private is the given object a Function? */
export const isFunction = (obj: any) => 'function' === typeof obj;


/** @private is the given object an Object? */
export const isObject = (obj: any) => obj !== null && typeof obj === 'object';

/**
* @private is the given object an Integer?
* see https://stackoverflow.com/questions/10834796/validate-that-a-string-is-a-positive-integer
*/
export const isInteger = (obj: any) => String(Math.floor(Number(obj))) === obj;

export const isEmptyChildren = (children: any) =>
React.Children.count(children) === 0;
61 changes: 61 additions & 0 deletions test/utils.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { setDeep } from '../src/utils';

describe('helpers', () => {
describe('setDeep', () => {
it('sets flat value', () => {
const obj = { x: 'y' };
const newObj = setDeep('flat', 'value', obj);
expect(obj).toEqual({ x: 'y' });
expect(newObj).toEqual({ x: 'y', flat: 'value' });
});

it('sets nested value', () => {
const obj = { x: 'y' };
const newObj = setDeep('nested.value', 'nested value', obj);
expect(obj).toEqual({ x: 'y' });
expect(newObj).toEqual({ x: 'y', nested: { value: 'nested value' } });
});

it('updates nested value', () => {
const obj = { x: 'y', nested: { value: 'a' } };
const newObj = setDeep('nested.value', 'b', obj);
expect(obj).toEqual({ x: 'y', nested: { value: 'a' } });
expect(newObj).toEqual({ x: 'y', nested: { value: 'b' } });
});

it('sets new array', () => {
const obj = { x: 'y' };
const newObj = setDeep('nested.0', 'value', obj);
expect(obj).toEqual({ x: 'y' });
expect(newObj).toEqual({ x: 'y', nested: ['value'] });
});

it('updates nested array value', () => {
const obj = { x: 'y', nested: ['a'] };
const newObj = setDeep('nested.0', 'b', obj);
expect(obj).toEqual({ x: 'y', nested: ['a'] });
expect(newObj).toEqual({ x: 'y', nested: ['b'] });
});

it('adds new item to nested array', () => {
const obj = { x: 'y', nested: ['a'] };
const newObj = setDeep('nested.1', 'b', obj);
expect(obj).toEqual({ x: 'y', nested: ['a'] });
expect(newObj).toEqual({ x: 'y', nested: ['a', 'b'] });
});

it('sticks to object with int key when defined', () => {
const obj = { x: 'y', nested: { 0: 'a' } };
const newObj = setDeep('nested.0', 'b', obj);
expect(obj).toEqual({ x: 'y', nested: { 0: 'a' } });
expect(newObj).toEqual({ x: 'y', nested: { 0: 'b' } });
});

it('supports bracket path', () => {
const obj = { x: 'y' };
const newObj = setDeep('nested[0]', 'value', obj);
expect(obj).toEqual({ x: 'y' });
expect(newObj).toEqual({ x: 'y', nested: ['value'] });
});
});
});

0 comments on commit 3a8e620

Please sign in to comment.