I want to implement an edit dialog for some metadata that I get from the backend. For this, I'm using Formik. When the user changes one metadata field then an icon is shown, which indicates that the field was changed. On submit, only the updated values should be sent to the backend. I read in other posts that you should compare the current field values with the initial values provided to the Formik form. This works perfectly fine for single values, like a changed title. However, the form I need to implement has also multi-value fields like creators. I implemented a custom field for that where the user can choose/provide multiple values for one field. The values of this field are saved as an array in the Formik form. The problem is that Formik also changes the initialValue of this field to the currently saved array and therefore I'm no longer able to check if the field is updated or not. Furthermore, I save the metadata fields provided by the backend in a state value because the response contains some further information needed for further implementation. This state value also contains the current value (before update) that a metadata field has and is used as initial values for the Formik form. Strangely, the multi-field component not only overwrites the initialValue of the field of the Formik form but also the value of the field in the state which is only read and never directly updated.
My dialog for editing metadata looks as follows:
const EditMetadataEventsModal = ({ close, selectedRows, updateBulkMetadata }) => {
const { t } = useTranslation();
const [selectedEvents, setSelectedEvents] = useState(selectedRows);
const [metadataFields, setMetadataFields] = useState({});
const [fetchedValues, setFetchedValues] = useState(null);
useEffect(() => {
async function fetchData() {
let eventIds = [];
selectedEvents.forEach((event) => eventIds.push(event.id));
// metadata for chosen events is fetched from backend and saved in state
const responseMetadataFields = await fetchEditMetadata(eventIds);
let initialValues = getInitialValues(responseMetadataFields);
setFetchedValues(initialValues);
setMetadataFields(responseMetadataFields);
}
fetchData();
}, []);
const handleSubmit = (values) => {
const response = updateBulkMetadata(metadataFields, values);
close();
};
return (
<>
<div className="modal-animation modal-overlay" />
<section className="modal wizard modal-animation">
<header>
<a className="fa fa-times close-modal" onClick={() => close()} />
<h2>{t('BULK_ACTIONS.EDIT_EVENTS_METADATA.CAPTION')}</h2>
</header>
<MuiPickersUtilsProvider utils={DateFnsUtils} locale={currentLanguage.dateLocale}>
<Formik initialValues={fetchedValues} onSubmit={(values) => handleSubmit(values)}>
{(formik) => (
<>
<div className="modal-content">
<div className="modal-body">
<div className="full-col">
<div className="obj header-description">
<span>{t('EDIT.DESCRIPTION')}</span>
</div>
<div className="obj tbl-details">
<header>
<span>{t('EDIT.TABLE.CAPTION')}</span>
</header>
<div className="obj-container">
<table className="main-tbl">
<tbody>
{metadataFields.mergedMetadata.map(
(metadata, key) =>
!metadata.readOnly && (
<tr key={key} className={cn({ info: metadata.differentValues })}>
<td>
<span>{t(metadata.label)}</span>
{metadata.required && <i className="required">*</i>}
</td>
<td className="editable ng-isolated-scope">
{/* Render single value or multi value input */}
{console.log('field value')}
{console.log(fetchedValues[metadata.id])}
{metadata.type === 'mixed_text' &&
!!metadata.collection &&
metadata.collection.length !== 0 ? (
<Field name={metadata.id} fieldInfo={metadata} component={RenderMultiField} />
) : (
<Field
name={metadata.id}
metadataField={metadata}
showCheck
component={RenderField}
/>
)}
</td>
</tr>
),
)}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{/* Buttons for cancel and submit */}
<footer>
<button
type="submit"
onClick={() => formik.handleSubmit()}
disabled={!(formik.dirty && formik.isValid)}
className={cn('submit', {
active: formik.dirty && formik.isValid,
inactive: !(formik.dirty && formik.isValid),
})}
>
{t('UPDATE')}
</button>
<button onClick={() => close()} className="cancel">
{t('CLOSE')}
</button>
</footer>
<div className="btm-spacer" />
</>
)}
</Formik>
</MuiPickersUtilsProvider>
</section>
</>
);
};
const getInitialValues = (metadataFields) => {
// Transform metadata fields provided by backend
let initialValues = {};
metadataFields.mergedMetadata.forEach((field) => {
initialValues[field.id] = field.value;
});
return initialValues;
};
Here is RenderMultiField:
const childRef = React.createRef();
const RenderMultiField = ({ fieldInfo, field, form }) => {
// Indicator if currently edit mode is activated
const [editMode, setEditMode] = useState(false);
// Temporary storage for value user currently types in
const [inputValue, setInputValue] = useState('');
useEffect(() => {
// Handle click outside the field and leave edit mode
const handleClickOutside = (e) => {
if (childRef.current && !childRef.current.contains(e.target)) {
setEditMode(false);
}
};
// Focus current field
if (childRef && childRef.current && editMode === true) {
childRef.current.focus();
}
// Adding event listener for detecting click outside
window.addEventListener('mousedown', handleClickOutside);
return () => {
window.removeEventListener('mousedown', handleClickOutside);
};
}, []);
// Handle change of value user currently types in
const handleChange = (e) => {
const itemValue = e.target.value;
setInputValue(itemValue);
};
const handleKeyDown = (event) => {
// Check if pressed key is Enter
if (event.keyCode === 13 && inputValue !== '') {
event.preventDefault();
// add input to formik field value if not already added
if (!field.value.find((e) => e === inputValue)) {
field.value[field.value.length] = inputValue;
form.setFieldValue(field.name, field.value);
}
// reset inputValue
setInputValue('');
}
};
// Remove item/value from inserted field values
const removeItem = (key) => {
field.value.splice(key, 1);
form.setFieldValue(field.name, field.value);
};
return (
// Render editable field for multiple values depending on type of metadata field
editMode ? (
<>
{fieldInfo.type === 'mixed_text' && !!fieldInfo.collection && (
<EditMultiSelect
collection={fieldInfo.collection}
field={field}
setEditMode={setEditMode}
inputValue={inputValue}
removeItem={removeItem}
handleChange={handleChange}
handleKeyDown={handleKeyDown}
/>
)}
</>
) : (
<ShowValue setEditMode={setEditMode} field={field} form={form} />
)
);
};
// Renders multi select
const EditMultiSelect = ({ collection, setEditMode, handleKeyDown, handleChange, inputValue, removeItem, field }) => {
const { t } = useTranslation();
return (
<>
<div ref={childRef}>
<div onBlur={() => setEditMode(false)}>
<input
type="text"
name={field.name}
value={inputValue}
onKeyDown={(e) => handleKeyDown(e)}
onChange={(e) => handleChange(e)}
placeholder={t('EDITABLE.MULTI.PLACEHOLDER')}
list="data-list"
/>
{/* Display possible options for values as dropdown */}
<datalist id="data-list">
{collection.map((item, key) => (
<option key={key}>{item.value}</option>
))}
</datalist>
</div>
{/* Render blue label for all values already in field array */}
{field.value instanceof Array &&
field.value.length !== 0 &&
field.value.map((item, key) => (
<span className="ng-multi-value" key={key}>
{item}
<a onClick={() => removeItem(key)}>
<i className="fa fa-times" />
</a>
</span>
))}
</div>
</>
);
};
// Shows the values of the array in non-edit mode
const ShowValue = ({ setEditMode, form: { initialValues }, field }) => {
return (
<div onClick={() => setEditMode(true)}>
{field.value instanceof Array && field.value.length !== 0 ? (
<ul>
{field.value.map((item, key) => (
<li key={key}>
<span>{item}</span>
</li>
))}
</ul>
) : (
<span className="editable preserve-newlines">{''}</span>
)}
<i className="edit fa fa-pencil-square" />
<i className={cn('saved fa fa-check', { active: initialValues[field.name] !== field.value })} />
</div>
);
};
export default RenderMultiField;
This is initialValues before a change and after:
InitialValues before change
InitialValues after change
This is the state of MetadataFields and FetchedValues before and after change:
State before change
State after change