jaredpalmer / formik

Build forms in React, without the tears 😭
https://formik.org
Apache License 2.0
33.97k stars 2.79k forks source link

Normalise null and array values - just like numbers #568

Open IgorJoergensen opened 6 years ago

IgorJoergensen commented 6 years ago

Bug, Feature, or Question?

Formik should be able to on form level to handle null and array values given and normalise values handing them off to fields - and then revert them back again when handing off to submit

For a form everything is a string - and thus Formik should be able to normalise this for fields to be able to work as intended without having to write encode/decode code for values on field level.

I do realise the above is not quite true, as you will have to parse the strings you get on field level and pass on to your sub component that probably involves a grid of some sort (for array values)

You already cater for this with numbers.

Current Behavior

Null values remain and has to be altered in each field situation since null as input field value will yield error Array values remain arrays and have to be altered in each field situation since input fields does not accept arrays

Desired Behavior

Null values are changed to empty strings on the inside of the render method Array values are stringified on the inside of the render method Null values are restored for submit value in case of empty string Array values are parsed for submit value

Suggested Solutions

I have to be able to handle null values in some forms I am making currently, and also some detailed array like fields - So for me the easiest and cleanest way was to create a HOC like super component for Formik - and I named it Omnik.

It simply just does the encoding/decoding at the right time and otherwise just pipes props directly down to Formik - which it then returns.

The right solution would probably be to bake it directly into Formik (if you agree with the problem/solution)

import React from 'react';
import { Formik, FormikConfig, FormikValues, FormikActions } from './Formik';

const isArray = (val: any) => {
  return Array.isArray(val);
};

const isObject = (val: any) => {
  return typeof val === 'object' && !Array.isArray(val) && val !== null;
};

export class Omnik<Values = object> extends React.Component<
  FormikConfig<Values>,
  {}
> {
  private initialValues: FormikValues = {};

  private formikProps: FormikConfig<Values>;

  constructor(props: FormikConfig<Values>) {
    super(props);

    this.initialValues = props.initialValues;
    this.formikProps = this.initializeFormikProps(props);
  }

  public render() {
    return <Formik {...this.formikProps} />;
  }

  public handleSubmitProxy = (
    values: Values,
    formikActions: FormikActions<Values>
  ) => {
    const { onSubmit } = this.props;
    const newValues = this.decodeValues(values);

    onSubmit(newValues, formikActions);
  };

  private encodeNullValues = (values: Values) => {
    const { encodeNullValues } = this;
    const newValues: any = Object.assign({}, values);

    // Iterate each property and check for a null value - then reset
    // to empty string if null is found - iterate recursivly for objects
    Object.keys(newValues).forEach(key => {
      const value: any = newValues[key];
      if (value === null) {
        newValues[key] = '';
      } else if (isObject(value)) {
        newValues[key] = encodeNullValues(value);
      }
    });

    return newValues;
  };

  private decodeNullValues = (
    values: Values,
    matchValues: FormikValues = this.initialValues
  ) => {
    const { decodeNullValues } = this;
    const newValues: any = Object.assign({}, values);

    Object.keys(newValues).forEach(key => {
      const value: any = newValues[key];
      const matchValue = matchValues[key];

      // If we get an empty string - then check in matchValues for a null value
      // to place on key instead of the empty string
      if (typeof value === 'string' && !value && matchValue === null) {
        newValues[key] = null;
      } else {
        if (isObject(value)) {
          newValues[key] = decodeNullValues(value, matchValue);
        }
      }
    });

    return newValues;
  };

  private encodeArrayValues = (values: Values) => {
    const { encodeArrayValues } = this;
    let newValues: any = Object.assign({}, values);

    // Iterate the given values and look for arrays to stringify
    Object.keys(newValues).forEach(key => {
      const value: any = newValues[key];

      if (isArray(value)) {
        newValues[key] = JSON.stringify(value);
      } else if (isObject(value)) {
        newValues[key] = encodeArrayValues(value);
      }
    });

    return newValues;
  };

  private decodeArrayValues = (
    values: Values,
    matchValues: FormikValues = this.initialValues
  ) => {
    const { decodeArrayValues } = this;
    let newValues: any = Object.assign({}, values);

    Object.keys(newValues).forEach(key => {
      const value: any = newValues[key];
      const matchValue = matchValues[key];

      if (isArray(matchValue)) {
        newValues[key] = JSON.parse(value);
      } else if (isObject(value)  && value !== null) {
        newValues[key] = decodeArrayValues(value, matchValues[key]);
      }
    });

    return newValues;
  };

  private encodeValues = (values: Values) => {
    const { encodeNullValues, encodeArrayValues } = this;
    let newValues = Object.assign({}, values);

    // First encode null values
    newValues = encodeNullValues(newValues);

    // Then stringify arrays
    newValues = encodeArrayValues(newValues);

    return newValues;
  };

  private decodeValues = (values: Values) => {
    const { decodeNullValues, decodeArrayValues } = this;
    let newValues = Object.assign({}, values);

    newValues = decodeNullValues(newValues);
    newValues = decodeArrayValues(newValues);

    return newValues;
  };

  private initializeFormikProps = (props: FormikConfig<Values>) => {
    const { encodeValues, handleSubmitProxy } = this;
    const formikProps = { ...props };
    const { initialValues } = props;

    formikProps.initialValues = encodeValues(initialValues);

    // Set a new handleSubmit to proxy
    formikProps.onSubmit = handleSubmitProxy;

    return formikProps;
  };
}

