Skip to content

Commit

Permalink
feat: add {validator} for custom validation
Browse files Browse the repository at this point in the history
  • Loading branch information
VolodymyrBaydalka committed Feb 3, 2021
1 parent 5a4ae93 commit ebe2130
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 7 deletions.
68 changes: 68 additions & 0 deletions examples/validator/README.md
@@ -0,0 +1,68 @@
By providing `validator` prop you can specify custom validation for files.

The value must be a function that accepts File object and returns null if file should be accepted or error object/array of error objects if file should me rejected.

```jsx harmony
import React from 'react';
import {useDropzone} from 'react-dropzone';

const maxLength = 20;

function nameLengthValidator(file) {
if (file.name.length > maxLength) {
return {
code: "name-too-large",
message: `Name is larger than ${maxLength} characters`
};
}

return null
}

function CustomValidation(props) {
const {
acceptedFiles,
fileRejections,
getRootProps,
getInputProps
} = useDropzone({
validator: nameLengthValidator
});

const acceptedFileItems = acceptedFiles.map(file => (
<li key={file.path}>
{file.path} - {file.size} bytes
</li>
));

const fileRejectionItems = fileRejections.map(({ file, errors }) => (
<li key={file.path}>
{file.path} - {file.size} bytes
<ul>
{errors.map(e => (
<li key={e.code}>{e.message}</li>
))}
</ul>
</li>
));

return (
<section className="container">
<div {...getRootProps({ className: 'dropzone' })}>
<input {...getInputProps()} />
<p>Drag 'n' drop some files here, or click to select files</p>
<em>(Only files with name less than 20 characters will be accepted)</em>
</div>
<aside>
<h4>Accepted files</h4>
<ul>{acceptedFileItems}</ul>
<h4>Rejected files</h4>
<ul>{fileRejectionItems}</ul>
</aside>
</section>
);
}

<CustomValidation />
```

28 changes: 22 additions & 6 deletions src/index.js
Expand Up @@ -60,7 +60,8 @@ const defaultProps = {
noClick: false,
noKeyboard: false,
noDrag: false,
noDragEventsBubbling: false
noDragEventsBubbling: false,
validator: null
}

Dropzone.defaultProps = defaultProps
Expand Down Expand Up @@ -226,7 +227,14 @@ Dropzone.propTypes = {
* @param {FileRejection[]} fileRejections
* @param {(DragEvent|Event)} event
*/
onDropRejected: PropTypes.func
onDropRejected: PropTypes.func,

/**
* Custom validation function
* @param {File} file
* @returns {FileError|FileError[]}
*/
validator: PropTypes.func
}

export default Dropzone
Expand Down Expand Up @@ -398,7 +406,8 @@ export function useDropzone(options = {}) {
noClick,
noKeyboard,
noDrag,
noDragEventsBubbling
noDragEventsBubbling,
validator
} = {
...defaultProps,
...options
Expand Down Expand Up @@ -615,11 +624,18 @@ export function useDropzone(options = {}) {
files.forEach(file => {
const [accepted, acceptError] = fileAccepted(file, accept)
const [sizeMatch, sizeError] = fileMatchSize(file, minSize, maxSize)
if (accepted && sizeMatch) {
const customErrors = validator ? validator(file) : null;

if (accepted && sizeMatch && !customErrors) {
acceptedFiles.push(file)
} else {
const errors = [acceptError, sizeError].filter(e => e)
fileRejections.push({ file, errors })
let errors = [acceptError, sizeError];

if (customErrors) {
errors = errors.concat(customErrors);
}

fileRejections.push({ file, errors: errors.filter(e => e) })
}
})

Expand Down
41 changes: 41 additions & 0 deletions src/index.spec.js
Expand Up @@ -2719,6 +2719,47 @@ describe('useDropzone() hook', () => {
expect(fn).not.toThrow()
})
})

describe('validator', () => {
it('rejects with custom error', async () => {
const validator = file => {
if (/dogs/i.test(file.name))
return { code: 'dogs-not-allowed', message: 'Dogs not allowed' };

return null;
}

const onDropSpy = jest.fn()

const ui = (
<Dropzone validator={validator} onDrop={onDropSpy} multiple={true}>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
)

const { container, rerender } = render(ui)
const dropzone = container.querySelector('div')

fireDrop(dropzone, createDtWithFiles(images))
await flushPromises(rerender, ui)

expect(onDropSpy).toHaveBeenCalledWith([images[0]], [
{
file: images[1],
errors: [
{
code: 'dogs-not-allowed',
message: 'Dogs not allowed',
}
]
}
], expect.anything())
})
})
})

async function flushPromises(rerender, ui) {
Expand Down
4 changes: 4 additions & 0 deletions styleguide.config.js
Expand Up @@ -59,6 +59,10 @@ module.exports = {
name: 'Accepting specific number of files',
content: 'examples/maxFiles/README.md'
},
{
name: 'Custom validation',
content: 'examples/validator/README.md'
},
{
name: 'Opening File Dialog Programmatically',
content: 'examples/file-dialog/README.md'
Expand Down
3 changes: 2 additions & 1 deletion typings/react-dropzone.d.ts
Expand Up @@ -10,7 +10,7 @@ export interface DropzoneProps extends DropzoneOptions {

export interface FileError {
message: string;
code: "file-too-large" | "file-too-small"|"too-many-files"|"file-invalid-type";
code: "file-too-large" | "file-too-small" | "too-many-files" | "file-invalid-type" | string;
}

export interface FileRejection {
Expand All @@ -34,6 +34,7 @@ export type DropzoneOptions = Pick<React.HTMLProps<HTMLElement>, PropTypes> & {
onDropRejected?: (fileRejections: FileRejection[], event: DropEvent) => void;
getFilesFromEvent?: (event: DropEvent) => Promise<Array<File | DataTransferItem>>;
onFileDialogCancel?: () => void;
validator?: <T extends File>(file: T) => FileError | FileError[];
};

export type DropEvent = React.DragEvent<HTMLElement> | React.ChangeEvent<HTMLInputElement> | DragEvent | Event;
Expand Down

0 comments on commit ebe2130

Please sign in to comment.