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); + }; +}