react-final-form
react-final-form copied to clipboard
[feature]not removing field name from state, when its value become empty
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.
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
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.
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 Thanks for sharing! I'm gonna go with the same fix 👌.
The reasoning for this decision is somewhat explained here.
Your notRemoveWhenEmpty
proposal sounds like a decent compromise. 👍
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
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
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
.
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.
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 :)
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.
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;
},
{}
);
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.
Releasing support for this feature would be much appreciated. Thank you for a great library anyway 👍
Would be nice to see this case supported. Thanks for the good work so far!
Are there plans to support this feature?
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.
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]);
})
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?
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 theparse
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 becomesundefined
, the object will not become empty (causing final-form to remove the whole object). Can someone think of a better workaround?
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.
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
@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.
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.
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