Skip to content

Instantly share code, notes, and snippets.

@SirMoustache
Last active May 3, 2019 09:05
Show Gist options
  • Select an option

  • Save SirMoustache/55571c699980d6ff7e789b822452bef7 to your computer and use it in GitHub Desktop.

Select an option

Save SirMoustache/55571c699980d6ff7e789b822452bef7 to your computer and use it in GitHub Desktop.
React File input using: redux-form, @material-ui and react-dropzone
/**
* 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);
/**
* 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;
/**
* 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;
/**
* 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;
/**
* 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