Skip to content

Commit

Permalink
feat: add test for unmounting multiple inputs, closes #712
Browse files Browse the repository at this point in the history
fix: change trottle to debounce
  • Loading branch information
felixmosh committed Jun 14, 2024
1 parent 6737b9e commit 1edc013
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 43 deletions.
62 changes: 58 additions & 4 deletions __tests__/Formsy.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Formsy>();

constructor(props) {
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -1197,7 +1213,45 @@ describe('form valid state', () => {

render(<TestForm />);

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 (
<>
<button type="button" onClick={() => setShowInputs(!showInputs)} data-testid="toggle-btn">
toggle inputs
</button>
{showInputs &&
Array.from({ length: 10 }, (_, index) => (
<TestInput key={index} name={`foo-${index}`} required={true} value={`${index}`} />
))}
</>
);
};

const validSpy = jest.fn();

const TestForm = () => (
<Formsy onValid={validSpy}>
<Inputs />
</Formsy>
);

jest.useFakeTimers();
const screen = render(<TestForm />);
jest.runAllTimers();
const toggleBtn = screen.getByTestId('toggle-btn');

validSpy.mockClear();

fireEvent.click(toggleBtn);
jest.runAllTimers();

expect(validSpy).toHaveBeenCalledTimes(1);
});
});

Expand Down
48 changes: 18 additions & 30 deletions src/Formsy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -103,7 +103,7 @@ export class Formsy extends React.Component<FormsyProps, FormsyState> {
formElement: 'form',
};

private readonly throttledValidateForm: () => void;
private readonly debouncedValidateForm: () => void;

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

public componentDidMount = () => {
Expand Down Expand Up @@ -336,20 +336,15 @@ export class Formsy extends React.Component<FormsyProps, FormsyState> {
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 = <V>(component: InputComponent<V>) => {
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
Expand Down Expand Up @@ -437,7 +432,7 @@ export class Formsy extends React.Component<FormsyProps, FormsyState> {
// 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);

Expand All @@ -449,24 +444,17 @@ export 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 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);
});
}
};

Expand Down
16 changes: 7 additions & 9 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,14 +143,12 @@ 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);
export function debounce(callback, timeout: number) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
callback.apply(this, args);
}, timeout);
};
}

0 comments on commit 1edc013

Please sign in to comment.