Skip to content

Commit 1edc013

Browse files
committed
feat: add test for unmounting multiple inputs, closes #712
fix: change trottle to debounce
1 parent 6737b9e commit 1edc013

File tree

3 files changed

+83
-43
lines changed

3 files changed

+83
-43
lines changed

__tests__/Formsy.spec.tsx

+58-4
Original file line numberDiff line numberDiff line change
@@ -770,7 +770,13 @@ describe('value === false', () => {
770770
});
771771

772772
it('should be able to set a deep value with updateInputsWithValue', () => {
773-
class TestForm extends React.Component<{}, { valueBar: number; valueFoo: { valueBar: number } }> {
773+
class TestForm extends React.Component<
774+
{},
775+
{
776+
valueBar: number;
777+
valueFoo: { valueBar: number };
778+
}
779+
> {
774780
formRef = React.createRef<Formsy>();
775781

776782
constructor(props) {
@@ -1061,7 +1067,12 @@ describe('form valid state', () => {
10611067
it('should be false when validationErrors is not empty', () => {
10621068
let isValid = true;
10631069

1064-
class TestForm extends React.Component<{}, { validationErrors: { [key: string]: ValidationError } }> {
1070+
class TestForm extends React.Component<
1071+
{},
1072+
{
1073+
validationErrors: { [key: string]: ValidationError };
1074+
}
1075+
> {
10651076
constructor(props) {
10661077
super(props);
10671078
this.state = {
@@ -1102,7 +1113,12 @@ describe('form valid state', () => {
11021113
it('should be true when validationErrors is not empty and preventExternalInvalidation is true', () => {
11031114
let isValid = true;
11041115

1105-
class TestForm extends React.Component<{}, { validationErrors: { [key: string]: ValidationError } }> {
1116+
class TestForm extends React.Component<
1117+
{},
1118+
{
1119+
validationErrors: { [key: string]: ValidationError };
1120+
}
1121+
> {
11061122
constructor(props) {
11071123
super(props);
11081124
this.state = {
@@ -1197,7 +1213,45 @@ describe('form valid state', () => {
11971213

11981214
render(<TestForm />);
11991215

1200-
expect(validSpy).toHaveBeenCalledTimes(1 + 1); // one for form mount & 1 for all attachToForm calls
1216+
expect(validSpy).toHaveBeenCalledTimes(1);
1217+
});
1218+
1219+
it('should revalidate form once when unmounting multiple inputs', () => {
1220+
const Inputs = () => {
1221+
const [showInputs, setShowInputs] = useState(true);
1222+
1223+
return (
1224+
<>
1225+
<button type="button" onClick={() => setShowInputs(!showInputs)} data-testid="toggle-btn">
1226+
toggle inputs
1227+
</button>
1228+
{showInputs &&
1229+
Array.from({ length: 10 }, (_, index) => (
1230+
<TestInput key={index} name={`foo-${index}`} required={true} value={`${index}`} />
1231+
))}
1232+
</>
1233+
);
1234+
};
1235+
1236+
const validSpy = jest.fn();
1237+
1238+
const TestForm = () => (
1239+
<Formsy onValid={validSpy}>
1240+
<Inputs />
1241+
</Formsy>
1242+
);
1243+
1244+
jest.useFakeTimers();
1245+
const screen = render(<TestForm />);
1246+
jest.runAllTimers();
1247+
const toggleBtn = screen.getByTestId('toggle-btn');
1248+
1249+
validSpy.mockClear();
1250+
1251+
fireEvent.click(toggleBtn);
1252+
jest.runAllTimers();
1253+
1254+
expect(validSpy).toHaveBeenCalledTimes(1);
12011255
});
12021256
});
12031257

src/Formsy.ts

+18-30
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
IUpdateInputsWithValue,
1313
ValidationError,
1414
} from './interfaces';
15-
import { throttle, isObject, isString } from './utils';
15+
import { debounce, isObject, isString } from './utils';
1616
import * as utils from './utils';
1717
import validationRules from './validationRules';
1818
import { PassDownProps } from './withFormsy';
@@ -103,7 +103,7 @@ export class Formsy extends React.Component<FormsyProps, FormsyState> {
103103
formElement: 'form',
104104
};
105105

106-
private readonly throttledValidateForm: () => void;
106+
private readonly debouncedValidateForm: () => void;
107107

108108
public constructor(props: FormsyProps) {
109109
super(props);
@@ -122,7 +122,7 @@ export class Formsy extends React.Component<FormsyProps, FormsyState> {
122122
};
123123
this.inputs = [];
124124
this.emptyArray = [];
125-
this.throttledValidateForm = throttle(this.validateForm, ONE_RENDER_FRAME);
125+
this.debouncedValidateForm = debounce(this.validateForm, ONE_RENDER_FRAME);
126126
}
127127

128128
public componentDidMount = () => {
@@ -336,20 +336,15 @@ export class Formsy extends React.Component<FormsyProps, FormsyState> {
336336
onChange(this.getModel(), this.isChanged());
337337
}
338338

339-
// Will be triggered immediately & every one frame rate
340-
this.throttledValidateForm();
339+
this.debouncedValidateForm();
341340
};
342341

343342
// Method put on each input component to unregister
344343
// itself from the form
345344
public detachFromForm = <V>(component: InputComponent<V>) => {
346-
const componentPos = this.inputs.indexOf(component);
345+
this.inputs = this.inputs.filter((input) => input !== component);
347346

348-
if (componentPos !== -1) {
349-
this.inputs = this.inputs.slice(0, componentPos).concat(this.inputs.slice(componentPos + 1));
350-
}
351-
352-
this.throttledValidateForm();
347+
this.debouncedValidateForm();
353348
};
354349

355350
// Checks if the values have changed from their initial value
@@ -437,7 +432,7 @@ export class Formsy extends React.Component<FormsyProps, FormsyState> {
437432
// and check their state
438433
public validateForm = () => {
439434
// We need a callback as we are validating all inputs again. This will
440-
// run when the last component has set its state
435+
// run when the last input has set its state
441436
const onValidationComplete = () => {
442437
const allIsValid = this.inputs.every((component) => component.state.isValid);
443438

@@ -449,24 +444,17 @@ export class Formsy extends React.Component<FormsyProps, FormsyState> {
449444
});
450445
};
451446

452-
// Run validation again in case affected by other inputs. The
453-
// last component validated will run the onValidationComplete callback
454-
this.inputs.forEach((component, index) => {
455-
const validationState = this.runValidation(component);
456-
const isFinalInput = index === this.inputs.length - 1;
457-
const callback = isFinalInput ? onValidationComplete : null;
458-
component.setState(validationState, callback);
459-
});
460-
461-
// If there are no inputs, set state where form is ready to trigger
462-
// change event. New inputs might be added later
463-
if (!this.inputs.length) {
464-
this.setState(
465-
{
466-
canChange: true,
467-
},
468-
onValidationComplete,
469-
);
447+
if (this.inputs.length === 0) {
448+
onValidationComplete();
449+
} else {
450+
// Run validation again in case affected by other inputs. The
451+
// last component validated will run the onValidationComplete callback
452+
this.inputs.forEach((component, index) => {
453+
const validationState = this.runValidation(component);
454+
const isLastInput = index === this.inputs.length - 1;
455+
const callback = isLastInput ? onValidationComplete : null;
456+
component.setState(validationState, callback);
457+
});
470458
}
471459
};
472460

src/utils.ts

+7-9
Original file line numberDiff line numberDiff line change
@@ -143,14 +143,12 @@ export function runRules<V>(
143143
return results;
144144
}
145145

146-
export function throttle(callback, interval) {
147-
let enableCall = true;
148-
149-
return function (...args) {
150-
if (!enableCall) return;
151-
152-
enableCall = false;
153-
callback.apply(this, args);
154-
setTimeout(() => (enableCall = true), interval);
146+
export function debounce(callback, timeout: number) {
147+
let timer;
148+
return (...args) => {
149+
clearTimeout(timer);
150+
timer = setTimeout(() => {
151+
callback.apply(this, args);
152+
}, timeout);
155153
};
156154
}

0 commit comments

Comments
 (0)