Last active
May 3, 2019 09:05
-
-
Save SirMoustache/55571c699980d6ff7e789b822452bef7 to your computer and use it in GitHub Desktop.
React File input using: redux-form, @material-ui and react-dropzone
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Absolute imports | |
| */ | |
| import React, { PureComponent } from 'react'; | |
| import PropTypes from 'prop-types'; | |
| import { compose as reduxCompose } from 'redux'; | |
| import { Field, reduxForm } from 'redux-form/immutable'; | |
| import { FormattedMessage } from 'react-intl'; | |
| /** | |
| * Global Components | |
| */ | |
| import FormFileField from '../FormFileField'; | |
| /** | |
| * Messages | |
| */ | |
| import messages from './messages'; | |
| class OrderForm extends PureComponent { | |
| handleFileChange = () => { | |
| const { submit } = this.props; | |
| setTimeout(() => submit(), 50); | |
| }; | |
| render() { | |
| const { handleSubmit } = this.props; | |
| return ( | |
| <form onSubmit={handleSubmit}> | |
| <Field | |
| name="Order" | |
| component={FormFileField} | |
| onFileChange={this.handleFileChange} | |
| label={<FormattedMessage {...messages.uploadOrder} />} | |
| /> | |
| </form> | |
| ); | |
| } | |
| } | |
| OrderForm.propTypes = { | |
| handleSubmit: PropTypes.func.isRequired, | |
| submit: PropTypes.func.isRequired, | |
| }; | |
| export default reduxCompose( | |
| reduxForm(), | |
| )(OrderForm); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Absolute imports | |
| */ | |
| import React, { useCallback, useState, useMemo } from 'react'; | |
| import PropTypes from 'prop-types'; | |
| import { FormattedMessage } from 'react-intl'; | |
| import styled from 'styled-components'; | |
| import isEmpty from 'ramda/src/isEmpty'; | |
| /** | |
| * Global Components | |
| */ | |
| import PurchaseOrderFile from '../PurchaseOrderFile'; | |
| import PurchaseOrderHint from '../PurchaseOrderHint'; | |
| import LoadingOverlay from '../LoadingOverlay'; | |
| import FileField from '../FileField'; | |
| /** | |
| * Utils | |
| */ | |
| import { allFilesMatchSize, allFilesAccepted } from '../../utils/fileUtils'; | |
| /** | |
| * Messages | |
| */ | |
| import messages from './messages'; | |
| import validationMessages from '../../messages/validationMessages'; | |
| import { bytesToMB } from '../../utils/valueUtils'; | |
| const Root = styled.div` | |
| position: relative; | |
| `; | |
| const PurchaseOrder = ({ | |
| allowedFileFormats = ['.doc', '.docx', '.pdf'], | |
| minFileSize = 0, | |
| maxFileSize = 100000, // 10000000 | |
| isDisabled, | |
| isLoading, | |
| purchaseOrderFile, | |
| onPurchaseOrderDelete, | |
| onPurchaseOrderUpload, | |
| }) => { | |
| const [rejectedFiles, setRejectedFiles] = useState([]); | |
| const [acceptedFiles, setAcceptedFiles] = useState([]); | |
| const handleChange = useCallback( | |
| (accepted, rejected) => { | |
| setAcceptedFiles(accepted); | |
| setRejectedFiles(rejected); | |
| if (!isEmpty(accepted)) { | |
| onPurchaseOrderUpload(accepted); | |
| } | |
| }, | |
| [onPurchaseOrderUpload, setAcceptedFiles, setRejectedFiles], | |
| ); | |
| const handleDelete = useCallback( | |
| () => { | |
| setAcceptedFiles([]); | |
| setRejectedFiles([]); | |
| onPurchaseOrderDelete(); | |
| }, | |
| [onPurchaseOrderDelete], | |
| ); | |
| const acceptedFileNames = useMemo( | |
| () => acceptedFiles.map(file => file.name).join(', '), | |
| [acceptedFiles], | |
| ); | |
| const hasFilesRejectedBySize = !allFilesMatchSize( | |
| maxFileSize, | |
| minFileSize, | |
| rejectedFiles, | |
| ); | |
| const hasFilesRejectedByExtension = !allFilesAccepted( | |
| allowedFileFormats, | |
| rejectedFiles, | |
| ); | |
| const hasErrors = useMemo(() => rejectedFiles.length > 0, [rejectedFiles]); | |
| const errorMessage = useMemo( | |
| () => { | |
| if (hasFilesRejectedBySize) { | |
| return ( | |
| <FormattedMessage | |
| {...validationMessages.maximumFileSizeError} | |
| values={{ | |
| sizeLimit: bytesToMB(maxFileSize), | |
| fileQuantity: 1, | |
| }} | |
| /> | |
| ); | |
| } | |
| if (hasFilesRejectedByExtension) { | |
| return ( | |
| <FormattedMessage | |
| {...validationMessages.fileFormatError} | |
| values={{ | |
| fileFormats: allowedFileFormats.join(', '), | |
| }} | |
| /> | |
| ); | |
| } | |
| return null; | |
| }, | |
| [hasFilesRejectedBySize, hasFilesRejectedByExtension], | |
| ); | |
| const isFieldDisabled = isDisabled || isLoading; | |
| return ( | |
| <Root> | |
| {isLoading && <LoadingOverlay size={24} />} | |
| {purchaseOrderFile ? ( | |
| <PurchaseOrderFile | |
| purchaseOrderFileName={purchaseOrderFile.Name} | |
| onPurchaseOrderDelete={handleDelete} | |
| /> | |
| ) : ( | |
| <FileField | |
| accept={allowedFileFormats} | |
| multiple={false} | |
| disabled={isFieldDisabled} | |
| onFileChange={handleChange} | |
| error={hasErrors} | |
| value={acceptedFileNames} | |
| label={<FormattedMessage {...messages.uploadPurchaseOrder} />} | |
| helperText={errorMessage} | |
| /> | |
| )} | |
| <PurchaseOrderHint /> | |
| </Root> | |
| ); | |
| }; | |
| PurchaseOrder.propTypes = { | |
| purchaseOrderFile: PropTypes.shape({ | |
| Name: PropTypes.string, | |
| Id: PropTypes.string, | |
| }), | |
| allowedFileFormats: PropTypes.arrayOf(PropTypes.string), | |
| onPurchaseOrderDelete: PropTypes.func.isRequired, | |
| onPurchaseOrderUpload: PropTypes.func.isRequired, | |
| }; | |
| export default PurchaseOrder; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Absolute imports | |
| */ | |
| import React, { PureComponent } from 'react'; | |
| import PropTypes from 'prop-types'; | |
| import Dropzone from 'react-dropzone'; | |
| /** | |
| * Material UI | |
| */ | |
| import Button from '@material-ui/core/Button'; | |
| import TextField from '@material-ui/core/TextField'; | |
| import IconButton from '@material-ui/core/IconButton'; | |
| import InputAdornment from '@material-ui/core/InputAdornment'; | |
| import CloudUploadIcon from '@material-ui/icons/CloudUpload'; | |
| import DeleteIcon from '@material-ui/icons/Delete'; | |
| /** | |
| * Global components | |
| */ | |
| import Row from '../Row'; | |
| import Column from '../Column'; | |
| class FileField extends PureComponent { | |
| fileInputRef = React.createRef(); | |
| hasFileValue = () => { | |
| const { value } = this.props; | |
| return Boolean(value); | |
| }; | |
| render() { | |
| const { | |
| label, | |
| InputProps, | |
| placeholder, | |
| accept, | |
| multiple, | |
| disabled, | |
| value, | |
| helperText, | |
| error, | |
| onFileChange, | |
| onClick, | |
| onFileDialogCancel, | |
| onClearField, | |
| ...rest | |
| } = this.props; | |
| return ( | |
| <Dropzone | |
| accept={accept} | |
| style={{ position: 'relative' }} | |
| multiple={multiple} | |
| disabled={disabled} | |
| onDrop={onFileChange} | |
| onClick={onClick} | |
| onFileDialogCancel={onFileDialogCancel} | |
| > | |
| <Row alignItems="flex-end"> | |
| <Column> | |
| <Button variant="fab" color="primary" aria-label="Add" mini> | |
| <CloudUploadIcon /> | |
| </Button> | |
| </Column> | |
| <Column flexGrow> | |
| <TextField | |
| inputRef={this.fileInputRef} | |
| label={label} | |
| placeholder={placeholder} | |
| disabled={disabled} | |
| error={error} | |
| value={value} | |
| fullWidth | |
| helperText={helperText} | |
| InputProps={{ | |
| readOnly: true, | |
| endAdornment: this.hasFileValue() && ( | |
| <InputAdornment position="end"> | |
| <IconButton | |
| aria-label="Delete" | |
| color="secondary" | |
| onClick={onClearField} | |
| > | |
| <DeleteIcon /> | |
| </IconButton> | |
| </InputAdornment> | |
| ), | |
| ...InputProps, | |
| }} | |
| {...rest} | |
| /> | |
| </Column> | |
| </Row> | |
| </Dropzone> | |
| ); | |
| } | |
| } | |
| FileField.propTypes = { | |
| multiple: PropTypes.bool, | |
| disabled: PropTypes.bool, | |
| label: PropTypes.node, | |
| placeholder: PropTypes.node, | |
| id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | |
| InputProps: PropTypes.object, | |
| fullWidth: PropTypes.bool, | |
| accept: PropTypes.string, | |
| value: PropTypes.any, | |
| helperText: PropTypes.node, | |
| error: PropTypes.bool, | |
| onFileChange: PropTypes.func, | |
| onClick: PropTypes.func, | |
| onFileDialogCancel: PropTypes.func, | |
| onClearField: PropTypes.func, | |
| }; | |
| FileField.defaultProps = { | |
| onFileChange: x => x, | |
| }; | |
| export default FileField; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Absolute imports | |
| */ | |
| import React, { memo } from 'react'; | |
| import PropTypes from 'prop-types'; | |
| import { useDropzone } from 'react-dropzone'; | |
| /** | |
| * Material UI | |
| */ | |
| import Fab from '@material-ui/core/Fab'; | |
| import TextField from '@material-ui/core/TextField'; | |
| import IconButton from '@material-ui/core/IconButton'; | |
| import InputAdornment from '@material-ui/core/InputAdornment'; | |
| import AttachFileIcon from '@material-ui/icons/AttachFile'; | |
| import DeleteIcon from '@material-ui/icons/Delete'; | |
| import RootRef from '@material-ui/core/RootRef'; | |
| /** | |
| * Global components | |
| */ | |
| import Row from '../Row'; | |
| import Column from '../Column'; | |
| const FileField = memo( | |
| ({ | |
| label, | |
| size = 'small', | |
| buttonColor = 'primary', | |
| InputProps, | |
| placeholder, | |
| accept, | |
| multiple, | |
| disabled, | |
| value, | |
| helperText, | |
| error, | |
| onFileChange, | |
| onClick, | |
| onFileDialogCancel, | |
| onClearField, | |
| ...rest | |
| }) => { | |
| const { getRootProps, getInputProps } = useDropzone({ | |
| accept, | |
| multiple, | |
| disabled, | |
| onClick, | |
| onDrop: onFileChange, | |
| onFileDialogCancel, | |
| }); | |
| const { ref, ...rootProps } = getRootProps(); | |
| const hasFileValue = () => Boolean(value); | |
| return ( | |
| <RootRef rootRef={ref}> | |
| <Row alignItems="flex-end" {...rootProps}> | |
| <input {...getInputProps()} /> | |
| <Column> | |
| <Fab | |
| size={size} | |
| color={buttonColor} | |
| disabled={disabled} | |
| aria-label="Add" | |
| > | |
| <AttachFileIcon /> | |
| </Fab> | |
| </Column> | |
| <Column flexGrow> | |
| <TextField | |
| label={label} | |
| placeholder={placeholder} | |
| disabled={disabled} | |
| error={error} | |
| value={value} | |
| fullWidth | |
| helperText={helperText} | |
| InputProps={{ | |
| readOnly: true, | |
| endAdornment: hasFileValue() && | |
| onClearField && ( | |
| <InputAdornment position="end"> | |
| <IconButton | |
| aria-label="Delete" | |
| color="secondary" | |
| onClick={onClearField} | |
| > | |
| <DeleteIcon /> | |
| </IconButton> | |
| </InputAdornment> | |
| ), | |
| ...InputProps, | |
| }} | |
| {...rest} | |
| /> | |
| </Column> | |
| </Row> | |
| </RootRef> | |
| ); | |
| }, | |
| ); | |
| FileField.propTypes = { | |
| multiple: PropTypes.bool, | |
| disabled: PropTypes.bool, | |
| label: PropTypes.node, | |
| placeholder: PropTypes.node, | |
| id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | |
| InputProps: PropTypes.object, | |
| fullWidth: PropTypes.bool, | |
| accept: PropTypes.string, | |
| value: PropTypes.any, | |
| helperText: PropTypes.node, | |
| error: PropTypes.bool, | |
| onFileChange: PropTypes.func, | |
| onClick: PropTypes.func, | |
| onFileDialogCancel: PropTypes.func, | |
| onClearField: PropTypes.func, | |
| size: PropTypes.oneOf(['small', 'medium', 'large']), | |
| buttonColor: PropTypes.oneOf(['default', 'inherit', 'primary', 'secondary']), | |
| }; | |
| FileField.defaultProps = { | |
| onFileChange: x => x, | |
| }; | |
| export default FileField; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Absolute imports | |
| */ | |
| import React, { PureComponent } from 'react'; | |
| import PropTypes from 'prop-types'; | |
| /** | |
| * Global components | |
| */ | |
| import FileField from '../FileField'; | |
| class FormFileField extends PureComponent { | |
| fileInputRef = React.createRef(); | |
| handleFileChange = acceptedFiles => { | |
| this.handleFieldChange(acceptedFiles); | |
| }; | |
| handleFileDialogCancel = () => { | |
| this.handleFieldChange(''); | |
| }; | |
| handleFieldChange = acceptedFiles => { | |
| const { | |
| input: { onChange, onBlur }, | |
| onFileChange, | |
| } = this.props; | |
| onBlur(acceptedFiles); | |
| onChange(acceptedFiles); | |
| onFileChange(acceptedFiles); | |
| }; | |
| handleClearField = event => { | |
| event.stopPropagation(); | |
| this.handleFieldChange(''); | |
| }; | |
| normalizeFileInputValue = values => { | |
| if (!values) { | |
| return ''; | |
| } | |
| return values.map(value => value.name); | |
| }; | |
| render() { | |
| const { | |
| label, | |
| InputProps, | |
| placeholder, | |
| accept, | |
| multiple, | |
| disabled, | |
| input: { value }, | |
| meta: { touched, error }, | |
| id, | |
| } = this.props; | |
| return ( | |
| <FileField | |
| accept={accept} | |
| multiple={multiple} | |
| disabled={disabled} | |
| label={label} | |
| id={id} | |
| placeholder={placeholder} | |
| onFileChange={this.handleFileChange} | |
| onClick={this.handleClick} | |
| onClearField={this.handleClearField} | |
| onFileDialogCancel={this.handleFileDialogCancel} | |
| error={Boolean(error) && Boolean(touched)} | |
| value={this.normalizeFileInputValue(value)} | |
| helperText={touched && error} | |
| InputProps={InputProps} | |
| /> | |
| ); | |
| } | |
| } | |
| FormFileField.propTypes = { | |
| input: PropTypes.object.isRequired, | |
| meta: PropTypes.object.isRequired, | |
| multiple: PropTypes.bool, | |
| disabled: PropTypes.bool, | |
| label: PropTypes.node, | |
| placeholder: PropTypes.node, | |
| id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | |
| InputProps: PropTypes.object, | |
| accept: PropTypes.string, | |
| onFileChange: PropTypes.func, | |
| }; | |
| FormFileField.defaultProps = { | |
| onFileChange: x => x, | |
| }; | |
| export default FormFileField; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment