Skip to content

Commit

Permalink
Revalidate form when dynamic inputs are added, fixes #550 (#551)
Browse files Browse the repository at this point in the history
* Revalidate form when dynamic inputs are added, fixes #550

* Improve pr after review
  • Loading branch information
felixmosh authored Aug 27, 2020
1 parent 800dff3 commit 82a789a
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 1 deletion.
53 changes: 53 additions & 0 deletions __tests__/Formsy.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable max-classes-per-file, react/destructuring-assignment */
import { mount } from 'enzyme';
import * as React from 'react';
import { useState } from 'react';

import DynamicInputForm from '../__test_utils__/DynamicInputForm';
import { getFormInstance, getWrapperInstance } from '../__test_utils__/getInput';
Expand Down Expand Up @@ -971,6 +972,58 @@ describe('form valid state', () => {

expect(isValid).toEqual(true);
});

it('should revalidate form when input added dynamically', () => {
let isValid = false;
const Inputs = () => {
const [counter, setCounter] = useState(1);

return (
<>
<button type="button" onClick={() => setCounter(counter + 1)} id="add">
+
</button>
{Array.from(Array(counter)).map((_, index) => (
<TestInput key={index} name={`foo-${index}`} required={true} value={index === 0 ? 'bla' : undefined} />
))}
</>
);
};

const TestForm = () => {
return (
<Formsy onInvalid={() => (isValid = false)} onValid={() => (isValid = true)}>
<Inputs />
</Formsy>
);
};
jest.useFakeTimers();
const form = mount(<TestForm />);
const plusButton = form.find('#add');
jest.runAllTimers();

expect(isValid).toBe(true);

plusButton.simulate('click');

expect(isValid).toBe(false);
});

it('should revalidate form once when mounting multiple inputs', () => {
const validSpy = jest.fn();
const TestForm = () => (
<Formsy onValid={validSpy}>
// onValid is called each time the form revalidates
{Array.from(Array(5)).map((_, index) => (
<TestInput key={index} name={`foo-${index}`} required={true} value={'bla'} />
))}
</Formsy>
);

mount(<TestForm />);

expect(validSpy).toHaveBeenCalledTimes(1 + 1); // one for form mount & 1 for all attachToForm calls
});
});

describe('onSubmit/onValidSubmit/onInvalidSubmit', () => {
Expand Down
10 changes: 9 additions & 1 deletion src/Formsy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
IUpdateInputsWithValue,
ValidationError,
} from './interfaces';
import { isObject, isString } from './utils';
import { throttle, isObject, isString } from './utils';
import * as utils from './utils';
import validationRules from './validationRules';
import { PassDownProps } from './withFormsy';
Expand Down Expand Up @@ -52,6 +52,8 @@ export interface FormsyState {
isValid: boolean;
}

const ONE_RENDER_FRAME = 66;

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

Expand Down Expand Up @@ -91,6 +93,8 @@ export class Formsy extends React.Component<FormsyProps, FormsyState> {
validationErrors: {},
};

private readonly throttledValidateForm: () => void;

public constructor(props: FormsyProps) {
super(props);
this.state = {
Expand All @@ -108,6 +112,7 @@ export class Formsy extends React.Component<FormsyProps, FormsyState> {
};
this.inputs = [];
this.emptyArray = [];
this.throttledValidateForm = throttle(this.validateForm, ONE_RENDER_FRAME);
}

public componentDidMount = () => {
Expand Down Expand Up @@ -320,6 +325,9 @@ export class Formsy extends React.Component<FormsyProps, FormsyState> {
if (canChange) {
onChange(this.getModel(), this.isChanged());
}

// Will be triggered immediately & every one frame rate
this.throttledValidateForm();
};

// Method put on each input component to unregister
Expand Down
12 changes: 12 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,15 @@ export function runRules<V>(

return results;
}

export function throttle(callback, interval) {
let enableCall = true;

return function (...args) {
if (!enableCall) return;

enableCall = false;
callback.apply(this, args);
setTimeout(() => (enableCall = true), interval);
};
}

0 comments on commit 82a789a

Please sign in to comment.