From 82a789a4fbd054e722f73a24a5c98aa3c276d7ad Mon Sep 17 00:00:00 2001
From: Felix Mosheev <9304194+felixmosh@users.noreply.github.com>
Date: Thu, 27 Aug 2020 20:06:31 +0300
Subject: [PATCH] Revalidate form when dynamic inputs are added, fixes #550
(#551)
* Revalidate form when dynamic inputs are added, fixes #550
* Improve pr after review
---
__tests__/Formsy.spec.tsx | 53 +++++++++++++++++++++++++++++++++++++++
src/Formsy.ts | 10 +++++++-
src/utils.ts | 12 +++++++++
3 files changed, 74 insertions(+), 1 deletion(-)
diff --git a/__tests__/Formsy.spec.tsx b/__tests__/Formsy.spec.tsx
index d34c0c2c..176d5956 100755
--- a/__tests__/Formsy.spec.tsx
+++ b/__tests__/Formsy.spec.tsx
@@ -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';
@@ -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 (
+ <>
+
+ {Array.from(Array(counter)).map((_, index) => (
+
+ ))}
+ >
+ );
+ };
+
+ const TestForm = () => {
+ return (
+ (isValid = false)} onValid={() => (isValid = true)}>
+
+
+ );
+ };
+ jest.useFakeTimers();
+ const form = mount();
+ 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 = () => (
+
+ // onValid is called each time the form revalidates
+ {Array.from(Array(5)).map((_, index) => (
+
+ ))}
+
+ );
+
+ mount();
+
+ expect(validSpy).toHaveBeenCalledTimes(1 + 1); // one for form mount & 1 for all attachToForm calls
+ });
});
describe('onSubmit/onValidSubmit/onInvalidSubmit', () => {
diff --git a/src/Formsy.ts b/src/Formsy.ts
index ff90d8e9..fb0eb409 100644
--- a/src/Formsy.ts
+++ b/src/Formsy.ts
@@ -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';
@@ -52,6 +52,8 @@ export interface FormsyState {
isValid: boolean;
}
+const ONE_RENDER_FRAME = 66;
+
export class Formsy extends React.Component {
public inputs: InstanceType>[];
@@ -91,6 +93,8 @@ export class Formsy extends React.Component {
validationErrors: {},
};
+ private readonly throttledValidateForm: () => void;
+
public constructor(props: FormsyProps) {
super(props);
this.state = {
@@ -108,6 +112,7 @@ export class Formsy extends React.Component {
};
this.inputs = [];
this.emptyArray = [];
+ this.throttledValidateForm = throttle(this.validateForm, ONE_RENDER_FRAME);
}
public componentDidMount = () => {
@@ -320,6 +325,9 @@ export class Formsy extends React.Component {
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
diff --git a/src/utils.ts b/src/utils.ts
index f7cc4301..a32175a6 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -142,3 +142,15 @@ export function runRules(
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);
+ };
+}