Skip to content

Commit

Permalink
Closes #94. Add isValid prop and isInitialValid option (#97)
Browse files Browse the repository at this point in the history
* Closes #94. Add isValid prop and isInitialValid option

* Allow isInitialValid to take a boolean or function
  • Loading branch information
jaredpalmer authored Jul 31, 2017
1 parent afb6363 commit f8aaf48
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 16 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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`.

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
18 changes: 15 additions & 3 deletions src/formik.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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}
Expand Down
19 changes: 6 additions & 13 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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) {
Expand All @@ -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;
29 changes: 29 additions & 0 deletions test/formik.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down

0 comments on commit f8aaf48

Please # to comment.