react-final-form icon indicating copy to clipboard operation
react-final-form copied to clipboard

[feature]not removing field name from state, when its value become empty

Open alex-shatalov opened this issue 7 years ago • 25 comments

In all examples if write something in input, than clear it, the field name will be dropped from state. But is it possibly maybe with some additional prop like notRemoveWhenEmpty in Field, the value will be null, or empty or anything else.

alex-shatalov avatar Jan 23 '18 07:01 alex-shatalov

This would be nice to have. Some times a form needs an empty string to be stored in the DB, and undefined != "".

@alex-shatalov UPDATE: Actually looking at the source code, you can override the default with the parse function doing parse={value=>value}, that way and empty string can be used

juanpasolano avatar Jun 05 '18 14:06 juanpasolano

The fact that parse, by default, isn't an identity function (e.g., value => (value)) is not what I would expect. @erikras, thoughts?

I can speak to my use case (maybe it applies to others-- I am migrating from redux-form, but I don't want to sound like a special snowflake, either). I have an existing record I am editing via HTTP PATCH. The field in question cannot be null -- this is enforced at the database level. If a user clears the field, it will be null, and thus its key will be set to undefined in the onSubmit function. (Analogously, I believe it would have been an empty string in redux-form).

By the time the object makes it to my ORM and ultimately becomes a SQL statement, the deleted property isn't included in the UPDATE sql statement. So the HTTP PATCH request successfully executes. The user then assumes the request is valid, but if they refresh the page, they would see the value they attempted to delete is still there. The fact that the user had entered an empty string in the text input was valuable -- I used that to throw an error.

Again, this might be my personal super snowflake use case, but I'm curious if it's something others have encountered. Maybe this is more of an issue with my ORM neglecting to attempt passing the undefined value.

petermikitsh avatar Sep 27 '18 01:09 petermikitsh

In the meantime, I've wrote a simple adapter for the Field component, and my application code imports this adapter component in place of the official one:

import {Field} from 'react-final-form';
import React from 'react';

const identity = value => (value);

/* This wraps react-final-form's <Field/> component.
 * The identity function ensures form values never get set to null,
 * but rather, empty strings.
 *
 * See https://github.com/final-form/react-final-form/issues/130
 */
export default props => <Field parse={identity} {...props}/>;

petermikitsh avatar Sep 28 '18 15:09 petermikitsh

@petermikitsh Thanks for sharing! I'm gonna go with the same fix 👌.

oliviertassinari avatar Oct 09 '18 11:10 oliviertassinari

The reasoning for this decision is somewhat explained here.

Your notRemoveWhenEmpty proposal sounds like a decent compromise. 👍

erikras avatar Mar 05 '19 14:03 erikras

thank you for the parse tip.

I had the same issue but I wanted a fix at the form level. So my solution was to wrap my submit handler in the form like so:

handleSubmit = (values) => {
  const {initialValues, onSubmit} = this.props;
  // look for key-value pairs present in initialValues but not in values
  const data = Object.keys(initialValues).reduce((acc, key) => {
    acc[key] = typeof values[key] === "undefined" ? '' : values[key];
    return acc;
  },{});
  // add key-value pairs that might be in values but not in initialValues:
  Object.assign(data, values);
  // continue submit
  onSubmit(data);
};

This way, if a field is initially set, then submitted empty, it will be part of the payload. Hope this helps.

Note: this doesn't work for nested properties

VincentCharpentier avatar May 17 '19 13:05 VincentCharpentier

update to @VincentCharpentier for nested data, quick recursive. thank you @VincentCharpentier and @erikras


import { merge, isObject } from 'lodash';

const checkForInitialValues = (initialValues, newValues) => {
  // library issue via https://github.com/final-form/react-final-form/issues/130#issuecomment-493447888
  const emptiedData = Object.keys(initialValues).reduce((acc, key) => {
    if (isObject(newValues[key])) {
      acc[key] = checkForInitialValues(initialValues[key], newValues[key]);
    } else {
      acc[key] = typeof newValues[key] === 'undefined' ? '' : newValues[key];
    }
    return acc;
  }, {});

  // need to deep merge to get new child properties
  return merge(emptiedData, newValues);
};

update: included null check via @Soundvessel -- thank you. update (9-9-19): replaced null check and typeof object check with lodash

JackHowa avatar Jul 22 '19 16:07 JackHowa

Be aware if you use the above but usenull instead of undefined for your emptied fields, needed for our particular API, you run into the odd issue where null makes typeof newValues[key] === 'object' true so you should add a check make it something like typeof newValues[key] === 'object' && newValues[key] !== null.

Soundvessel avatar Aug 13 '19 23:08 Soundvessel

The solution provided by @JackHowa is incoimplete - it does not work in some corner cases (e.g. dates, arrays).

This is where we've got so far with marmelab/react-admin:

const sanitizeEmptyValues = (initialValues: object, values: object) => {
    // For every field initially provided, we check whether it value has been removed
    // and set it explicitly to an empty string
    if (!initialValues) return values;
    const initialValuesWithEmptyFields = Object.keys(initialValues).reduce(
        (acc, key) => {
            if (values[key] instanceof Date || Array.isArray(values[key])) {
                acc[key] = values[key];
            } else if (
                typeof values[key] === 'object' &&
                values[key] !== null
            ) {
                acc[key] = sanitizeEmptyValues(initialValues[key], values[key]);
            } else {
                acc[key] =
                    typeof values[key] === 'undefined' ? null : values[key];
            }
            return acc;
        },
        {}
    );

    // Finally, we merge back the values to not miss any which wasn't initially provided
    return merge(initialValuesWithEmptyFields, values);
};

Let me say that finding this kind of corner case should not be left to us. @erikras I strongly believe this should be in the react-final-form core.

fzaninotto avatar Nov 29 '19 07:11 fzaninotto

by now I've face this issue a lot and I'm still not sure what would be the best generic solution. Sometimes I just don't want to submit empty keys, sometimes I need it some it gets erased in DB.

Indeed I ran into the same issue with date and array as you did @fzaninotto

anyway this is what I've come up with this my last message, if this can be useful to anyone. Note that I'm using null as default empty value

function enforceSubmitClearedFormKeys(initialValues, values) {
  const _initialValues = typeof initialValues === 'object' ? initialValues : {};
  const _values = typeof values === 'object' ? values : {};

  const keys = Array.from(
    new Set(Object.keys(_initialValues || {}).concat(Object.keys(_values || {}))),
  );

  return keys.reduce((result, key) => {
    let value;
    if (_values !== null) {
      value = _values[key];
    }
    let initValue;
    if (_initialValues !== null) {
      initValue = _initialValues[key];
    }
    const initDefined = typeof initValue !== 'undefined' && initValue !== null;
    const valueDefined = typeof value !== 'undefined' && value !== null;
    if (initDefined && valueDefined) {
      // both defined, we may need to apply same logic on this property
      if (typeof value === 'object' && !(value instanceof Date) && !(value instanceof Array)) {
        value = enforceSubmitClearedFormKeys(initValue, value);
      }
    } else if (initDefined && !valueDefined) {
      // property was erased
      value = null;
    }
    result[key] = value;
    return result;
  }, {});
}

I do agree there should be some configuration available in final-form to handle that but at least we have a workaround :)

