【问题标题】:How to upload file with redux-form?如何使用 redux-form 上传文件?
【发布时间】:2017-02-03 12:41:01
【问题描述】:

尝试上传文件时,我无法在商店中获得正确的值。我得到的不是文件内容,而是{ 0: {} }。 代码如下:

const renderInput = field => (
  <div>
    <input {...field.input} type={field.type}/>
    {
      field.meta.touched &&
      field.meta.error &&
      <span className={styles.error}>{field.meta.error}</span>
    }
  </div>
);

render() {

  ...

  <form className={styles.form} onSubmit={handleSubmit(submit)}>
    <div className={styles.interface}>
      <label>userpic</label>
      <Field
        name="userpic"
        component={renderInput}
        type="file"
      />
    </div>
    <div>
      <button type="submit" disabled={submitting}>Submit</button>
    <div>
  </form>

  ...

}

我在网上找到的所有示例都是使用 redux-form v5 制作的。

如何在 redux-form v6 中进行文件输入?

【问题讨论】:

    标签: redux redux-form


    【解决方案1】:

    创建一个像这样的字段组件:

    import React, {Component} from 'react'
    
    export default class FieldFileInput  extends Component{
      constructor(props) {
        super(props)
        this.onChange = this.onChange.bind(this)
      }
    
      onChange(e) {
        const { input: { onChange } } = this.props
        onChange(e.target.files[0])
      }
    
      render(){
        const { input: { value } } = this.props
        const {input,label, required, meta, } = this.props  //whatever props you send to the component from redux-form Field
        return(
         <div><label>{label}</label>
         <div>
           <input
            type='file'
            accept='.jpg, .png, .jpeg'
            onChange={this.onChange}
           />
         </div>
         </div>
        )
    }
    }
    

    将此组件传递给您需要的 Field 组件。如果您追求简单的文件上传功能,则无需额外的 Dropzone 或其他库。

    【讨论】:

    • 你的答案应该是公认的答案,因为它是让它在没有任何其他依赖的情况下与 redux-form 一起工作的更简单的方法。 :+1:
    • @OwlyMoly Uncaught TypeError: Cannot read property 'onChange' of undefined
    • @shorif2000 :这很奇怪,这对我有用。您是否尝试过将句柄更改方法作为该字段的 onChange 属性传递给该字段。示例:` `
    • @shorif2000 使用此句柄更改触发change 以在每次更改上传的文件时处理字段值更改。我希望这能解决您的问题。
    • 这样做会导致控制台出现错误A non-serializable value was detected in an action, in the path: payload. Value:。它正在工作,但打印出来,有什么想法吗?
    【解决方案2】:

    我的带有Dropzone的redux表单输入包装器示例

    import React, {Component, PropTypes} from 'react';
    import Dropzone from 'react-dropzone';
    import { Form } from 'elements';
    import { Field } from 'redux-form';
    
    class FileInput extends Component {
      static propTypes = {
        dropzone_options: PropTypes.object,
        meta: PropTypes.object,
        label: PropTypes.string,
        classNameLabel: PropTypes.string,
        input: PropTypes.object,
        className: PropTypes.string,
        children: PropTypes.node,
        cbFunction: PropTypes.func,
      };
    
      static defaultProps = {
        className: '',
        cbFunction: () => {},
      };
    
      render() {
        const { className, input: { onChange }, dropzone_options, meta: { error, touched }, label, classNameLabel, children, name, cbFunction } = this.props;
    
        return (
          <div className={`${className}` + (error && touched ? ' has-error ' : '')}>
            {label && <p className={classNameLabel || ''}>{label}</p>}
            <Dropzone
              {...dropzone_options}
              onDrop={(f) => {
                cbFunction(f);
                return onChange(f);
              }}
              className="dropzone-input"
              name={name}
            >
              {children}
            </Dropzone>
            {error && touched ? error : ''}
          </div>
        );
      }
    }
    export default props => <Field {...props} component={FileInput} />;
    

    热使用它:

    <FileInput
     name="add_photo"
     label="Others:"
      classNameLabel="file-input-label"
      className="file-input"
      dropzone_options={{
        multiple: false,
        accept: 'image/*'
      }}
    >
      <span>Add more</span>
    </FileInput>
    

    【讨论】:

    • 我相信onDrop函数中收到的文件需要先用FileReader读取,然后才能发送到服务器。否则没用。如果它有助于完全回答原始问题,我可以在这里举一个例子。
    【解决方案3】:

    另一种渲染预览图像的方法(下面的示例使用 React 16+ 语法并且只接受单个图像文件发送到 API;但是,通过一些小的调整,它也可以扩展到多个图像和其他字段输入):

    工作示例https://codesandbox.io/s/m58q8l054x

    工作示例(过时)https://codesandbox.io/s/8kywn8q9xl


    之前

    之后


    容器/UploadForm.js

    import React, { Component } from "react";
    import { Form, Field, reduxForm } from "redux-form";
    import DropZoneField from "../components/dropzoneField";
    
    const imageIsRequired = value => (!value ? "Required" : undefined);
    
    class UploadImageForm extends Component {
      state = { imageFile: [] };
    
      handleFormSubmit = formProps => {
        const fd = new FormData();
        fd.append("imageFile", formProps.imageToUpload.file);
        // append any additional Redux form fields
        // create an AJAX request here with the created formData
    
        alert(JSON.stringify(formProps, null, 4));
      };
    
      handleOnDrop = (newImageFile, onChange) => {
        const imageFile = {
          file: newImageFile[0],
          name: newImageFile[0].name,
          preview: URL.createObjectURL(newImageFile[0]),
          size: newImageFile[0].size
        };
    
        this.setState({ imageFile: [imageFile] }, () => onChange(imageFile));
      };
    
      resetForm = () => this.setState({ imageFile: [] }, () => this.props.reset());
    
      render = () => (
        <div className="app-container">
          <h1 className="title">Upload An Image</h1>
          <hr />
          <Form onSubmit={this.props.handleSubmit(this.handleFormSubmit)}>
            <Field
              name="imageToUpload"
              component={DropZoneField}
              type="file"
              imagefile={this.state.imageFile}
              handleOnDrop={this.handleOnDrop}
              validate={[imageIsRequired]}
            />
            <button
              type="submit"
              className="uk-button uk-button-primary uk-button-large"
              disabled={this.props.submitting}
            >
              Submit
            </button>
            <button
              type="button"
              className="uk-button uk-button-default uk-button-large"
              disabled={this.props.pristine || this.props.submitting}
              onClick={this.resetForm}
              style={{ float: "right" }}
            >
              Clear
            </button>
          </Form>
          <div className="clear" />
        </div>
      );
    }
    
    export default reduxForm({ form: "UploadImageForm" })(UploadImageForm);
    

    components/dropzoneField.js

    import React from "react";
    import PropTypes from "prop-types";
    import DropZone from "react-dropzone";
    import ImagePreview from "./imagePreview";
    import Placeholder from "./placeholder";
    import ShowError from "./showError";
    
    const DropZoneField = ({
      handleOnDrop,
      input: { onChange },
      imagefile,
      meta: { error, touched }
    }) => (
      <div className="preview-container">
        <DropZone
          accept="image/jpeg, image/png, image/gif, image/bmp"
          className="upload-container"
          onDrop={file => handleOnDrop(file, onChange)}
        >
          {({ getRootProps, getInputProps }) =>
            imagefile && imagefile.length > 0 ? (
              <ImagePreview imagefile={imagefile} />
            ) : (
              <Placeholder
                error={error}
                touched={touched}
                getInputProps={getInputProps}
                getRootProps={getRootProps}
              />
            )
          }
        </DropZone>
        <ShowError error={error} touched={touched} />
      </div>
    );
    
    DropZoneField.propTypes = {
      error: PropTypes.string,
      handleOnDrop: PropTypes.func.isRequired,
      imagefile: PropTypes.arrayOf(
        PropTypes.shape({
          file: PropTypes.file,
          name: PropTypes.string,
          preview: PropTypes.string,
          size: PropTypes.number
        })
      ),
      label: PropTypes.string,
      onChange: PropTypes.func,
      touched: PropTypes.bool
    };
    
    export default DropZoneField;
    

    components/imagePreview.js

    import React from "react";
    import PropTypes from "prop-types";
    
    const ImagePreview = ({ imagefile }) =>
      imagefile.map(({ name, preview, size }) => (
        <div key={name} className="render-preview">
          <div className="image-container">
            <img src={preview} alt={name} />
          </div>
          <div className="details">
            {name} - {(size / 1024000).toFixed(2)}MB
          </div>
        </div>
      ));
    
    ImagePreview.propTypes = {
      imagefile: PropTypes.arrayOf(
        PropTypes.shape({
          file: PropTypes.file,
          name: PropTypes.string,
          preview: PropTypes.string,
          size: PropTypes.number
        })
      )
    };
    
    export default ImagePreview;
    

    components/placeholder.js

    import React from "react";
    import PropTypes from "prop-types";
    import { MdCloudUpload } from "react-icons/md";
    
    const Placeholder = ({ getInputProps, getRootProps, error, touched }) => (
      <div
        {...getRootProps()}
        className={`placeholder-preview ${error && touched ? "has-error" : ""}`}
      >
        <input {...getInputProps()} />
        <MdCloudUpload style={{ fontSize: 100, paddingTop: 85 }} />
        <p>Click or drag image file to this area to upload.</p>
      </div>
    );
    
    Placeholder.propTypes = {
      error: PropTypes.string,
      getInputProps: PropTypes.func.isRequired,
      getRootProps: PropTypes.func.isRequired,
      touched: PropTypes.bool
    };
    
    export default Placeholder;
    

    components/showError.js

    import React from "react";
    import PropTypes from "prop-types";
    import { MdInfoOutline } from "react-icons/md";
    
    const ShowError = ({ error, touched }) =>
      touched && error ? (
        <div className="error">
          <MdInfoOutline
            style={{ position: "relative", top: -2, marginRight: 2 }}
          />
          {error}
        </div>
      ) : null;
    
    ShowError.propTypes = {
      error: PropTypes.string,
      touched: PropTypes.bool
    };
    
    export default ShowError;
    

    styles.css

    img {
      max-height: 240px;
      margin: 0 auto;
    }
    
    .app-container {
      width: 500px;
      margin: 30px auto;
    }
    
    .clear {
      clear: both;
    }
    
    .details,
    .title {
      text-align: center;
    }
    
    .error {
      margin-top: 4px;
      color: red;
    }
    
    .has-error {
      border: 1px dotted red;
    }
    
    .image-container {
      align-items: center;
      display: flex;
      width: 85%;
      height: 80%;
      float: left;
      margin: 15px 10px 10px 37px;
      text-align: center;
    }
    
    .preview-container {
      height: 335px;
      width: 100%;
      margin-bottom: 40px;
    }
    
    .placeholder-preview,
    .render-preview {
      text-align: center;
      background-color: #efebeb;
      height: 100%;
      width: 100%;
      border-radius: 5px;
    }
    
    .upload-container {
      cursor: pointer;
      height: 300px;
    }
    

    【讨论】:

    • 感谢代码,不过这不适用于最新版本的 react-dropzone。请务必在您的依赖项中使用"react-dropzone": "5.0.1"
    • @Fr4nz -- 将所有依赖项更新为最新并包含新代码。
    【解决方案4】:

    我设法在 material-ui 上使用 redux-form 来包装 TextField,如下所示:

    B4 编辑:

    修改后:

     <Field name="image" component={FileTextField} floatingLabelText={messages.chooseImage} fullWidth={true} />
    

    组件定义为:

    const styles = {
      button: {
        margin: 12
      },
      exampleImageInput: {
        cursor: 'pointer',
        position: 'absolute',
        top: 0,
        bottom: 0,
        right: 0,
        left: 0,
        width: '100%',
        opacity: 0
      },
      FFS:{
        position: 'absolute',
        lineHeight: '1.5',
        top: '38',
        transition: 'none',
        zIndex: '1',
        transform: 'none',
        transformOrigin: 'none',
        pointerEvents: 'none',
        userSelect: 'none',
        fontSize: '16',
        color: 'rgba(0, 0, 0, 0.8)',
      }
    };
    
    export const FileTextField  = ({
                                      floatingLabelText,
                                      fullWidth,
                                      input,
                                      label,
                                      meta: { touched, error },
                                      ...custom })=>{
      if (input.value && input.value[0] && input.value[0].name) {
        floatingLabelText = input.value[0].name;
      }
      delete input.value;
      return (
        <TextField
          hintText={label}
          fullWidth={fullWidth}
          floatingLabelShrinkStyle={styles.FFS}
          floatingLabelText={floatingLabelText}
          inputStyle={styles.exampleImageInput}
          type="file"
          errorText={error}
          {...input}
          {...custom}
        />
      )
    }
    

    【讨论】:

      【解决方案5】:

      如果您需要 base64 编码将其发送到您的后端,这里有一个适合我的修改版本:

      export class FileInput extends React.Component {
      
        getBase64 = (file) => {
          return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.readAsDataURL(file);
            reader.onload = () => resolve(reader.result);
            reader.onerror = error => reject(error);
          });
        }
      
        onFileChange = async (e) => {
          const { input } = this.props
          const targetFile = e.target.files[0]
          if (targetFile) {
            const val = await this.getBase64(targetFile)
            input.onChange(val)
          } else {
            input.onChange(null)
          }
        }
      
        render() {
      
          return (
            <input
              type="file"
              onChange={this.onFileChange}
            />
          )
        }
      }
      

      那么您的字段组件将如下所示:

      &lt;Field component={FileInput} name="primary_image" type="file" /&gt;

      【讨论】:

      • 如何删除 blob 文件链接示例 Data:application/pdf;base64, docBlob: "data:application/pdf;base64,JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9Qcm9kdWNlcihEeW5hbWljUERIGZvciAuTkVUIHY4LjAuMS40MCBcKE
      【解决方案6】:

      对于 React >= 16 和 ReduxForm >= 8(测试版本是 16.8.6 for React 和 8.2.5) 跟随组件工作。

      (解决方案由 DarkBitz 在相关GitHub issue 中发布)

      const adaptFileEventToValue = delegate => e => delegate(e.target.files[0]);
      
      const FileInput = ({ 
        input: { value: omitValue, onChange, onBlur, ...inputProps }, 
        meta: omitMeta, 
        ...props 
      }) => {
        return (
          <input
            onChange={adaptFileEventToValue(onChange)}
            onBlur={adaptFileEventToValue(onBlur)}
            type="file"
            {...props.input}
            {...props}
          />
        );
      };
      
      export const FileUpload = (props) => {
          const { handleSubmit } = props;
          const onFormSubmit = (data) => {
              console.log(data);
          }
          return (
                <form onSubmit={handleSubmit(onFormSubmit)}>
                  <div>
                    <label>Attachment</label>
                    <Field name="attachment" component={FileInput} type="file"/>
                  </div>
                  <button type="submit">Submit</button>
                </form>
          )
      }
      

      【讨论】:

      • 我在使用您的解决方案时得到了这个 ``` 错误:不变量失败:在分派之间检测到状态突变,在路径 'form.UploadImageForm.values.attachment.lastModifiedDate' 中。` ``
      【解决方案7】:

      使用 Redux 表单

      const { handleSubmit } = props;
        //make a const file to hold the file prop.
        const file = useRef();
        
        // create a function to replace the redux-form input-file value to custom value.
        const fileUpload = () => {
        // jsx to take file input
        // on change store the files /file[0] to file variable
          return (
            <div className='file-upload'>
              <input
                type='file'
                id='file-input'
                accept='.png'
                onChange={(ev) => {
                  file.current = ev.target.files;
                }}
                required
              />
            </div>
          );
        };
      
        //catch the redux-form values!
        //loop through the files and add into formdata
        //form data takes key and value
        //enter the key name as multer-config fieldname
        //then add remaining data into the formdata
        //make a request and send data.
        const onSubmitFormValues = (formValues) => {
          const data = new FormData();
          for (let i = 0; i < file.current.length; i++) {
            data.append("categoryImage", file.current[i]);
          }
      
          data.append("categoryName", formValues.categoryName);
        Axios.post("http://localhost:8080/api/v1/dev/addNewCategory", data)
            .then((response) => console.log(response))
            .catch((err) => console.log(err));
        };
      <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

      【讨论】:

        【解决方案8】:

        您也可以为此目的使用 react-dropzone。下面的代码对我来说很好

        filecomponent.js

        import React from 'react'
        import { useDropzone } from 'react-dropzone'
        function MyDropzone(props) {
        
            const onDrop = (filesToUpload) => {
                return props.input.onChange(filesToUpload[0]);
            }
        
            const onChange = (filesToUpload) => {
                return props.input.onChange(filesToUpload[0]);
            }
        
            const { getRootProps, getInputProps } = useDropzone({ onDrop });
            return (
                 <div {...getRootProps()}>
                  <input {...getInputProps()} onChange={e => onChange(e.target.files)} />
                  <p> Drop or select yout file</p>
                 </div>
                )
        }
        
        export default MyDropzone;
        

        在表格中使用这个

             <Field
               name="myfile"
               component={renderFile}
             />
        

        【讨论】:

          猜你喜欢
          • 2018-06-15
          • 2017-05-02
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-07-12
          • 2015-10-23
          • 2011-12-03
          • 1970-01-01
          相关资源
          最近更新 更多