Repository: jaredpalmer/formik
Pull request #520 (2018-03-15 20:48:35)

Add replace array helper
Hi, thank you for this great lib!

I've recently encountered a case where I wanted to replace the value of an array and noticed a helper was missing.


Patch:
diff --git a/README.md b/README.md
index 74a361a..c15502c 100644
--- a/README.md
+++ b/README.md
@@ -44,6 +44,14 @@ By now, you might be thinking, "Why didn't you just use
 minimal API that does the really really annoying stuff, and leaves the rest up
 to you.**
 
+## Testimonials
+
+> "I can't believe people ever put forms in redux, or did anything else other than this."  
+> --[James Long](https://twitter.com/jlongster), Creator of Prettier and Actual Budget
+
+> ["Formik. All day. All long."](https://twitter.com/ken_wheeler/status/974676242501947397?s=20)  
+> --[Ken Wheeler](https://twitter.com/ken_wheeler), Director of Open Source at Formidable Labs
+
 ## Influences
 
 Formik started by expanding on
@@ -80,6 +88,8 @@ You can also try before you buy with this
 * [An Introduction to Formik](https://youtu.be/-tDy7ds0dag?t=33s) by
   [Jared Palmer](https://twitter.com/jaredpalmer) @ Spotify NYC. August 15th, 2017.
 
+
+
 ## Community Articles / Tutorials
 
 * [Better React Forms with Formik](https://mead.io/formik/?utm_source=github&utm_campaign=formikrepo)
diff --git a/src/FieldArray.tsx b/src/FieldArray.tsx
index 45610ad..3244d4b 100644
--- a/src/FieldArray.tsx
+++ b/src/FieldArray.tsx
@@ -28,6 +28,10 @@ export interface ArrayHelpers {
   insert: (index: number, value: any) => void;
   /** Curried fn to insert an element at a given index into the array */
   handleInsert: (index: number, value: any) => () => void;
+  /** Imperatively replace a value at an index of an array  */
+  replace: (index: number, value: any) => void;
+  /** Curried fn to replace an element at a given index into the array */
+  handleReplace: (index: number, value: any) => () => void;
   /** Imperatively add an element to the beginning of an array and return its length */
   unshift: (value: any) => number;
   /** Curried fn to add an element to the beginning of an array */
@@ -67,6 +71,12 @@ export const insert = (array: any[], index: number, value: any) => {
   return copy;
 };
 
+export const replace = (array: any[], index: number, value: any) => {
+  const copy = [...(array || [])];
+  copy[index] = value;
+  return copy;
+};
+
 export class FieldArray extends React.Component<FieldArrayConfig, {}> {
   static defaultProps = {
     validateOnChange: true,
@@ -151,6 +161,16 @@ export class FieldArray extends React.Component<FieldArrayConfig, {}> {
 
   handleInsert = (index: number, value: any) => () => this.insert(index, value);
 
+  replace = (index: number, value: any) =>
+    this.updateArrayField(
+      (array: any[]) => replace(array, index, value),
+      false,
+      false
+    );
+
+  handleReplace = (index: number, value: any) => () =>
+    this.replace(index, value);
+
   unshift = (value: any) => {
     let arr = [];
     this.updateArrayField(
@@ -218,6 +238,7 @@ export class FieldArray extends React.Component<FieldArrayConfig, {}> {
       swap: this.swap,
       move: this.move,
       insert: this.insert,
+      replace: this.replace,
       unshift: this.unshift,
       remove: this.remove,
       handlePush: this.handlePush,
@@ -225,6 +246,7 @@ export class FieldArray extends React.Component<FieldArrayConfig, {}> {
       handleSwap: this.handleSwap,
       handleMove: this.handleMove,
       handleInsert: this.handleInsert,
+      handleReplace: this.handleReplace,
       handleUnshift: this.handleUnshift,
       handleRemove: this.handleRemove,
     };
diff --git a/src/Formik.tsx b/src/Formik.tsx
index 15c10df..fe2244c 100644
--- a/src/Formik.tsx
+++ b/src/Formik.tsx
@@ -9,7 +9,6 @@ import {
   isEmptyChildren,
   setIn,
   setNestedObjectValues,
-  isReactNative,
 } from './utils';
 
 /**
@@ -266,7 +265,7 @@ export class Formik<ExtraProps = {}, Values = object> extends React.Component<
   initialValues: Values;
 
   hcCache: {
-    [key: string]: (e: React.ChangeEvent<any>) => void;
+    [key: string]: (e: string | React.ChangeEvent<any>) => void;
   } = {};
   hbCache: {
     [key: string]: (e: any) => void;
@@ -423,17 +422,39 @@ export class Formik<ExtraProps = {}, Values = object> extends React.Component<
   };
 
   handleChange = (
-    eventOrString: any
-  ): void | ((e: React.ChangeEvent<any>) => void) => {
+    eventOrPath: string | React.ChangeEvent<any>
+  ): void | ((eventOrTextValue: string | React.ChangeEvent<any>) => void) => {
     // @todo someone make this less disgusting.
-    const executeChange = (e: React.ChangeEvent<any>, path?: string) => {
-      e.persist();
-      let field = path;
-      let val = e;
+    //
+    // executeChange is the core of handleChange, we'll use it cache change
+    // handlers like Preact's linkState.
+    const executeChange = (
+      eventOrTextValue: string | React.ChangeEvent<any>,
+      maybePath?: string
+    ) => {
+      // By default, assume that the first argument is a string. This allows us to use
+      // handleChange with React Native and React Native Web's onChangeText prop which
+      // provides just the value of the input.
+      let field = maybePath;
+      let val = eventOrTextValue;
       let parsed;
-      if (!isReactNative) {
-        const { type, name, id, value, checked, outerHTML } = e.target;
-        field = path ? path : name ? name : id;
+      // If the first argument is not a string though, it has to be a synthetic React Event (or a fake one),
+      // so we handle like we would a normal HTML change event.
+      if (!isString(eventOrTextValue)) {
+        // If we can, persist the event
+        // @see https://reactjs.org/docs/events.html#event-pooling
+        if ((eventOrTextValue as React.ChangeEvent<any>).persist) {
+          (eventOrTextValue as React.ChangeEvent<any>).persist();
+        }
+        const {
+          type,
+          name,
+          id,
+          value,
+          checked,
+          outerHTML,
+        } = (eventOrTextValue as React.ChangeEvent<any>).target;
+        field = maybePath ? maybePath : name ? name : id;
         if (!field && process.env.NODE_ENV !== 'production') {
           warnAboutMissingIdentifier({
             htmlContent: outerHTML,
@@ -463,14 +484,21 @@ export class Formik<ExtraProps = {}, Values = object> extends React.Component<
       }
     };
 
-    if (isString(eventOrString)) {
-      // cache these handlers by key like Preact's linkState does for perf boost
-      return typeof this.hcCache[eventOrString] === 'function'
-        ? this.hcCache[eventOrString]
-        : (this.hcCache[eventOrString] = (event: React.ChangeEvent<any>) =>
-            executeChange(event, eventOrString));
+    // Actually execute logic above....
+    // cache these handlers by key like Preact's linkState does for perf boost
+    if (isString(eventOrPath as string)) {
+      return isFunction(this.hcCache[eventOrPath as string])
+        ? this.hcCache[eventOrPath as string] // return the cached handled
+        : (this.hcCache[eventOrPath as string] = (
+            // make a new one
+            event: React.ChangeEvent<any> | string
+          ) =>
+            executeChange(
+              event /* string or event, does not matter */,
+              eventOrPath as string /* this is path to the field now */
+            ));
     } else {
-      executeChange(eventOrString);
+      executeChange(eventOrPath);
     }
   };
 
@@ -548,7 +576,9 @@ export class Formik<ExtraProps = {}, Values = object> extends React.Component<
 
   handleBlur = (eventOrString: any): void | ((e: any) => void) => {
     const executeBlur = (e: any, path?: string) => {
-      e.persist();
+      if (e.persist) {
+        e.persist();
+      }
       const { name, id, outerHTML } = e.target;
       const field = path ? path : name ? name : id;
 
@@ -571,7 +601,7 @@ export class Formik<ExtraProps = {}, Values = object> extends React.Component<
 
     if (isString(eventOrString)) {
       // cache these handlers by key like Preact's linkState does for perf boost
-      return typeof this.hbCache[eventOrString] === 'function'
+      return isFunction(this.hbCache[eventOrString])
         ? this.hbCache[eventOrString]
         : (this.hbCache[eventOrString] = (event: any) =>
             executeBlur(event, eventOrString));


Test patch:
diff --git a/test/FieldArray.test.tsx b/test/FieldArray.test.tsx
index 3e53cea..67f5380 100644
--- a/test/FieldArray.test.tsx
+++ b/test/FieldArray.test.tsx
@@ -182,6 +182,34 @@ describe('<FieldArray />', () => {
     });
   });
 
+  describe('props.replace()', () => {
+    it('should replace a value at given index of field array', () => {
+      let formikBag: any;
+      let arrayHelpers: any;
+      ReactDOM.render(
+        <TestForm
+          render={(props: any) => {
+            formikBag = props;
+            return (
+              <FieldArray
+                name="friends"
+                render={arrayProps => {
+                  arrayHelpers = arrayProps;
+                  return null;
+                }}
+              />
+            );
+          }}
+        />,
+        node
+      );
+
+      arrayHelpers.replace(1, 'brian');
+      const expected = ['jared', 'brian', 'brent'];
+      expect(formikBag.values.friends).toEqual(expected);
+    });
+  });
+
   describe('props.unshift()', () => {
     it('should add a value to start of field array and return its length', () => {
       let formikBag: any;