VincentCharpentier avatar Nov 29 '19 10:11 VincentCharpentier

Your notRemoveWhenEmpty proposal sounds like a decent compromise. 👍

What about something like submitEmptyValue that not only keeps the property in values but allows you to set what that value is when empty such as null, undefined, whatever...?

We could allow the values state to be untouched and instead have this as some kind of parse done inside the handleSubmit?

Edit: Or not, I guess that means we can't pull the same form API in our onSubmit...

This is difficult because our API will ignore undefined properties. We have to send null with optional fields to clear them.

Soundvessel avatar Dec 02 '19 22:12 Soundvessel

Thanks for all the answers on the thread! I've added a variant to the solution posted by @fzaninotto as I need to handle an array of fields:

    (acc, key) => {
      const value = values[key];

      if (
        value instanceof Date ||
        (Array.isArray(value) && typeof value[0] !== 'object')
      ) {
        acc[key] = value;
      } else if (Array.isArray(value) && typeof value[0] === 'object') {
        acc[key] = value.map((val, itr) => {
          return includeRemovedValues(initialValues[key][itr], val);
        });
      } else if (typeof value === 'object' && value !== null) {
        acc[key] = includeRemovedValues(initialValues[key], value);
      } else {
        acc[key] = typeof value === 'undefined' ? null : value;
      }

      return acc;
    },
    {}
  );

mischa-s avatar Apr 27 '20 23:04 mischa-s

In the meantime, I've wrote a simple adapter for the Field component, and my application code imports this adapter component in place of the official one:

import {Field} from 'react-final-form';
import React from 'react';

const identity = value => (value);

/* This wraps react-final-form's <Field/> component.
 * The identity function ensures form values never get set to null,
 * but rather, empty strings.
 *
 * See https://github.com/final-form/react-final-form/issues/130
 */
export default props => <Field parse={identity} {...props}/>;

This solution worked to me, but a notRemoveWhenEmpty option will be great.

douglasjunior avatar May 22 '20 16:05 douglasjunior

Releasing support for this feature would be much appreciated. Thank you for a great library anyway 👍

makker avatar Oct 26 '20 08:10 makker

Would be nice to see this case supported. Thanks for the good work so far!

HeikkiMoilanen avatar Oct 26 '20 08:10 HeikkiMoilanen

Are there plans to support this feature?

wenscl avatar Jan 29 '21 14:01 wenscl

In the meantime, I've wrote a simple adapter for the Field component, and my application code imports this adapter component in place of the official one:

import {Field} from 'react-final-form';
import React from 'react';

