Repository: jaredpalmer/formik
Pull request #580 (2018-04-12 13:26:31)

Add innerRef prop to Field & FastField string components
Reopened: https://github.com/jaredpalmer/formik/pull/478


Patch:
diff --git a/README.md b/README.md
index c15502c..1491051 100644
--- a/README.md
+++ b/README.md
@@ -368,6 +368,7 @@ npm install yup --save
     - [`validationSchema?: Schema | (() => Schema)`](#validationschema-schema----schema)
   - [`<Field />`](#field-)
     - [`validate?: (value: any) => undefined | string | Promise<any>`](#validate-value-any--undefined--string--promiseany)
+    - [Refs](#refs)
   - [`<FieldArray/>`](#fieldarray)
       - [`name: string`](#name-string)
       - [`validateOnChange?: boolean`](#validateonchange-boolean-1)
@@ -1322,6 +1323,10 @@ const validate = value => {
 Note: To allow for i18n libraries, the TypeScript typings for `validate` are
 slightly relaxed and allow you to return a `Function` (e.g. `i18n('invalid')`).
 
+#### Refs
+
+When you are **not** using a custom component and you need to access the underlying DOM node created by `Field` (e.g. to call `focus`), pass the callback to the `innerRef` prop instead.
+
 ### `<FieldArray/>`
 
 `<FieldArray />` is a component that helps with common array/list manipulations. You pass it a `name` property with the path to the key within `values` that holds the relevant array. `<FieldArray />` will then give you access to array helper methods via render props. For convenience, calling these methods will trigger validation and also manage `touched` for you.
diff --git a/src/FastField.tsx b/src/FastField.tsx
index 06f3f2e..3c85e7a 100644
--- a/src/FastField.tsx
+++ b/src/FastField.tsx
@@ -33,6 +33,7 @@ export class FastField<
     render: PropTypes.func,
     children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
     validate: PropTypes.func,
+    innerRef: PropTypes.func,
   };
 
   reset: Function;
@@ -292,9 +293,11 @@ export class FastField<
     }
 
     if (typeof component === 'string') {
+      const { innerRef, ...rest } = props;
       return React.createElement(component as any, {
+        ref: innerRef,
         ...field,
-        ...props,
+        ...rest,
         children,
       });
     }
diff --git a/src/Field.tsx b/src/Field.tsx
index 05af2c5..7782bb4 100644
--- a/src/Field.tsx
+++ b/src/Field.tsx
@@ -73,6 +73,9 @@ export interface FieldConfig {
 
   /** Field value */
   value?: any;
+
+  /** Inner ref */
+  innerRef?: (instance: any) => void;
 }
 
 export type FieldAttributes = GenericFieldHTMLAttributes & FieldConfig;
@@ -96,6 +99,7 @@ export class Field<Props extends FieldAttributes = any> extends React.Component<
     render: PropTypes.func,
     children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
     validate: PropTypes.func,
+    innerRef: PropTypes.func,
   };
 
   componentWillMount() {
@@ -181,9 +185,11 @@ export class Field<Props extends FieldAttributes = any> extends React.Component<
     }
 
     if (typeof component === 'string') {
+      const { innerRef, ...rest } = props;
       return React.createElement(component as any, {
+        ref: innerRef,
         ...field,
-        ...props,
+        ...rest,
         children,
       });
     }


Test patch:
diff --git a/test/FastField.test.tsx b/test/FastField.test.tsx
index 5d8e9b5..de74bd6 100644
--- a/test/FastField.test.tsx
+++ b/test/FastField.test.tsx
@@ -3,7 +3,7 @@ import * as ReactDOM from 'react-dom';
 
 import { FastField as Field, FieldProps, Formik, FormikProps } from '../src';
 
-import { shallow } from 'enzyme';
+import { shallow, mount } from 'enzyme';
 import { noop } from './testHelpers';
 
 interface TestFormValues {
@@ -170,6 +170,37 @@ describe('A <Field />', () => {
       expect(actual.field.value).toBe('jared');
       expect(actual.form).toEqual(injected);
     });
+
+    it('assigns innerRef as a ref to string components', () => {
+      const innerRef = jest.fn();
+      const tree = mount(<Field name="name" innerRef={innerRef} />, {
+        context: {
+          formik: {
+            registerField: jest.fn(noop),
+          },
+        },
+      });
+      const element = tree.find('input').instance();
+      expect(innerRef).toHaveBeenCalledWith(element);
+    });
+
+    it('forwards innerRef to React component', () => {
+      let actual: any; /** FieldProps ;) */
+      const Component: React.SFC<FieldProps> = props =>
+        (actual = props) && null;
+
+      const innerRef = jest.fn();
+
+      ReactDOM.render(
+        <TestForm
+          render={() => (
+            <Field name="name" component={Component} innerRef={innerRef} />
+          )}
+        />,
+        node
+      );
+      expect(actual.innerRef).toBe(innerRef);
+    });
   });
 
   describe('<Field render />', () => {
diff --git a/test/Field.test.tsx b/test/Field.test.tsx
index 100aa81..9b47df0 100644
--- a/test/Field.test.tsx
+++ b/test/Field.test.tsx
@@ -2,7 +2,7 @@ import * as React from 'react';
 import * as ReactDOM from 'react-dom';
 import { Formik, Field, FieldProps, FormikProps } from '../src';
 
-import { shallow } from 'enzyme';
+import { shallow, mount } from 'enzyme';
 import { noop } from './testHelpers';
 
 interface TestFormValues {
@@ -90,6 +90,7 @@ describe('A <Field />', () => {
           value: 'ian',
         },
       });
+
       expect(handleBlur).toHaveBeenCalled();
       expect(setFieldError).toHaveBeenCalled();
       expect(validate).toHaveBeenCalled();
@@ -179,6 +180,33 @@ describe('A <Field />', () => {
       expect(actual.field.onBlur).toBe(handleBlur);
       expect(actual.form).toEqual(injected);
     });
+
+    it('assigns innerRef as a ref to string components', () => {
+      const innerRef = jest.fn();
+      const tree = mount(<Field name="name" innerRef={innerRef} />, {
+        context: { formik: {} },
+      });
+      const element = tree.find('input').instance();
+      expect(innerRef).toHaveBeenCalledWith(element);
+    });
+
+    it('forwards innerRef to React component', () => {
+      let actual: any; /** FieldProps ;) */
+      const Component: React.SFC<FieldProps> = props =>
+        (actual = props) && null;
+
+      const innerRef = jest.fn();
+
+      ReactDOM.render(
+        <TestForm
+          render={() => (
+            <Field name="name" component={Component} innerRef={innerRef} />
+          )}
+        />,
+        node
+      );
+      expect(actual.innerRef).toBe(innerRef);
+    });
   });
 
   describe('<Field render />', () => {