Repository: jaredpalmer/formik
Pull request #477 (2018-03-02 13:36:58)

Add submitCount to Formik
Adds a property to state, which tracks number of times user tried to submit the form.

If we agree on the implementation, I will add related tests & documentation 🤖.

### WIP
- [x] Tests
- [x] Documentation

### Related issues
Resolves https://github.com/jaredpalmer/formik/issues/375

prichodko: @jaredpalmer I added necessary tests and also renamed test file, so it matches with this change - https://github.com/jaredpalmer/formik/pull/452.

However, @jaredpalmer could you elaborate how doctoc works? When I run it, I don't see any changes made.
jaredpalmer: you need to run `doctoc readme.md`
jaredpalmer: What happens when resetForm is called?
prichodko: Great catch @jaredpalmer, thanks. Fixed and test added.

Patch:
diff --git a/README.md b/README.md
index 9cb6d18..dc0afd7 100644
--- a/README.md
+++ b/README.md
@@ -326,19 +326,20 @@ npm install yup --save
       - [`handleBlur: (e: any) => void`](#handleblur-e-any--void)
       - [`handleChange: (e: React.ChangeEvent<any>) => void`](#handlechange-e-reactchangeeventany--void)
       - [`handleReset: () => void`](#handlereset---void)
-      - [`handleSubmit: (e: React.FormEvent<HTMLFormEvent> | undefined) => void`](#handlesubmit-e-reactformeventhtmlformevent--void)
+      - [`handleSubmit: (e: React.FormEvent<HTMLFormEvent>) => void`](#handlesubmit-e-reactformeventhtmlformevent--void)
       - [`isSubmitting: boolean`](#issubmitting-boolean)
       - [`isValid: boolean`](#isvalid-boolean)
       - [`resetForm: (nextValues?: Values) => void`](#resetform-nextvalues-values--void)
       - [`setErrors: (fields: { [field: string]: string }) => void`](#seterrors-fields--field-string-string---void)
       - [`setFieldError: (field: string, errorMsg: string) => void`](#setfielderror-field-string-errormsg-string--void)
       - [`setFieldTouched: (field: string, isTouched: boolean, shouldValidate?: boolean) => void`](#setfieldtouched-field-string-istouched-boolean-shouldvalidate-boolean--void)
+      - [`submitForm: () => void`](#submitform---void)
+      - [`submitCount: number`](#submitcount-number)
       - [`setFieldValue: (field: string, value: any, shouldValidate?: boolean) => void`](#setfieldvalue-field-string-value-any-shouldvalidate-boolean--void)
       - [`setStatus: (status?: any) => void`](#setstatus-status-any--void)
       - [`setSubmitting: (isSubmitting: boolean) => void`](#setsubmitting-issubmitting-boolean--void)
       - [`setTouched: (fields: { [field: string]: boolean }) => void`](#settouched-fields--field-string-boolean---void)
       - [`setValues: (fields: { [field: string]: any }) => void`](#setvalues-fields--field-string-any---void)
-      - [`submitForm`](#submitform-fields--field-string-any---void)
       - [`status?: any`](#status-any)
       - [`touched: { [field: string]: boolean }`](#touched--field-string-boolean-)
       - [`values: { [field: string]: any }`](#values--field-string-any-)
@@ -972,6 +973,11 @@ Set the touched state of a field imperatively. `field` should match the key of
 
 Trigger a form submission.
 
+##### `submitCount: number`
+
+Number of times user tried to submit the form. Increases when [`handleSubmit`](#handlesubmit-values-values-formikbag-formikbag--void) is called, resets after calling  
+[`handleReset`](#handlereset---void). `submitCount` is readonly computed property and should not be mutated directly.
+
 ##### `setFieldValue: (field: string, value: any, shouldValidate?: boolean) => void`
 
 Set the value of a field imperatively. `field` should match the key of
@@ -1312,7 +1318,7 @@ slightly relaxed and allow you to return a `Function` (e.g. `i18n('invalid')`).
 
 ```jsx
 import React from 'react';
-import { Formik, Form, Field, FieldArray } from 'formik'
+import { Formik, Form, Field, FieldArray } from 'formik';
 
 // Here is an example of a form with an editable list.
 // Next to each input are buttons for insert and remove.
@@ -1331,7 +1337,7 @@ export const FriendList = () => (
         <FieldArray
           name="friends"
           render={arrayHelpers => (
-          <Form>
+            <Form>
               {values.friends && values.friends.length > 0 ? (
                 values.friends.map((friend, index) => (
                   <div>
@@ -1351,10 +1357,7 @@ export const FriendList = () => (
                   </div>
                 ))
               ) : (
-                <button
-                  type="button"
-                  onClick={() => arrayHelpers.push('')}
-                >
+                <button type="button" onClick={() => arrayHelpers.push('')}>
                   {/* show this when user has removed all friends from the list */}
                   Add a friend
                 </button>
diff --git a/examples/MultistepWizard.js b/examples/MultistepWizard.js
index bb31a2c..03b93bf 100644
--- a/examples/MultistepWizard.js
+++ b/examples/MultistepWizard.js
@@ -64,6 +64,7 @@ class Wizard extends React.Component {
       <Formik
         initialValues={values}
         enableReinitialize={false}
+        validate={this.validate}
         onSubmit={this.handleSubmit}
         render={({ values, handleSubmit, isSubmitting, handleReset }) => (
           <form onSubmit={handleSubmit}>
diff --git a/src/Formik.tsx b/src/Formik.tsx
index b9800de..15c10df 100644
--- a/src/Formik.tsx
+++ b/src/Formik.tsx
@@ -55,6 +55,8 @@ export interface FormikState<Values> {
   isSubmitting: boolean;
   /** Top level status state, in case you need it */
   status?: any;
+  /** Number of times user tried to submit the form */
+  submitCount: number;
 }
 
 /**
@@ -288,6 +290,7 @@ export class Formik<ExtraProps = {}, Values = object> extends React.Component<
       errors: {},
       touched: {},
       isSubmitting: false,
+      submitCount: 0,
     };
     this.fields = {};
     this.initialValues = props.initialValues || ({} as any);
@@ -499,13 +502,14 @@ export class Formik<ExtraProps = {}, Values = object> extends React.Component<
 
   submitForm = () => {
     // Recursively set all values to `true`.
-    this.setState({
+    this.setState(prevState => ({
       touched: setNestedObjectValues<FormikTouched<Values>>(
-        this.state.values,
+        prevState.values,
         true
       ),
       isSubmitting: true,
-    });
+      submitCount: prevState.submitCount + 1,
+    }));
 
     if (this.props.validate) {
       const maybePromisedErrors =
@@ -615,6 +619,7 @@ export class Formik<ExtraProps = {}, Values = object> extends React.Component<
       error: undefined,
       status: undefined,
       values,
+      submitCount: 0,
     });
 
     Object.keys(this.fields).map(f => this.fields[f]());


Test patch:
diff --git a/test/formik.test.tsx b/test/Formik.test.tsx
similarity index 96%
rename from test/formik.test.tsx
rename to test/Formik.test.tsx
index 3f55d3d..a09ea69 100644
--- a/test/formik.test.tsx
+++ b/test/Formik.test.tsx
@@ -68,6 +68,7 @@ describe('<Formik>', () => {
     expect(tree.find(Form).props().errors).toEqual({});
     expect(tree.find(Form).props().dirty).toBe(false);
     expect(tree.find(Form).props().isValid).toBe(false);
+    expect(tree.find(Form).props().submitCount).toBe(0);
   });
 
   describe('FormikHandlers', () => {
@@ -852,6 +853,21 @@ describe('<Formik>', () => {
       expect(tree.find(Form).props().dirty).toBe(true);
       expect(tree.find(Form).props().isValid).toBe(true);
     });
+
+    it('should increase submitCount after submitting the form', async () => {
+      const tree = shallow(BasicForm);
+      expect(tree.find(Form).props().submitCount).toBe(0);
+      await tree
+        .find(Form)
+        .props()
+        .submitForm();
+      expect(
+        tree
+          .update()
+          .find(Form)
+          .props().submitCount
+      ).toBe(1);
+    });
   });
 
   describe('componentWillReceiveProps', () => {
@@ -1044,5 +1060,33 @@ describe('<Formik>', () => {
           .props().dirty
       ).toEqual(false);
     });
+
+    it('should reset submitCount', () => {
+      const tree = mount(<WithState />);
+
+      tree
+        .find(Form)
+        .props()
+        .handleSubmit();
+
+      expect(
+        tree
+          .update()
+          .find(Form)
+          .props().submitCount
+      ).toEqual(1);
+
+      tree
+        .find(Form)
+        .props()
+        .handleReset();
+
+      expect(
+        tree
+          .update()
+          .find(Form)
+          .props().submitCount
+      ).toEqual(0);
+    });
   });
 });