const identity = value => (value);

/* This wraps react-final-form's <Field/> component.
 * The identity function ensures form values never get set to null,
 * but rather, empty strings.
 *
 * See https://github.com/final-form/react-final-form/issues/130
 */
export default props => <Field parse={identity} {...props}/>;

Thanks for this!

Note, this fix alone will not submit empty string values for fields which were never set. So if you have an optional text input foo and the user never touched it, you will not get foo: "" in the values submitted. You will get foo: "" if the user initially typed in some characters and then deleted them leaving the input empty.

In my case, I need to send to empty string values for optional fields if they are never touched. My use case is a dynamic form populated by a backend API response. Sometimes the form has previously submitted values which need to be rehydrated so that the user can edit the form.

I used your solution and supplemented it by passing initialValues which I reduced like so:

// In order to submit empty string values for untouched (optional) fields we have to set empty string initial values
// However, we may have a previously submitted value
// Therefore, we reduce the (previously submitted) responses adding in empty string values for any unanswered questions
const initialValues = customFields.reduce(
  (acc, curr) => ({
    ...acc,
    [curr.name]: responses[curr.name] ? responses[curr.name] : '',
  }),
  {},
)

In the above code, customFields are the data we use to create the dynamic form fields and responses are the previously submitted values (or simply {} if this is the first time the user is filling it out).

Hopefully this will help someone in the future.

Thanks again.

single-stop-neil avatar May 20 '21 23:05 single-stop-neil

Ran into this issue, ended up creating a function like this, using lodash isNil to determine if the value is missing:

const stringIdentity = value => _.isNil(value) ? '' : value;

And then including this inside a wrapper to the onSubmit method, to mutate the values:

Object.keys(initialValues).forEach((key)=>{
    values[key] = stringIdentity(values[key]);
})

aintHuman avatar Jul 18 '21 09:07 aintHuman

As can be seen in this simple example, react-final-form does not just set empty values to undefined, but it also recursively sets their containing objects to undefined in case of a nested structure.

I am using the form to edit an object that needs to have a fixed structure, so react-final-form removing certain properties from the object will break my form. But undefined needs to be a supported value for some of its properties, so I cannot use the parse workaround. The only workaround that I can think of for now is to add an addition bogus property to my object, so that if the other property becomes undefined, the object will not become empty (causing final-form to remove the whole object). Can someone think of a better workaround?

cdauth avatar Jul 23 '21 10:07 cdauth

As can be seen in this simple example, react-final-form does not just set empty values to undefined, but it also recursively sets their containing objects to undefined in case of a nested structure.

I am using the form to edit an object that needs to have a fixed structure, so react-final-form removing certain properties from the object will break my form. But undefined needs to be a supported value for some of its properties, so I cannot use the parse workaround. The only workaround that I can think of for now is to add an addition bogus property to my object, so that if the other property becomes undefined, the object will not become empty (causing final-form to remove the whole object). Can someone think of a better workaround?

I think it works with the parse workaround

josechavezm avatar Jul 23 '21 16:07 josechavezm

It works because it's an input field, whose value never becomes undefined. In my real-world scenario, the data type of the field is more complex and needs to be set to undefined in certain scenarios.

cdauth avatar Jul 23 '21 17:07 cdauth

this can be closed right @alex-shatalov ? I think the answer by https://github.com/final-form/react-final-form/issues/130#issuecomment-620291085 is really effective for this

JackHowa avatar Sep 03 '21 20:09 JackHowa

@erikras Thank you for posting the explanation for this. I wonder whether this should also go in the migration page for Formik? We migrated but didn't realise this issue until a customer complained. It's highly possibly that I just missed this bit of the docs so my apologies if that's the case.

JamieDixon avatar May 23 '22 15:05 JamieDixon

In the meantime, I've wrote a simple adapter for the Field component, and my application code imports this adapter component in place of the official one:

import {Field} from 'react-final-form';
import React from 'react';

const identity = value => (value);

/* This wraps react-final-form's <Field/> component.
 * The identity function ensures form values never get set to null,
 * but rather, empty strings.
 *
 * See https://github.com/final-form/react-final-form/issues/130
 */
export default props => <Field parse={identity} {...props}/>;

Amazing! it worked like charm.

oribenez avatar Oct 30 '22 10:10 oribenez

I understand that this issue with Final Form (FF) setting input values to undefined is not new, but I'd like to propose an alternative approach. While I comprehend the rationale explained in the linked document, I believe there's a more intuitive solution.

Instead of FF automatically converting empty strings ('') to undefined, it should retain these empty strings; and if the developer needed meta.pristine to remain true after a user types and then clears an input, they could initialize the form field with '' which would provide the desired effect, IINM. This change would enhance the API's intuitiveness and flexibility imo.

FWIW, i love this library and im happy to use it either way. Thanks @erikras

xOIBrandon avatar Dec 18 '23 17:12 xOIBrandon