Formik - 状态和初始值被覆盖

时间:2021-04-29 07:41:03

标签: javascript reactjs formik

我想为我从后端获得的一些元数据实现一个编辑对话框。为此,我正在使用 Formik。当用户更改一个元数据字段时,会显示一个图标,表示该字段已更改。提交时,只应将更新的值发送到后端。我在其他帖子中读到您应该将当前字段值与提供给 Formik 表单的初始值进行比较。这对于单个值非常有效,例如更改的标题。但是,我需要实现的表单也有像创建者这样的多值字段。我实现了一个自定义字段,用户可以在其中为一个字段选择/提供多个值。此字段的值在 Formik 表单中保存为数组。问题是 Formik 还将此字段的 initialValue 更改为当前保存的数组,因此我无法再检查该字段是否已更新。此外,我将后端提供的元数据字段保存在一个状态值中,因为响​​应包含一些进一步实现所需的进一步信息。此状态值还包含元数据字段具有并用作 Formik 表单的初始值的当前值(更新前)。奇怪的是,多字段组件不仅覆盖了Formik表单字段的initialValue,而且还覆盖了处于只读状态,从不直接更新的字段值。

我用于编辑元数据的对话框如下所示:

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;
};

这是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;

这是更改前和更改后的 initialValuesInitialValues before change InitialValues after change

这是改变前后MetadataFieldsFetchedValues的状态: State before change State after change

0 个答案:

没有答案