Skip to content

Instantly share code, notes, and snippets.

@josephdburdick
Created June 17, 2021 17:41
Show Gist options
  • Save josephdburdick/d044dab71b2830e01a92481055ee8f18 to your computer and use it in GitHub Desktop.
Save josephdburdick/d044dab71b2830e01a92481055ee8f18 to your computer and use it in GitHub Desktop.
Field Input Field example
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { FormGroup, Label, FormFeedback } from 'reactstrap';
import { RequiredMark } from './';
import { constants, log } from '@src/lib';
class FileInputField extends Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.renderErrorMessages = this.renderErrorMessages.bind(this);
this.checkFiles = this.checkFiles.bind(this);
this.handleReset = this.handleReset.bind(this);
this.state = {
color: '',
errors: [],
};
}
// TODO: handle form reset error
handleReset({ reason = false }) {
const errors = [];
if (reason) {
errors.push(reason.trim());
}
this.props.handleFileInputValueChange(false);
return errors;
}
checkFiles = fileList => {
const errors = [];
const {
accept,
fileSizeMax,
messages,
multiple,
required,
handleFileInputValueChange,
} = this.props;
const { length } = fileList;
const files = [];
for (let i = 0; i < length; i++) {
files.push(fileList[i]);
}
// Testing input as a whole
if (required) {
if (files[0] == null) {
return errors.push(messages.FILE_NOT_SELECTED);
}
}
// If multiple files are attempted on a single file input
if (!multiple && files.length > 1) {
return errors.push(messages.FILE_COUNT_EXCEEDED);
}
// Testing each file inside input
files.map(file => {
// File size
if (file.size > parseInt(fileSizeMax)) {
return errors.push(`
${file.name}:
${messages.FILE_SIZE_EXCEEDED}`);
}
// File type
if (accept.trim().length) {
const allowedTypes = accept.replace(/\s|\.+/g, '').split(',');
const splitName = file.name.split('.');
const fileExtension = splitName[splitName.length - 1].toLowerCase();
const isAllowed = allowedTypes.indexOf(fileExtension) > -1;
if (!isAllowed) {
return errors.push(
`
${file.name}:
${messages.FILE_WRONG_TYPE}`.trim()
);
}
}
});
/*
Automate the testing with a function
return a value if false
*/
this.setState({
color: errors.length ? 'danger' : 'success',
errors,
});
handleFileInputValueChange(errors.length ? false : this.props.value);
return errors;
};
handleChange = async event => {
event.persist();
const { messages, multiple } = this.props;
const errors = [...this.state.errors];
const field = event.target;
let fieldErrors = fieldErrors || false;
if (event.type === 'dragenter') {
this.props.handleFileInputValueChange(
`Soltar ${multiple ? 'uno o más archivos' : 'el archivo'} aquí`
);
}
if (event.type === 'dragleave') {
this.setState({ color: errors.length ? 'danger' : '' });
this.props.handleFileInputValueChange(false);
}
if (event.type === 'drop') {
this.checkFiles(event.dataTransfer.files);
}
// proceed if no errors and a file is present
if (field.files.length && !errors.length) {
fieldErrors = await this.checkFiles(field.files);
if (fieldErrors.length) errors.push(...fieldErrors);
const { files } = field;
const [file] = files;
const reader = new FileReader();
const progressEvent = event => {
if (!errors.length && event.type === 'loadstart') {
reader.abort();
this.checkFiles(field);
this.setState({ color: 'warning' });
this.props.handleFileInputValueChange(messages.FILE_LOADING);
}
if (!errors.length && event.type === 'progress') {
this.setState({ color: 'success' });
this.props.handleFileInputValueChange(messages.FILE_LOADING);
}
if (event.type === 'error') {
this.setState({ color: 'danger' });
this.props.handleFileInputValueChange(messages.FILE_ERROR);
reader.abort();
}
if (event.type === 'abort') {
// reader.abort();
this.setState({ color: 'warning' });
this.props.handleFileInputValueChange(messages.FILE_ABORT);
}
if (!this.state.errors.length && event.type === 'loadend') {
this.props.handleFileInputValueChange('Datos cargados');
const fileInputValue = files.length > 1
? `${files.length} ${messages.FILES_SELECTED}`
: file.name;
setTimeout(() => {
this.setState({ color: errors.length ? 'danger' : 'success' });
this.props.handleFileInputValueChange(fileInputValue);
}, 1000);
}
};
reader.onloadstart = progressEvent;
reader.onprogress = progressEvent;
reader.onload = progressEvent;
reader.onloadend = progressEvent;
reader.onabort = progressEvent;
reader.onerror = progressEvent;
fieldErrors = this.checkFiles(field.files);
if (fieldErrors.length) {
errors.push(...fieldErrors);
field.value = null;
reader.abort();
return false;
}
reader.readAsDataURL(field.files[0]);
log('form file input event: ', event);
return this.props.onChange(event);
}
fieldErrors = this.checkFiles(field);
// return errors from testing files individually
if (fieldErrors.length) {
this.setState({ color: 'danger' });
errors.push(...fieldErrors);
field.value = null;
return false;
}
};
renderErrorMessages() {
const { errors } = this.state;
if (errors.length > 0) {
const errMsgString = errors.map((msg, i) => (
<span key={`msg-${i}`}>{msg}<br /></span>
));
return <FormFeedback className="small">{errMsgString}</FormFeedback>;
}
}
render() {
const {
formLabel,
elementName,
index,
required,
value,
placeholder,
accept,
multiple,
} = this.props;
const { color } = this.state;
const generateIdFromString = str => str.replace(/ /g, '-');
const generatedId = generateIdFromString(
formLabel ? formLabel : elementName
);
return (
<FormGroup
onReset={e => this.handleReset(e)}
color={color}
key={'form-group' + index}
>
{formLabel
? <Label
key={'label' + index}
for={`input-${generatedId}-${index}`}
className="small text-uppercase text-muted"
>
{formLabel}
<RequiredMark required={required} />
</Label>
: null}
<div className="pos-r file-input">
<div className="input-group">
<span className="input-group-addon" id="icon-upload">
<i className="fa fa-upload" aria-hidden="true" />
</span>
<input
className="form-control"
type="text"
value={value ? value : placeholder}
aria-describedby="icon-upload"
readOnly
/>
</div>
<input
type="file"
id={`input-${generatedId}-${this.props.index}`}
key={`input-${generatedId}-${this.props.index}`}
name={elementName}
accept={accept}
placeholder={placeholder}
className="form-control"
onChange={this.handleChange}
onDragEnter={this.handleChange}
onDrop={this.handleChange}
multiple={multiple}
required={required}
/>
</div>
{this.renderErrorMessages()}
</FormGroup>
);
}
}
FileInputField.propTypes = {
index: PropTypes.number,
formLabel: PropTypes.string,
placeholder: PropTypes.string,
elementName: PropTypes.string,
required: PropTypes.bool,
multiple: PropTypes.bool,
onChange: PropTypes.func,
fileSizeMax: PropTypes.string,
accept: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
messages: PropTypes.objectOf(PropTypes.string),
handleFileInputValueChange: PropTypes.func,
color: PropTypes.string,
errors: PropTypes.array,
};
FileInputField.defaultProps = {
required: true,
multiple: false,
accept: '.doc, .docx, .pdf',
placeholder: 'Subir documento...',
value: '',
// fileSizeMax: 10485760, // 10MB, set in .env
messages: {
FILE_ABORT: constants('File was aborted', 'feedback'),
FILE_COUNT_EXCEEDED: constants('Multiple files not permitted', 'feedback'),
FILE_ERROR: constants('There is a problem with the file', 'feedback'),
FILE_LOADING: constants('Loading', 'feedback'),
FILE_NOT_SELECTED: constants('No file selected', 'feedback'),
FILE_SIZE_EXCEEDED: constants('File size exceeded', 'feedback'),
FILES_SELECTED: constants('files selected', 'feedback'),
FILE_WRONG_TYPE: constants('Type of file incorrect', 'feedback'),
},
};
export default FileInputField;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment