From 1edc013568422677c1475a03d75b93eef3d5582e Mon Sep 17 00:00:00 2001 From: Felix Mosheev <9304194+felixmosh@users.noreply.github.com> Date: Fri, 14 Jun 2024 20:31:56 +0300 Subject: [PATCH] feat: add test for unmounting multiple inputs, closes #712 fix: change trottle to debounce --- __tests__/Formsy.spec.tsx | 62 ++++++++++++++++++++++++++++++++++++--- src/Formsy.ts | 48 ++++++++++++------------------ src/utils.ts | 16 +++++----- 3 files changed, 83 insertions(+), 43 deletions(-) diff --git a/__tests__/Formsy.spec.tsx b/__tests__/Formsy.spec.tsx index 1ecb6ac3..6879b8dd 100755 --- a/__tests__/Formsy.spec.tsx +++ b/__tests__/Formsy.spec.tsx @@ -770,7 +770,13 @@ describe('value === false', () => { }); it('should be able to set a deep value with updateInputsWithValue', () => { - class TestForm extends React.Component<{}, { valueBar: number; valueFoo: { valueBar: number } }> { + class TestForm extends React.Component< + {}, + { + valueBar: number; + valueFoo: { valueBar: number }; + } + > { formRef = React.createRef(); constructor(props) { @@ -1061,7 +1067,12 @@ describe('form valid state', () => { it('should be false when validationErrors is not empty', () => { let isValid = true; - class TestForm extends React.Component<{}, { validationErrors: { [key: string]: ValidationError } }> { + class TestForm extends React.Component< + {}, + { + validationErrors: { [key: string]: ValidationError }; + } + > { constructor(props) { super(props); this.state = { @@ -1102,7 +1113,12 @@ 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]: ValidationError } }> { + class TestForm extends React.Component< + {}, + { + validationErrors: { [key: string]: ValidationError }; + } + > { constructor(props) { super(props); this.state = { @@ -1197,7 +1213,45 @@ describe('form valid state', () => { render(); - expect(validSpy).toHaveBeenCalledTimes(1 + 1); // one for form mount & 1 for all attachToForm calls + expect(validSpy).toHaveBeenCalledTimes(1); + }); + + it('should revalidate form once when unmounting multiple inputs', () => { + const Inputs = () => { + const [showInputs, setShowInputs] = useState(true); + + return ( + <> + + {showInputs && + Array.from({ length: 10 }, (_, index) => ( + + ))} + + ); + }; + + const validSpy = jest.fn(); + + const TestForm = () => ( + + + + ); + + jest.useFakeTimers(); + const screen = render(); + jest.runAllTimers(); + const toggleBtn = screen.getByTestId('toggle-btn'); + + validSpy.mockClear(); + + fireEvent.click(toggleBtn); + jest.runAllTimers(); + + expect(validSpy).toHaveBeenCalledTimes(1); }); }); diff --git a/src/Formsy.ts b/src/Formsy.ts index 52e8afe5..5d5ebb11 100644 --- a/src/Formsy.ts +++ b/src/Formsy.ts @@ -12,7 +12,7 @@ import { IUpdateInputsWithValue, ValidationError, } from './interfaces'; -import { throttle, isObject, isString } from './utils'; +import { debounce, isObject, isString } from './utils'; import * as utils from './utils'; import validationRules from './validationRules'; import { PassDownProps } from './withFormsy'; @@ -103,7 +103,7 @@ export class Formsy extends React.Component { formElement: 'form', }; - private readonly throttledValidateForm: () => void; + private readonly debouncedValidateForm: () => void; public constructor(props: FormsyProps) { super(props); @@ -122,7 +122,7 @@ export class Formsy extends React.Component { }; this.inputs = []; this.emptyArray = []; - this.throttledValidateForm = throttle(this.validateForm, ONE_RENDER_FRAME); + this.debouncedValidateForm = debounce(this.validateForm, ONE_RENDER_FRAME); } public componentDidMount = () => { @@ -336,20 +336,15 @@ export class Formsy extends React.Component { onChange(this.getModel(), this.isChanged()); } - // Will be triggered immediately & every one frame rate - this.throttledValidateForm(); + this.debouncedValidateForm(); }; // Method put on each input component to unregister // itself from the form public detachFromForm = (component: InputComponent) => { - const componentPos = this.inputs.indexOf(component); + this.inputs = this.inputs.filter((input) => input !== component); - if (componentPos !== -1) { - this.inputs = this.inputs.slice(0, componentPos).concat(this.inputs.slice(componentPos + 1)); - } - - this.throttledValidateForm(); + this.debouncedValidateForm(); }; // Checks if the values have changed from their initial value @@ -437,7 +432,7 @@ export class Formsy extends React.Component { // and check their state public validateForm = () => { // We need a callback as we are validating all inputs again. This will - // run when the last component has set its state + // run when the last input has set its state const onValidationComplete = () => { const allIsValid = this.inputs.every((component) => component.state.isValid); @@ -449,24 +444,17 @@ export class Formsy extends React.Component { }); }; - // Run validation again in case affected by other inputs. The - // last component validated will run the onValidationComplete callback - this.inputs.forEach((component, index) => { - 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, - ); + if (this.inputs.length === 0) { + onValidationComplete(); + } else { + // Run validation again in case affected by other inputs. The + // last component validated will run the onValidationComplete callback + this.inputs.forEach((component, index) => { + const validationState = this.runValidation(component); + const isLastInput = index === this.inputs.length - 1; + const callback = isLastInput ? onValidationComplete : null; + component.setState(validationState, callback); + }); } }; diff --git a/src/utils.ts b/src/utils.ts index 8e88122b..f5f6a23e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -143,14 +143,12 @@ 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); +export function debounce(callback, timeout: number) { + let timer; + return (...args) => { + clearTimeout(timer); + timer = setTimeout(() => { + callback.apply(this, args); + }, timeout); }; }