From b4b1dc0c49c3424015e9d11beff46a7aa1d7cec1 Mon Sep 17 00:00:00 2001 From: Jared Palmer Date: Tue, 28 Jan 2020 16:22:12 -0500 Subject: [PATCH 01/51] Add parse, format, formatOnBlur to getFieldProps() --- packages/formik/src/Formik.tsx | 76 +++++++++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 5 deletions(-) diff --git a/packages/formik/src/Formik.tsx b/packages/formik/src/Formik.tsx index 7cbef8ea5..35fa15cfc 100755 --- a/packages/formik/src/Formik.tsx +++ b/packages/formik/src/Formik.tsx @@ -24,6 +24,7 @@ import { getActiveElement, getIn, isObject, + isInputEvent, } from './utils'; import { FormikProvider } from './FormikContext'; import invariant from 'tiny-warning'; @@ -51,6 +52,11 @@ type FormikMessage = payload: FormikState; }; +const defaultParseFn = (value: unknown, _name: string) => value; + +const defaultFormatFn = (value: unknown, _name: string) => + value === undefined ? '' : value; + // State reducer function formikReducer( state: FormikState, @@ -892,22 +898,46 @@ export function useFormik({ [state.errors, state.touched, state.values] ); - const getFieldHelpers = React.useCallback( + const getFieldHelpers = useEventCallback( (name: string): FieldHelperProps => { return { setValue: (value: any) => setFieldValue(name, value), setTouched: (value: boolean) => setFieldTouched(name, value), setError: (value: any) => setFieldError(name, value), }; - }, - [setFieldValue, setFieldTouched, setFieldError] + } + ); + + const getValueFromEvent = useEventCallback( + (event: React.SyntheticEvent, fieldName: string) => { + if (event.persist) { + event.persist(); + } + const target = event.target ? event.target : event.currentTarget; + const { type, value, checked, options, multiple } = target; + let val; + let parsed; + val = /number|range/.test(type) + ? ((parsed = parseFloat(value)), isNaN(parsed) ? '' : parsed) + : /checkbox/.test(type) // checkboxes + ? getValueForCheckbox(getIn(state.values, fieldName!), checked, value) + : !!multiple // ? getSelectedValues(options) : value; - return val; } ); @@ -963,7 +964,7 @@ export function useFormik({ value: valueProp, // value is special for checkboxes as: is, multiple, - parse = defaultParseFn, + parse = /number|range/.test(type) ? numberParseFn : defaultParseFn, format = defaultFormatFn, formatOnBlur = false, } = nameOrOptions; diff --git a/packages/formik/test/Field.test.tsx b/packages/formik/test/Field.test.tsx index 3d227caeb..df11f2f47 100644 --- a/packages/formik/test/Field.test.tsx +++ b/packages/formik/test/Field.test.tsx @@ -124,11 +124,11 @@ describe('Field / FastField', () => { ); - const { handleBlur, handleChange } = getFormProps(); + const { handleBlur } = getFormProps(); injected.forEach((props, idx) => { expect(props.field.name).toBe('name'); expect(props.field.value).toBe('jared'); - expect(props.field.onChange).toBe(handleChange); + expect(props.field.onChange).toEqual(expect.any(Function)); expect(props.field.onBlur).toBe(handleBlur); expect(props.form).toEqual(getFormProps()); if (idx !== 2) { @@ -145,7 +145,7 @@ describe('Field / FastField', () => { expect(asInjectedProps.name).toBe('name'); expect(asInjectedProps.value).toBe('jared'); - expect(asInjectedProps.onChange).toBe(handleChange); + expect(asInjectedProps.onChange).toEqual(expect.any(Function)); expect(asInjectedProps.onBlur).toBe(handleBlur); expect(queryAllByText(TEXT)).toHaveLength(4); @@ -170,11 +170,11 @@ describe('Field / FastField', () => { ); - const { handleBlur, handleChange } = getFormProps(); + const { handleBlur } = getFormProps(); injected.forEach((props, idx) => { expect(props.field.name).toBe('name'); expect(props.field.value).toBe('jared'); - expect(props.field.onChange).toBe(handleChange); + expect(props.field.onChange).toEqual(expect.any(Function)); expect(props.field.onBlur).toBe(handleBlur); expect(props.form).toEqual(getFormProps()); if (idx !== 2) { @@ -191,7 +191,7 @@ describe('Field / FastField', () => { expect(asInjectedProps.name).toBe('name'); expect(asInjectedProps.value).toBe('jared'); - expect(asInjectedProps.onChange).toBe(handleChange); + expect(asInjectedProps.onChange).toEqual(expect.any(Function)); expect(asInjectedProps.onBlur).toBe(handleBlur); expect(queryAllByText(TEXT)).toHaveLength(4); }); From d750a8f8124845bc1f82be116633b1de4360937a Mon Sep 17 00:00:00 2001 From: Jared Palmer Date: Thu, 17 Sep 2020 10:31:30 -0400 Subject: [PATCH 07/51] Migate context to use-context-selector --- packages/formik/package.json | 3 ++- packages/formik/src/Formik.tsx | 13 ++++++++++--- packages/formik/src/FormikContext.tsx | 21 ++++++++++++++++++--- packages/formik/src/connect.tsx | 15 +++++++-------- yarn.lock | 17 +++++++---------- 5 files changed, 44 insertions(+), 25 deletions(-) diff --git a/packages/formik/package.json b/packages/formik/package.json index b434ff6bd..e8a4a52d8 100644 --- a/packages/formik/package.json +++ b/packages/formik/package.json @@ -48,7 +48,8 @@ "react-fast-compare": "^2.0.1", "scheduler": "^0.18.0", "tiny-warning": "^1.0.2", - "tslib": "^1.10.0" + "tslib": "^1.10.0", + "use-context-selector": "^1.1.2" }, "resolutions": { "@types/react": "16.9.17", diff --git a/packages/formik/src/Formik.tsx b/packages/formik/src/Formik.tsx index f27a8d7cc..20a2bf533 100755 --- a/packages/formik/src/Formik.tsx +++ b/packages/formik/src/Formik.tsx @@ -12,7 +12,8 @@ import { FieldMetaProps, FieldHelperProps, FieldInputProps, - FormikHelpers, FormikHandlers, + FormikHelpers, + FormikHandlers, } from './types'; import { isFunction, @@ -455,7 +456,13 @@ export function useFormik({ validateFormWithLowPriority(initialValues.current); } } - }, [enableReinitialize, props.initialValues, resetForm, validateOnMount, validateFormWithLowPriority]); + }, [ + enableReinitialize, + props.initialValues, + resetForm, + validateOnMount, + validateFormWithLowPriority, + ]); React.useEffect(() => { if ( @@ -621,7 +628,7 @@ export function useFormik({ if (!isString(eventOrTextValue)) { // If we can, persist the event // @see https://reactjs.org/docs/events.html#event-pooling - if ((eventOrTextValue as React.ChangeEvent).persist) { + if (!!(eventOrTextValue as React.ChangeEvent).persist) { (eventOrTextValue as React.ChangeEvent).persist(); } const target = eventOrTextValue.target diff --git a/packages/formik/src/FormikContext.tsx b/packages/formik/src/FormikContext.tsx index 7c27ae3c6..7daab0da3 100644 --- a/packages/formik/src/FormikContext.tsx +++ b/packages/formik/src/FormikContext.tsx @@ -1,15 +1,30 @@ import * as React from 'react'; import { FormikContextType } from './types'; import invariant from 'tiny-warning'; +import { createContext, useContextSelector } from 'use-context-selector'; -export const FormikContext = React.createContext>( +export const FormikContext = createContext>( undefined as any ); export const FormikProvider = FormikContext.Provider; -export const FormikConsumer = FormikContext.Consumer; + +export function FormikConsumer({ + children, +}: { + children: (formik: FormikContextType) => React.ReactNode; +}) { + const formik = useContextSelector( + FormikContext, + React.useCallback(c => c, []) + ) as FormikContextType; + return <>{children(formik)}; +} export function useFormikContext() { - const formik = React.useContext>(FormikContext); + const formik = useContextSelector( + FormikContext, + React.useCallback(c => c, []) + ) as FormikContextType; invariant( !!formik, diff --git a/packages/formik/src/connect.tsx b/packages/formik/src/connect.tsx index a009d0e76..8bc51892d 100644 --- a/packages/formik/src/connect.tsx +++ b/packages/formik/src/connect.tsx @@ -1,9 +1,8 @@ -import * as React from 'react'; import hoistNonReactStatics from 'hoist-non-react-statics'; - -import { FormikContextType } from './types'; -import { FormikConsumer } from './FormikContext'; +import * as React from 'react'; import invariant from 'tiny-warning'; +import { FormikConsumer } from './FormikContext'; +import { FormikContextType } from './types'; /** * Connect any component to Formik context, and inject as a prop called `formik`; @@ -12,16 +11,16 @@ import invariant from 'tiny-warning'; export function connect( Comp: React.ComponentType }> ) { - const C: React.SFC = (props: OuterProps) => ( - - {formik => { + const C: React.FC = (props: OuterProps) => ( + ) => { invariant( !!formik, `Formik context is undefined, please verify you are rendering
, , , , or your custom context-using component as a child of a component. Component name: ${Comp.name}` ); return ; }} - + /> ); const componentDisplayName = Comp.displayName || diff --git a/yarn.lock b/yarn.lock index 0892bed93..1197ba708 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2173,7 +2173,7 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== -"@types/react-dom@^16.9.4": +"@types/react-dom@16.9.4", "@types/react-dom@^16.9.4": version "16.9.4" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.4.tgz#0b58df09a60961dcb77f62d4f1832427513420df" integrity sha512-fya9xteU/n90tda0s+FtN5Ym4tbgxpq/hb/Af24dvs6uYnYn+fspaxw5USlw0R8apDNwxsqumdRoCoKitckQqw== @@ -2188,15 +2188,7 @@ "@types/prop-types" "*" "@types/react" "*" -"@types/react@*", "@types/react@^16.8.23": - version "16.9.16" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.16.tgz#4f12515707148b1f53a8eaa4341dae5dfefb066d" - integrity sha512-dQ3wlehuBbYlfvRXfF5G+5TbZF3xqgkikK7DWAsQXe2KnzV+kjD4W2ea+ThCrKASZn9h98bjjPzoTYzfRqyBkw== - dependencies: - "@types/prop-types" "*" - csstype "^2.2.0" - -"@types/react@^16.9.17": +"@types/react@*", "@types/react@16.9.17", "@types/react@^16.8.23", "@types/react@^16.9.17": version "16.9.17" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.17.tgz#58f0cc0e9ec2425d1441dd7b623421a867aa253e" integrity sha512-UP27In4fp4sWF5JgyV6pwVPAQM83Fj76JOcg02X5BZcpSu5Wx+fP9RMqc2v0ssBoQIFvD5JdKY41gjJJKmw6Bg== @@ -12230,6 +12222,11 @@ url-to-options@^1.0.1: resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" integrity sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k= +use-context-selector@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/use-context-selector/-/use-context-selector-1.1.2.tgz#63bf6e3502fd383f1257c49df2495e05564d262e" + integrity sha512-uwtrMKEHjZQ8JXMZLUljVDY4CqpAC3MgMcmtpZACG+pJeJ7GNDzBW0thC2RbJX2wKYMvLtYRXgQEqaOnmlF/KQ== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" From e2fe6720d4c25c30026802dc47225c52d04d1500 Mon Sep 17 00:00:00 2001 From: Jared Palmer Date: Tue, 27 Oct 2020 13:57:23 -0400 Subject: [PATCH 08/51] Enter prerelease mode --- .changeset/pre.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/pre.json diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 000000000..dfc5034dd --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,9 @@ +{ + "mode": "pre", + "tag": "next", + "initialVersions": { + "formik": "2.2.1", + "formik-native": "2.1.9" + }, + "changesets": [] +} From 5efd691b8784fda6645d362189f55c618f030758 Mon Sep 17 00:00:00 2001 From: Jared Palmer Date: Tue, 27 Oct 2020 14:27:07 -0400 Subject: [PATCH 09/51] Add changeset --- .changeset/quiet-beans-attack.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/quiet-beans-attack.md diff --git a/.changeset/quiet-beans-attack.md b/.changeset/quiet-beans-attack.md new file mode 100644 index 000000000..00169e094 --- /dev/null +++ b/.changeset/quiet-beans-attack.md @@ -0,0 +1,8 @@ +--- +'formik': major +--- + +Added `parse`, `format`, and `formatOnBlur` to `getFieldProps` options, ``, and `useField`. Going forward, there is no reason aside from backwards compatibility to continue using either `formikProps.handleChange` or `formikProps.handleBlur`. These are both inferior to the `onChange` and `onBlur` functions returned by `getFieldProps()` which the ability to utilize `parse`, `format`, and `formatOnBlur`. + +**Breaking Change** +Instead of just passing back `formikProps.handleChange` and `formikProps.handleBlur`, the `onChange` and `onBlur` handlers returned by `getFieldProps()` (and thus `useField`/``) are now scoped to the field already and now accept either a React Synthetic event or a value. In the past, you could need to curry the handler with the string name of field to get this functionality. This likely doesn't impact many users, but it is technically breaking nonetheless. From 18f82f87a535b4206c47837933d99833a46aca24 Mon Sep 17 00:00:00 2001 From: Jared Palmer Date: Tue, 27 Oct 2020 14:27:49 -0400 Subject: [PATCH 10/51] Format string --- examples/format-string-by-pattern/index.js | 6 +++--- examples/parse-format/index.js | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/format-string-by-pattern/index.js b/examples/format-string-by-pattern/index.js index d3da73e99..ab0703d96 100644 --- a/examples/format-string-by-pattern/index.js +++ b/examples/format-string-by-pattern/index.js @@ -9,7 +9,7 @@ const masks = [ { name: 'phone-3', parse: '+49 (AAAA) BBBBBB' }, ]; -const sleep = ms => new Promise(r => setTimeout(r, ms)); +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const Example = () => { return ( @@ -19,13 +19,13 @@ const Example = () => { prev[curr.name] = ''; return prev; }, {})} - onSubmit={async values => { + onSubmit={async (values) => { await sleep(500); alert(JSON.stringify(values, null, 2)); }} render={({ values }) => ( - {masks.map(mask => ( + {masks.map((mask) => (
); }; From d12154623c1ed22f135ea50724a9ce64f776eff8 Mon Sep 17 00:00:00 2001 From: Jared Palmer Date: Tue, 27 Oct 2020 15:21:46 -0400 Subject: [PATCH 13/51] Fix parse and format in Field component --- app/package.json | 1 + app/pages/format.tsx | 46 ++++++++++++++++++++++++++++++ app/pages/index.tsx | 5 ++++ cypress/integration/format.spec.ts | 15 ++++++++++ packages/formik/src/Field.tsx | 18 +++++++----- 5 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 app/pages/format.tsx create mode 100644 cypress/integration/format.spec.ts diff --git a/app/package.json b/app/package.json index d29ce15bb..c7a78a18e 100644 --- a/app/package.json +++ b/app/package.json @@ -8,6 +8,7 @@ "start": "next start" }, "dependencies": { + "format-string-by-pattern": "^1.2.1", "formik": "^2.1.5", "next": "9.5.3", "react": "16.13.1", diff --git a/app/pages/format.tsx b/app/pages/format.tsx new file mode 100644 index 000000000..35f42e3f4 --- /dev/null +++ b/app/pages/format.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { Formik, Field, Form } from 'formik'; +import formatString from 'format-string-by-pattern'; + +const masks = [ + { name: 'phone-1', parse: '999-999-9999' }, + { name: 'phone-2', parse: '(999) 999-9999' }, + { name: 'phone-3', parse: '+49 (AAAA) BBBBBB' }, +]; + +const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); + +export default function Example() { + return ( +
+ { + prev[curr.name] = ''; + return prev; + }, {} as any)} + onSubmit={async values => { + await sleep(300); + alert(JSON.stringify(values, null, 2)); + }} + > +
+ {masks.map(mask => ( +
+ +
+ ))} + + +
+
+
+ ); +} diff --git a/app/pages/index.tsx b/app/pages/index.tsx index 30daffeed..cffed6a9f 100644 --- a/app/pages/index.tsx +++ b/app/pages/index.tsx @@ -16,6 +16,11 @@ function Home() { Async Submission +
  • + + Parse + Format + +