Repository: jaredpalmer/formik (ts)

Issue:
RFC: isValid prop for the whole form
Hey guys!

First of all, we've really enjoyed using Formik in our products. It's made form submission so much easier. One pain point that we have in our usage though is that there's no easy way to always tell if the whole form is valid. 

This would be nice for doing something like disabling the submit button based off one prop instead of having to check for other things. For example, as far as I can tell nothing shows up in the errors array until at least one field has been typed in. So although the form is invalid, you can't tell that just by checking the errors array.

Just wanted to see if anyone else thought something like that would be a good idea... Thanks again for the awesome library!

Pull request:
Closes #94. Add isValid prop and isInitialValid option

Patch:
diff --git a/README.md b/README.md
index 2d3439c..2ba221b 100644
--- a/README.md
+++ b/README.md
@@ -46,6 +46,7 @@ You can also try before you buy with this **[demo of Formik on CodeSandbox.io](h
       - [`displayName?: string`](#displayname-string)
       - [`handleSubmit: (values: Values, formikBag: FormikBag) => void`](#handlesubmit-values-values-formikbag-formikbag--void)
         - [The "FormikBag":](#the-formikbag)
+      - [`isInitialValid?: boolean | (props: Props) => boolean`](#isinitialvalid-boolean--props-props--boolean)
       - [`mapPropsToValues?: (props: Props) => Values`](#mappropstovalues-props-props--values)
       - [`validate?: (values: Values, props: Props) => FormikError<Values> | Promise<any>`](#validate-values-values-props-props--formikerrorvalues--promiseany)
       - [`validateOnBlur?: boolean`](#validateonblur-boolean)
@@ -59,6 +60,7 @@ You can also try before you buy with this **[demo of Formik on CodeSandbox.io](h
       - [`handleReset: () => void`](#handlereset---void)
       - [`handleSubmit: (e: React.FormEvent<HTMLFormEvent>) => void`](#handlesubmit-e-reactformeventhtmlformevent--void)
       - [`isSubmitting: boolean`](#issubmitting-boolean)
+      - [`isValid: boolean`](#isvalid-boolean)
       - [`resetForm: (nextProps?: Props) => void`](#resetform-nextprops-props--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)
@@ -255,6 +257,10 @@ Your form submission handler. It is passed your forms [`values`] and the "Formik
 
 Note: [`errors`], [`touched`], [`status`] and all event handlers are NOT included in the `FormikBag`.
 
+##### `isInitialValid?: boolean | (props: Props) => boolean`
+
+Default is `false`. Control the initial value of [`isValid`] prop prior to mount. You can also pass a function. Useful for situations when you want to enable/disable a submit and reset buttons on initial mount.
+
 ##### `mapPropsToValues?: (props: Props) => Values`
 
 If this option is specified, then Formik will transfer its results into updatable form state and make these values available to the new component as [`props.values`][`values`]. If `mapPropsToValues` is not specified, then Formik will map all props that are not functions to the inner component's [`props.values`][`values`]. That is, if you omit it, Formik will only pass `props` where `typeof props[k] !== 'function'`, where `k` is some key. 
@@ -353,6 +359,10 @@ Submit handler. This should be passed to `<form onSubmit={handleSubmit}>...</for
 ##### `isSubmitting: boolean`
 Submitting state. Either `true` or `false`. Formik will set this to `true` on your behalf before calling [`handleSubmit`] to reduce boilerplate.
 
+##### `isValid: boolean` 
+
+Returns `true` if the there are no [`errors`], or the result of [`isInitialValid`] the form if is in "pristine" condition (i.e. not [`dirty`])).
+
 ##### `resetForm: (nextProps?: Props) => void`
 Imperatively reset the form. This will clear [`errors`] and [`touched`], set [`isSubmitting`] to `false` and rerun `mapPropsToValues` with the current `WrappedComponent`'s `props` or what's passed as an argument. That latter is useful for calling `resetForm` within `componentWillReceiveProps`.
 
@@ -669,6 +679,7 @@ MIT License.
 [`displayName`]: #displayname-string
 [`handleSubmit`]: #handlesubmit-payload-formikbag--void
 [`FormikBag`]: #the-formikbag
+[`isInitialValid`]: #isinitialvalid-boolean--props-props--boolean
 [`mapPropsToValues`]: #mappropstovalues-props--props
 [`validate`]: #validate-values-values-props-props--formikerrorvalues--promiseany
 [`validateOnBlur`]: #validateonblur-boolean
@@ -684,6 +695,7 @@ MIT License.
 [`handleReset`]: #handlereset---void
 [`handleSubmit`]: #handlesubmit-e-reactformeventhtmlformevent--void
 [`isSubmitting`]: #issubmitting-boolean
+[`isValid`]: #isvalid-boolean
 [`resetForm`]: #resetform-nextprops-props--void
 [`setErrors`]: #seterrors-fields--field-string-string---void
 [`setFieldError`]: #setfielderror-field-string-errormsg-string--void
diff --git a/package.json b/package.json
index 3407e63..c08d9d1 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "formik",
   "description": "Forms in React, without tears",
-  "version": "0.8.5",
+  "version": "0.8.6",
   "license": "MIT",
   "author": "Jared Palmer <jared@palmer.net>",
   "repository": "jaredpalmer/formik",
diff --git a/src/formik.tsx b/src/formik.tsx
index 10b60a4..5be202c 100644
--- a/src/formik.tsx
+++ b/src/formik.tsx
@@ -1,6 +1,6 @@
 import * as React from 'react';
 
-import { isPromise, isReactNative, values } from './utils';
+import { isFunction, isPromise, isReactNative, values } from './utils';
 
 import { hoistNonReactStatics } from './hoistStatics';
 
@@ -61,6 +61,8 @@ export interface FormikConfig<
   validateOnChange?: boolean;
   /** Tells Formik to validate the form on each input's onBlur event */
   validateOnBlur?: boolean;
+  /** Tell Formik if initial form values are valid or not on first render */
+  isInitialValid?: boolean | ((props: Props) => boolean | undefined);
 }
 
 /**
@@ -90,6 +92,8 @@ export interface FormikState<Values> {
 export interface FormikComputedProps {
   /** True if any input has been touched. False otherwise. */
   readonly dirty: boolean;
+  /** Result of isInitiallyValid on mount, then whether true values pass validation. */
+  readonly isValid: boolean;
 }
 
 /**
@@ -187,6 +191,7 @@ export function Formik<Props, Values extends FormikValues, Payload = Values>({
   handleSubmit,
   validateOnChange = false,
   validateOnBlur = true,
+  isInitialValid = false,
 }: FormikConfig<Props, Values, Payload>): ComponentDecorator<
   Props,
   InjectedFormikProps<Props, Values>
@@ -280,7 +285,7 @@ export function Formik<Props, Values extends FormikValues, Payload = Values>({
         if (validate) {
           const maybePromisedErrors = validate(values, this.props);
           if (isPromise(maybePromisedErrors)) {
-            (validate(values, this.props) as Promise<any>).then(
+            (maybePromisedErrors as Promise<any>).then(
               () => {
                 this.setState({ errors: {} });
               },
@@ -398,7 +403,7 @@ Formik cannot determine which value to update. For more info see https://github.
           const maybePromisedErrors =
             validate(this.state.values, this.props) || {};
           if (isPromise(maybePromisedErrors)) {
-            (validate(this.state.values, this.props) as Promise<any>).then(
+            (maybePromisedErrors as Promise<any>).then(
               () => {
                 this.setState({ errors: {} });
                 this.executeSubmit();
@@ -515,12 +520,19 @@ Formik cannot determine which value to update. For more info see https://github.
       };
 
       render() {
+        const dirty =
+          values<boolean>(this.state.touched).filter(Boolean).length > 0;
         return (
           <WrappedComponent
             {...this.props}
             {...this.state}
-            dirty={
-              values<boolean>(this.state.touched).filter(Boolean).length > 0
+            dirty={dirty}
+            isValid={
+              dirty
+                ? this.state.errors && Object.keys(this.state.errors).length > 0
+                : isInitialValid !== false && isFunction(isInitialValid)
+                  ? (isInitialValid as (props: Props) => boolean)(this.props)
+                  : isInitialValid as boolean
             }
             setStatus={this.setStatus}
             setError={this.setError}
diff --git a/src/utils.ts b/src/utils.ts
index ec9504b..db8b5e0 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,9 +1,4 @@
-/**
- * Returns whether something is a Promise or not.
- * @param value anything
- * 
- * @see https://github.com/pburtchaell/redux-promise-middleware/blob/master/src/isPromise.js
- */
+/** @private is the given object/value a promise? */
 export function isPromise(value: any): boolean {
   if (value !== null && typeof value === 'object') {
     return value && typeof value.then === 'function';
@@ -12,19 +7,14 @@ export function isPromise(value: any): boolean {
   return false;
 }
 
-/**
- * True if running in React Native. 
- */
+/** @private is running React Native?  */
 export const isReactNative =
   typeof window !== 'undefined' &&
   window.navigator &&
   window.navigator.product &&
   window.navigator.product === 'ReactNative';
 
-/**
- * Returns values object in a new array
- * @param obj any object
- */
+/** @private Returns values of an object in a new array */
 export function values<T>(obj: any): T[] {
   const vals = [];
   for (var key in obj) {
@@ -34,3 +24,6 @@ export function values<T>(obj: any): T[] {
   }
   return vals;
 }
+
+/** @private is the given object a Function? */
+export const isFunction = (obj: any) => 'function' === typeof obj;


Test patch:
diff --git a/test/formik.test.tsx b/test/formik.test.tsx
index 43484c9..014f29e 100644
--- a/test/formik.test.tsx
+++ b/test/formik.test.tsx
@@ -79,6 +79,35 @@ describe('Formik', () => {
     expect(tree.find(Form).props().values).toEqual({ name: 'jared' });
     expect(tree.find(Form).props().errors).toEqual({});
     expect(tree.find(Form).props().dirty).toBe(false);
+    expect(tree.find(Form).props().isValid).toBe(false);
+  });
+
+  it('should compute isValid if isInitialValid is present and returns true', () => {
+    const InvalidForm = FormFactory({ isInitialValid: props => true });
+    const tree = shallow(<InvalidForm user={{ name: 'jared' }} />);
+    expect(tree.find(Form).props().dirty).toBe(false);
+    expect(tree.find(Form).props().isValid).toBe(true);
+  });
+
+  it('should compute isValid if isInitialValid is present and returns false', () => {
+    const InvalidForm = FormFactory({ isInitialValid: props => false });
+    const tree = shallow(<InvalidForm user={{ name: 'jared' }} />);
+    expect(tree.find(Form).props().dirty).toBe(false);
+    expect(tree.find(Form).props().isValid).toBe(false);
+  });
+
+  it('should compute isValid if isInitialValid boolean is present and set to true', () => {
+    const InvalidForm = FormFactory({ isInitialValid: true });
+    const tree = shallow(<InvalidForm user={{ name: 'jared' }} />);
+    expect(tree.find(Form).props().dirty).toBe(false);
+    expect(tree.find(Form).props().isValid).toBe(true);
+  });
+
+  it('should compute isValid if isInitialValid is present and set to false', () => {
+    const InvalidForm = FormFactory({ isInitialValid: false });
+    const tree = shallow(<InvalidForm user={{ name: 'jared' }} />);
+    expect(tree.find(Form).props().dirty).toBe(false);
+    expect(tree.find(Form).props().isValid).toBe(false);
   });
 
   describe('FormikHandlers', () => {