Skip to content

Commit

Permalink
Respect dot notation in reset (#393)
Browse files Browse the repository at this point in the history
  • Loading branch information
rkuykendall authored Feb 18, 2020
1 parent d4eb73e commit a2d2e5f
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 92 deletions.
28 changes: 18 additions & 10 deletions __tests__/Formsy-spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -675,26 +675,29 @@ describe('value === false', () => {
});

it('should be able to reset the form using custom data', () => {
class TestForm extends React.Component<{}, { value: boolean | string }> {
class TestForm extends React.Component<{}, { value: number; valueDeep: number }> {
constructor(props) {
super(props);
this.state = {
value: true,
value: 1,
valueDeep: 11,
};
}

changeValue() {
this.setState({
value: false,
value: 2,
valueDeep: 12,
});
}

render() {
const { value } = this.state;
const { value, valueDeep } = this.state;

return (
<Formsy>
<TestInput name="foo" value={value} />
<TestInput name="bar.foo" value={valueDeep} />
<button type="submit">Save</button>
</Formsy>
);
Expand All @@ -703,17 +706,22 @@ describe('value === false', () => {

const form = mount(<TestForm />);
const input = form.find(TestInput).at(0);
const inputDeep = form.find(TestInput).at(1);
const formsyForm = form.find(Formsy);

expect(getInputInstance(input).getValue()).toEqual(true);
expect(getInputInstance(input).getValue()).toEqual(1);
expect(getInputInstance(inputDeep).getValue()).toEqual(11);

((form.instance() as TestForm) as TestForm).changeValue();
expect(getInputInstance(input).getValue()).toEqual(false);
expect(getInputInstance(input).getValue()).toEqual(2);
expect(getInputInstance(inputDeep).getValue()).toEqual(12);

getFormInstance(formsyForm).reset({
foo: 'bar',
foo: 3,
bar: { foo: 13 },
});
expect(getInputInstance(input).getValue()).toEqual('bar');
expect(getInputInstance(input).getValue()).toEqual(3);
expect(getInputInstance(inputDeep).getValue()).toEqual(13);
});
});

Expand Down Expand Up @@ -871,7 +879,7 @@ describe('form valid state', () => {
it('should be false when validationErrors is not empty', () => {
let isValid = true;

class TestForm extends React.Component<{}, { validationErrors: { [key: string]: string } }> {
class TestForm extends React.Component<{}, { validationErrors: { [key: string]: ValidationError } }> {
constructor(props) {
super(props);
this.state = {
Expand Down Expand Up @@ -909,7 +917,7 @@ describe('form valid state', () => {

it('should be true when validationErrors is not empty and preventExternalInvalidation is true', () => {
let isValid = true;
class TestForm extends React.Component<{}, { validationErrors: { [key: string]: string } }> {
class TestForm extends React.Component<{}, { validationErrors: { [key: string]: ValidationError } }> {
constructor(props) {
super(props);
this.state = {
Expand Down
11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@
"version": "npm run build && git add dist && npm run changelog && git add CHANGELOG.md"
},
"dependencies": {
"form-data-to-object": "^0.2.0",
"lodash.get": "^4.4.2",
"lodash.has": "^4.5.2",
"lodash.isplainobject": "^4.0.6",
"lodash.set": "^4.3.2",
"prop-types": "^15.7.2"
},
"devDependencies": {
Expand All @@ -78,10 +80,13 @@
"@rollup/plugin-node-resolve": "^7.1.1",
"@types/enzyme": "^3.10.3",
"@types/jest": "^25.1.1",
"@types/lodash.get": "^4.4.6",
"@types/lodash.has": "^4.5.6",
"@types/lodash.isplainobject": "^4.0.6",
"@types/lodash.set": "^4.3.6",
"@types/prop-types": "^15.7.1",
"@types/react": "^16.8.24",
"@types/react-dom": "^16.8.5",
"@types/react": "^16.9.19",
"@types/react-dom": "^16.9.5",
"@typescript-eslint/eslint-plugin": "^2.14.0",
"@typescript-eslint/parser": "^2.14.0",
"auto-changelog": "^1.14.1",
Expand Down
1 change: 0 additions & 1 deletion src/global.d.ts
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
declare module 'form-data-to-object';
119 changes: 48 additions & 71 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import React from 'react';
import PropTypes from 'prop-types';
import formDataToObject from 'form-data-to-object';
import get from 'lodash.get';
import has from 'lodash.has';
import set from 'lodash.set';

import * as utils from './utils';
import validationRules from './validationRules';
import Wrapper, { propTypes } from './Wrapper';
import Wrapper, { PassDownProps, propTypes, WrapperState } from './Wrapper';
import FormsyContext from './FormsyContext';

import {
FormsyContextInterface,
IModel,
InputComponent,
IResetModel,
IUpdateInputsWithValue,
IUpdateInputsWithError,
IUpdateInputsWithValue,
ValidationFunction,
FormsyContextInterface,
} from './interfaces';
import { isObject, isString } from './utils';

type FormHTMLAttributesCleaned = Omit<React.FormHTMLAttributes<HTMLFormElement>, 'onChange' | 'onSubmit'>;

Expand Down Expand Up @@ -45,7 +48,7 @@ export interface FormsyState {
}

class Formsy extends React.Component<FormsyProps, FormsyState> {
public inputs: any[];
public inputs: InstanceType<any & PassDownProps<any>>[];

public emptyArray: any[];

Expand Down Expand Up @@ -109,7 +112,7 @@ class Formsy extends React.Component<FormsyProps, FormsyState> {
public componentDidUpdate = (prevProps: FormsyProps) => {
const { validationErrors, disabled } = this.props;

if (validationErrors && typeof validationErrors === 'object' && Object.keys(validationErrors).length > 0) {
if (validationErrors && isObject(validationErrors) && Object.keys(validationErrors).length > 0) {
this.setInputValidationErrors(validationErrors);
}

Expand Down Expand Up @@ -179,13 +182,10 @@ class Formsy extends React.Component<FormsyProps, FormsyState> {

this.inputs.forEach(component => {
const { name } = component.props;
const args = [
{
isValid: !(name in errors),
validationError: typeof errors[name] === 'string' ? [errors[name]] : errors[name],
},
];
component.setState(...args);
component.setState({
isValid: !(name in errors),
validationError: isString(errors[name]) ? [errors[name]] : errors[name],
});
});
if (!preventExternalInvalidation && isValid) {
this.setFormValidState(false);
Expand Down Expand Up @@ -218,18 +218,11 @@ class Formsy extends React.Component<FormsyProps, FormsyState> {
return mapping(model);
}

return formDataToObject.toObj(
Object.keys(model).reduce((mappedModel, key) => {
const keyArray = key.split('.');
let base: IModel = mappedModel;
while (keyArray.length) {
const currentKey = keyArray.shift() as string;
base[currentKey] = keyArray.length ? base[currentKey] || {} : model[key];
base = base[currentKey];
}
return mappedModel;
}, {}),
);
const returnModel = {};
Object.keys(model).forEach(key => {
set(returnModel, key, model[key]);
});
return returnModel;
};

public reset = (model?: IModel) => {
Expand All @@ -248,11 +241,11 @@ class Formsy extends React.Component<FormsyProps, FormsyState> {
};

// Reset each key in the model to the original / initial / specified value
public resetModel: IResetModel = data => {
private resetModel: IResetModel = data => {
this.inputs.forEach(component => {
const { name } = component.props;
if (data && Object.prototype.hasOwnProperty.call(data, name)) {
component.setValue(data[name]);
if (data && has(data, name)) {
component.setValue(get(data, name));
} else {
component.resetValue();
}
Expand All @@ -261,8 +254,9 @@ class Formsy extends React.Component<FormsyProps, FormsyState> {
};

// Checks validation on current value or a passed value
public runValidation = <V>(component: InputComponent<V>, value = component.state.value) => {
public runValidation = <V>(component: InputComponent<V>, value = component.state.value): Partial<WrapperState<V>> => {
const { validationErrors } = this.props;
const { validationError, validationErrors: componentValidationErrors, name } = component.props;
const currentValues = this.getCurrentValues();
const validationResults = utils.runRules(value, currentValues, component.validations, validationRules);
const requiredResults = utils.runRules(value, currentValues, component.requiredValidations, validationRules);
Expand All @@ -272,7 +266,7 @@ class Formsy extends React.Component<FormsyProps, FormsyState> {
return {
isRequired,
isValid: isRequired ? false : isValid,
error: (() => {
validationError: (() => {
if (isValid && !isRequired) {
return this.emptyArray;
}
Expand All @@ -281,24 +275,18 @@ class Formsy extends React.Component<FormsyProps, FormsyState> {
return validationResults.errors;
}

if (validationErrors && validationErrors[component.props.name]) {
return typeof validationErrors[component.props.name] === 'string'
? [validationErrors[component.props.name]]
: validationErrors[component.props.name];
if (validationErrors && validationErrors[name]) {
return isString(validationErrors[name]) ? [validationErrors[name]] : validationErrors[name];
}

if (isRequired) {
const error = component.props.validationErrors[requiredResults.success[0]] || component.props.validationError;
const error = componentValidationErrors[requiredResults.success[0]] || validationError;
return error ? [error] : null;
}

if (validationResults.failed.length) {
return validationResults.failed
.map(failed =>
component.props.validationErrors[failed]
? component.props.validationErrors[failed]
: component.props.validationError,
)
.map(failed => (componentValidationErrors[failed] ? componentValidationErrors[failed] : validationError))
.filter((x, pos, arr) => arr.indexOf(x) === pos); // remove duplicates
}

Expand Down Expand Up @@ -349,6 +337,7 @@ class Formsy extends React.Component<FormsyProps, FormsyState> {
this.setFormPristine(false);
const model = this.getModel();
onSubmit(model, this.resetModel, this.updateInputsWithError);

if (isValid) {
onValidSubmit(model, this.resetModel, this.updateInputsWithError);
} else {
Expand All @@ -372,14 +361,12 @@ class Formsy extends React.Component<FormsyProps, FormsyState> {
)}`,
);
}
const args = [
{
isValid: preventExternalInvalidation,
validationError: utils.isString(error) ? [error] : error,
},
];
component.setState(...args);
component.setState({
isValid: preventExternalInvalidation,
validationError: utils.isString(error) ? [error] : error,
});
});

if (invalidate && isValid) {
this.setFormValidState(false);
}
Expand Down Expand Up @@ -408,17 +395,10 @@ class Formsy extends React.Component<FormsyProps, FormsyState> {
onChange(this.getModel(), this.isChanged());
}

const validation = this.runValidation<V>(component);
const validationState = this.runValidation<V>(component);
// Run through the validations, split them up and call
// the validator IF there is a value or it is required
component.setState(
{
isRequired: validation.isRequired,
isValid: validation.isValid,
validationError: validation.error,
},
this.validateForm,
);
component.setState(validationState, this.validateForm);
};

// Validate the form by going through all child input components
Expand All @@ -440,30 +420,28 @@ class Formsy extends React.Component<FormsyProps, FormsyState> {
// Run validation again in case affected by other inputs. The
// last component validated will run the onValidationComplete callback
this.inputs.forEach((component, index) => {
const validation = this.runValidation(component);
component.setState(
{
isValid: validation.isValid,
isRequired: validation.isRequired,
validationError: validation.error,
},
index === this.inputs.length - 1 ? onValidationComplete : null,
);
const validationState = this.runValidation(component);
const isFinalInput = index === this.inputs.length - 1;
const callback = isFinalInput ? onValidationComplete : null;
component.setState(validationState, callback);
});

// If there are no inputs, set state where form is ready to trigger
// change event. New inputs might be added later
if (!this.inputs.length) {
this.setState({
canChange: true,
});
onValidationComplete();
this.setState(
{
canChange: true,
},
onValidationComplete,
);
}
};

public render = () => {
const {
/* eslint-disable @typescript-eslint/no-unused-vars */
children,
mapping,
onChange,
onInvalid,
Expand All @@ -472,10 +450,9 @@ class Formsy extends React.Component<FormsyProps, FormsyState> {
onSubmit,
onValid,
onValidSubmit,
preventExternalInvalidation,
preventDefaultSubmit,
preventExternalInvalidation,
validationErrors,
children,
/* eslint-enable @typescript-eslint/no-unused-vars */
...nonFormsyProps
} = this.props;
Expand Down
Loading

0 comments on commit a2d2e5f

Please sign in to comment.