trigger onChange defined inside a component when setValue is called

Viewed 747

I have built a wrapper around react-dropzone:

Dropzone.jsx

import React, { useState, useCallback } from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import { useDropzone } from "react-dropzone";

import { noop } from "../utils";

const Dropzone = ({
  id,
  name,
  label,
  className,
  touched,
  error,
  showError,
  children,
  onDrop,
  showUploadedFiles,
  onChange,
  onRemoveFile // ...rest
}) => {
  const [uploadedFiles, setUploadedFiles] = useState([]);

  const _onChange = value => {
    onChange();
    console.log(">>>>>", "onchange triggerd", value);
    setUploadedFiles(prevValue => [
      ...prevValue.map(fileObj => {
        if (fileObj.file.name === value.filename) {
          return {
            id: value.id,
            file: fileObj.file
          };
        }
        return fileObj;
      })
    ]);
  };

  const _onDrop = useCallback(
    acceptedFiles => {
      setUploadedFiles(prevValue => [
        ...prevValue,
        ...acceptedFiles.map(file => ({
          id: "PLACEHOLDER",
          file
        }))
      ]);
      onDrop(acceptedFiles);
    },
    [onDrop]
  );

  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop: _onDrop
  });
  return (
    // eslint-disable-next-line jsx-a11y/label-has-for
    <label htmlFor={id || name}>
      <span className="label-text">{label}</span>
      {showUploadedFiles && (
        <div className="uploaded-files">
          {uploadedFiles.length > 0
            ? uploadedFiles.map(fileObj => (
                <div className="uploaded-file">
                  <span className="file-name">{fileObj.file.name}</span>
                </div>
              ))
            : "No files uploaded"}
        </div>
      )}
      <div
        {...getRootProps()}
        onChange={_onChange}
        className="dropzone-container"
      >
        <input {...getInputProps()} />
        <div className={classNames("dropzone", className)}>
          {children ? (
            children(isDragActive)
          ) : (
            <div className="dropzone-content">
              <span>Upload</span>
            </div>
          )}
        </div>
      </div>
      {showError && touched && error.message && (
        <span className="error">{error.message}</span>
      )}
    </label>
  );
};

Dropzone.propTypes = {
  id: PropTypes.string,
  name: PropTypes.string.isRequired,
  placeholder: PropTypes.string,
  label: PropTypes.string,
  control: PropTypes.instanceOf(Object).isRequired,
  className: PropTypes.string,
  popperClassName: PropTypes.string,
  touched: PropTypes.bool,
  error: PropTypes.instanceOf(Object),
  showError: PropTypes.bool,
  children: PropTypes.func,
  onDrop: PropTypes.func,
  showUploadedFiles: PropTypes.bool,
  onChange: PropTypes.func,
  onRemoveFile: PropTypes.func
};

Dropzone.defaultProps = {
  id: "",
  placeholder: "",
  label: "",
  className: "",
  popperClassName: "",
  touched: false,
  error: {},
  showError: false,
  children: null,
  onDrop: noop,
  showUploadedFiles: false,
  onChange: noop,
  onRemoveFile: noop
};

export default Dropzone;

And a wrapper around Dropzone.jsx to use it with react-hook-form.

DropzoneWrapper.jsx

import React from "react";
import PropTypes from "prop-types";
import { Controller } from "react-hook-form";

import Dropzone from "./Dropzone";

const DropzoneWrapper = ({ control, ...rest }) => (
  <Controller as={Dropzone} control={control} showError {...rest} />
);

DropzoneWrapper.propTypes = {
  control: PropTypes.instanceOf(Object).isRequired
};

export default DropzoneWrapper;

I have also built a new component for Form. But I won't go into the details of that as it is not necessary to show it here for solving this problem. But I have built a codesandbox, in which we have a file called Form.jsx

In the App.jsx, When I click the Click Me button, I call setValue on Dropzone.

import React from "react";
import "./styles.css";
import { useForm } from "react-hook-form";

import Form from "./controls/Form";
import DropzoneWrapper from "./controls/DropzoneWrapper";

export default function App() {
  const formMethods = useForm({});
  const { setValue } = formMethods;

  const onButtonClick = () => {
    setValue("example", { filename: "test", id: "1234" });
  };

  return (
    <div className="App">
      <Form methods={formMethods}>
        <DropzoneWrapper field name="example" showUploadedFiles />
        <button onClick={onButtonClick} type="button">
          Click Me
        </button>
      </Form>
    </div>
  );
}

I don't know how to wire it up with onChange function of Dropzone.jsx, so that when I do setValue('example', something), console.log inside _onChange of Dropzone.jsx is called.

Again here is the codesandbox link if you missed it:

Edit purple-brook-nxpm7

Update


@Sabit Rakhim Thank you for your hard work and time. But unfortunately this is not what I intended. Button click is just an example here.

  • Actually, when file is chosen, onDrop function will be called.
  • I will fire a redux action in the body of onDrop function, which will call the API to upload the file.
  • When file is uploaded, server will return me uuid.
  • Now, I will fetch that uuid in my App.jsx file and then, I will send it to the dropzone component by doing setValue.

Now, if my component (dropzone in this case)'s onChange method gets called by setValue in App.jsx, then I will replace the id of uploaded file from PLACEHOLDER to actual id.

Now, when I submit the form, I should get those files inside the form's values that are being passed as the parameter to submit function.

This is the whole flow.

1 Answers

Maybe not in the best way)

Dropzone

const Dropzone = ({
  id,
  name,
  label,
  className,
  touched,
  error,
  showError,
  children,
  onDrop,
  showUploadedFiles,
  onChange,
  isClicked, // get value from props
  setClicked,
  onRemoveFile // ...rest
}) => {
  const [uploadedFiles, setUploadedFiles] = useState([]);
  const _onChange = value => {
    console.log(">>>>>", "onchange triggerd", value);
    onChange();
    setUploadedFiles(prevValue => [
      ...prevValue.map(fileObj => {
        if (fileObj.file.name === value.filename) {
          return {
            id: value.id,
            file: fileObj.file
          };
        }
        return fileObj;
      })
    ]);
    setClicked(false); // you can remove it if you sure that users click only 1 time
  };
  React.useEffect(() => { //run _onChange if click event change, you can add conditions there
    _onChange();
  }, [isClicked]);

App.js


export default function App() {
  const formMethods = useForm({});
  const [isClicked, setClicked] = React.useState(false); //added bool to check status
  const { setValue } = formMethods;

  const onButtonClick = () => {
    setValue("example", { filename: "test", id: "1234" });
    setClicked(true);
  };

  return (
    <div className="App">
      <Form methods={formMethods}>
        <DropzoneWrapper
          field
          name="example"
          showUploadedFiles
          isClicked={isClicked}
          setClicked={setClicked}
        />
        <button onClick={onButtonClick} type="button">
          Click Me
        </button>
      </Form>
    </div>
  );
}

Related