1

I have a form created with redux-form. I added a dropzone created with react-dropzone.

I have two endpoints:

  • [POST] /image: to upload an image (using react-dropzone). This returns a payload that contains the ID of the just uploaded file (saved in the database in the backend)
  • [POST] /my_entity: to save the entity I'm creating (using redux-form)

The uploading process is this:

  1. When drop one or more files in the dropzone, then a callback from the main form is called;
  2. The callback from the main form calls a thunk that uploads the file calling my upload endpoint ([POST] /image);
  3. My upload endpoint returns a payload with the data of the just uploaded file and its assigned ID in the form of a resource URI (ex.: /images/1)
  4. A redux managed by redux-orm saves the payload in Redux

WHAT I NEED TO ACHIEVE

I need to add the ID of the just uploaded file to a field managed by redux-form.

When clicking "Save" or "Update", I need to send to call [POST] /my_entity.

The payload I need to send is similar to this:

{
  "title": "The custom title",
  "newImages": [
    "/images/1",
    "/images/2",
    "/images/3"
  ]
}

WHICH IS THE PROBLEM

My problem is that I'm not able to find a path to add the resource URI (ex.: image/1) to the field newImages managed by redux-form.

My code

The Form component

class Form extends React.Component {
  constructor(props) {
    super(props);
    this.state = { uploadingImages: [] };
    this.onDrop = this.onDrop.bind(this);
  }

  onDrop(acceptedFiles) {
    const { onImageUpload } = this.props;

    acceptedFiles.map((file) => {
      const reader = new FileReader();
      const thumbnailTmpId = cuid();

      reader.onload = (e) => {
        // Add the image into the state.
        // Since FileReader reading process is asynchronous,
        // it's better to get the latest snapshot state
        // (i.e., prevState) and update it.
        this.setState((prevState) => ({
          uploadingImages: [
            ...prevState.uploadingImages,
            { id: thumbnailTmpId, src: e.target.result },
          ],
        }));
      };
      reader.readAsDataURL(file);

      // ! ! !
      //
      // This is the uploadImageThunk
      //
      // ! ! !
      onImageUpload(thumbnailTmpId, file);

      return file;
    });
  }

  // ... Other code

  render() {
    const {
      change,
      currentItemModel,
      handleSubmit,
      disableButton,
      isWorking,
      toggleEditing,
      fieldIsVariable,
      specificsMap,
    } = this.props;
    const { openVariantsBuilder, uploadingImages } = this.state;
    const buttonLabel = currentItemModel ? 'Aggiorna' : 'Crea';
    const itemImages = null !== currentItemModel ? currentItemModel.itemImages : [];

    return (
      // "handleSubmit" is the prop passed by the parent component as exactly "onSubmit"
      // https://github.com/erikras/redux-form/issues/1270#issuecomment-239636759
      <form onSubmit={handleSubmit}>
        <div id="Item" className="container">
          <div className="row">
            <div className="col-sm-4">
              <div className="Section boxed rounded">
                <div className="row">
                  <div className="col-12">
                    <Field
                      name="newImages"
                      component={RenderField}
                      type="dropzone"
                      handleOnDrop={this.onDrop}
                      idPrefix={idPrefix}
                      className="form-control-sm"
                      disabled={isWorking}
                      accept="image/*"
                    />
                  </div>
                </div>
                <div className="row">
                  <div className="col-12">
                    <ImagesGrid uploadingImages={uploadingImages} images={itemImages}/>
                  </div>
                </div>
              </div>
            </div>
            <div className="col-sm-8">
              <div className="Section boxed rounded">
                <div className="row">
                  <div className="col-sm-12">
                    <Field
                      component={RenderField}
                      name="title"
                      type="text"
                      placeholder="Nome"
                      idPrefix={idPrefix}
                      className="form-control-sm"
                      disabled={isWorking}
                      label="Nome"
                    />
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
        <div className="row">
          <div className="col-sm-12 text-center">
            <button type="submit" className="btn btn-primary btn-sm" disabled={disableButton}>
              {buttonLabel}
              {isWorking && <Spinner/>}
            </button>
            {currentItemModel ? (
              <button
                type="button"
                className="btn btn-outline-primary btn-sm"
                onClick={() => toggleEditing({ id: currentItemModel.id, editing: false })}
              >
                Cancel
              </button>
            ) : (
              ''
            )}
          </div>
        </div>
      </form>
    );
  }
}

In this component, note two things:

  1. In onDrop() method I call onImageUpload(): this is the thunk uploadImageThunk()
  2. To render a field I use the component RenderField.

The uploadImageThunk()

// ...

