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

Added nested values proposal prototype (#202) #207

Merged
merged 13 commits into from Nov 27, 2017
3 changes: 2 additions & 1 deletion src/Field.tsx
@@ -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
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
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
@@ -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'] });
});
});
});