EDIT: Updated the isObject function as null is actually an object

jaredpalmer commented 6 years ago

Interesting. This seems magical, but useful

IgorJoergensen commented 6 years ago

Edited the isObject function since null would slip through....

IgorJoergensen commented 6 years ago

Also probably there will need to be something done to validation part, so values are decoded when handing off to validation method also...

You probably expect to validate against null instead of empty string when running into "no value" on such a field where you potentially handed off null...

stale[bot] commented 6 years ago

Hola! So here's the deal, between open source and my day job and life and what not, I have a lot to manage, so I use a GitHub bot to automate a few things here and there. This particular GitHub bot is going to mark this as stale because it has not had recent activity for a while. It will be closed if no further activity occurs in a few days. Do not take this personally--seriously--this is a completely automated action. If this is a mistake, just make a comment, DM me, send a carrier pidgeon, or a smoke signal.

stale[bot] commented 6 years ago

ProBot automatically closed this due to inactivity. Holler if this is a mistake, and we'll re-open it.

danieltodonnell commented 5 years ago

Holler. I would like to see Formik handle null values

danivivriti commented 5 years ago

Using Formik to create a form intensive app, this will be lifesaver. Now I'm manually checking and replacing null values as the API i'm consuming is always return null if it is empty.

zabaikin commented 5 years ago

Holler. I would like to see Formik handle null values

Me too ;)

johnrom commented 5 years ago

I'm reopening this because it was auto-closed with open questions.

prajavk commented 5 years ago

@jaredpalmer In realtime, backend returns null values to load any form. UI doesn't know what values are returned to Formik and replace all record with {""} empty strings. When will Formik handle null values?

Gusterson commented 5 years ago

In the meantime, what is a recommended work-around?

I have a database returning many nulls I want to keep (and re-insert into the DB) but I keep getting "Warning: value prop on input should not be null..."

raiskila commented 5 years ago

I think the recommended workaround is to maintain some code that transforms the data you get from the server to a format expected by Formik before you pass it to Formik, and the other way around when submitting.

johnrom commented 5 years ago

You can either transform these values at initialization/submit or when printing/changing the value.

Note: selectEmptyStringWhenNull is what happens by default which is why this issue exists, but you can make any transformation you want there.

// transform at initialization/submit
const Form = (
    <Formik 
        initialValues={selectNotNullValues} 
        onSubmit={compose(selectNullValues, doSubmit} 
    >
        {formik => (
        <Form>
            // transform at field level
            <Field 
                 name="stringValue"
                 onChange={compose(selectNullOrString, formik.handleChange("stringValue"}} 
                 value={selectEmptyStringWhenNull(formik.values.stringValue)} />
        </Form>
    </Formik>
);
ksweetie commented 5 years ago

I feel like this is something Formik should handle by default. I can't think of any case where it's ok for Formik to set both a field's onchange handler and initial value to null.

In the meantime, here's what I'm doing to make this less painless. This sets nulls to '', recursively.

const user = { name: 'Stan', title: null, address: { street: null } }
const initializeValues = (obj) => JSON.parse(JSON.stringify(obj, (k, v) => (v === null ? '' : v)))

<Formik initialValues={initializeValues(user)}></Formik>
zabaikin commented 4 years ago

Any updates on this issue? :)

rishabhr16 commented 4 years ago

@ksweetie If I have a form that has a lot of nested fields, let's say 4,5 levels deep, would this still be an efficient way to go about it?

johnrom commented 4 years ago

I would prefer Formik not modify the values that I pass to it unless I tell it to. Calling a userland function to sanitize your initialValues seems like a good idea.

What if the value of the field should instead default to 0 when null? I think it'd be better to pass a property like onParse={value => value !== null ? value : ''}

Related topic: #1525

ksweetie commented 4 years ago

@rishabhr16 It should be fine. I wouldn't worry about pre-optimizing something until you notice a problem.

@johnrom Yeah, I like that idea.

lucksp commented 4 years ago

is my issue here related? https://stackoverflow.com/questions/63062806/formik-validateonblur-returns-error-with-null-fields-when-i-add-new-field-dele

miquelvir commented 3 years ago

For those using useFormik(), I found myself in the same problem. I don't know much about js, so please add any suggestions if needed, but for now I have a working copy which mirrors the API of useFormik but takes care of replacing null values by empty strings on initialization, and going the other way around on onSubmit.

import {useFormik} from "formik";

export function useNormik(props) {
    if ("initialValues" in props) {
        let nullSafeInitialValues = {};
        for (const [key, value] of Object.entries(props.initialValues)) {  // todo how to make clean
          if (value === null) {
              nullSafeInitialValues[key] = '';
          } else {
              nullSafeInitialValues[key] = value;
          }
        }
        props.initialValues = nullSafeInitialValues;
    }

    if ("onSubmit" in props){
        const oldOnSubmit = props.onSubmit;
        const nullSafeOnSubmit = (values, actions) => {
            let normalizedValues = {};
            for (const [key, value] of Object.entries(values)) {  // todo how to make clean
              if (value === '') normalizedValues[key] = null;
            }
            oldOnSubmit(normalizedValues, actions);
        }
        props.onSubmit = nullSafeOnSubmit;
    }

    return useFormik(props);
}
rgallison commented 1 year ago

I actually have a use case for maintaining nulls. It's easy enough to default to a string when passing to the input, but my endpoint expects null. In fact, I am struggling because Formik is removing my null values