export function uploadImageThunk(thumbnailTmpId, ImageFile, currentModel) {
  return (dispatch, getState) => {
    const callbacks = [];
    const onUploadProgress = (progressEvent) => {
      const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
      dispatch(uploadImageProgressAction({ id: thumbnailTmpId, progress }));
      return progress;
    };
    callbacks.push({ name: 'onUploadProgress', callback: onUploadProgress });

    dispatch(uploadImageUploadingAction({ id: thumbnailTmpId, uploading: true }));

    const token = ctxGetUserToken(getState());
    const account = dbGetAccountDetails(getState());

    const ImageData = new FormData();
    ImageData.append('file', ImageFile);
    const placeholders = [{ replace: '__account__', value: account.id }];

    return upload(
      FILE_IMAGE_UPLOAD_ENDPOINT,
      token,
      placeholders,
      HTTP_POST,
      ImageData,
      [],
      callbacks
    )
      .then((response) => {
        dispatch(uploadImageUploadingAction({ id: thumbnailTmpId, uploading: false }));

        return response.data;
      })
      .then((data) => {
        dispatch(uploadImageSuccessAction(data));

        return data;
      })
      .catch((e) => {
        dispatch(uploadImageUploadingAction({ id: thumbnailTmpId, uploading: false }));

        if (e instanceof SubmissionError) {
          dispatch(uploadImageErrorAction({ id: thumbnailTmpId, error: e.errors._error }));
          throw e;
        }

        dispatch(uploadImageErrorAction({ id: thumbnailTmpId, error: e.errors._error }));
      });
  };
}

The RenderField component

const RenderField = (props) => {
  const {
    disabled,
    input,
    meta,
    placeholder,
    required,
    type,
    prepend,
    prependclass,
    options,
    defaultOption,
    renderAsColorField,
    specificColorValues,
    changeHandler,
    checkboxState,
    handleOnDrop,
  } = props;
  let { idPrefix, labelClasses, label, className } = props;
  // Coercing error to boolean with !! (https://stackoverflow.com/a/29951409/1399706)
  const hasError = meta.touched && !!meta.error;
  if (hasError) {
    props.input['aria-describedby'] = `${props.idPrefix}_${props.input.name}_helpBlock`;
    props.input['aria-invalid'] = true;
  }

  idPrefix = null === idPrefix ? '' : `_${idPrefix}`;
  const id = `${idPrefix}_${input.name}`;

  let RenderingField = null;
  let fieldContainerClasses = `form-group${hasError ? ' has-error' : ''}`;

  // ...

  switch (type) {
    // ...
    case 'dropzone':
      RenderingField = DropZoneField;
      break;
    default:
      throw new Error(`Unrecognized field ${type}. Allowed types are: "text", "textarea".`);
  }

  return (
    <div className={fieldContainerClasses}>
      {/* The checkbox has the implementation of labels and errors directly in the component */}
      {label && 'checkbox' !== type && (
        <label htmlFor={id} className={labelClasses}>
          {label}
        </label>
      )}
      {hasError && 'checkbox' !== type && (
        <span className="help-block" id={`${idPrefix}_${input.name}_helpBlock`}>
          {meta.error}
        </span>
      )}
      <RenderingField
        {...input}
        type={type}
        required={required}
        placeholder={placeholder}
        disabled={disabled}
        id={id}
        className={className}
        prepend={prepend}
        prependclass={prependclass}
        options={options}
        defaultoption={defaultOption}
        specificColorValues={specificColorValues}
        renderAsColorField
        changeHandler={changeHandler}
        label={label}
        state={checkboxState}
        handleOnDrop={handleOnDrop}
      />
    </div>
  );
};

The DropZoneField component

The component that actually handles the drag 'n' drop is this:

const DropZoneField = (props) => {
  const { handleOnDrop } = props;

  return (
    <Dropzone onDrop={handleOnDrop}>
      {({ getRootProps, getInputProps, isDragActive }) => (
        <section>
          <div {...getRootProps()}>
            <input {...getInputProps()} />
            <div
              className={`text-center dropzone-area rounded ${
                isDragActive ? 'dropzone-drag-active' : 'dropzone-drag-not-active'
              }`}
            >
              {isDragActive ? (
                <p className="dropzone-text">
                  Ora rilascia le immagini per caricarle nel prodotto.
                </p>
              ) : (
                <p className="dropzone-text">
                  Sposta qui le immagini o clicca per selezionarle dal tuo computer.
                </p>
              )}
            </div>
          </div>
        </section>
      )}
    </Dropzone>
  );
}

Now, the two parts works each one on its own:

  1. The file is correctly uploaded
  2. When saving the form, the entity is correctly saved

What I cannot achieve is to send, along with the entity's information, also the resource URIs of the just uploaded files.

I cannot find a path to add the resource URIs to the field in redux-form.

I read some articles online and found some good answers here on StackOverflow, but, after trying, I'm failing: none of them clarified me which path to follow and how to pass data from the callback method Form.onDrop() to the field newImages in the Form.

Any help is much appreciated as I'm struggling with this for many days.

Please, keep in mind one last thing: this flow will be used in many places and with many different entitites. The endpoint to upload the files is always the same while the forms and the endpoints of the entities will be many and different from one to the other.

Thank you very much for reading until here, also if you don't have an answer for me.

Aerendir
  • 6,152
  • 9
  • 55
  • 108

0 Answers0