diff --git a/examples/validator/README.md b/examples/validator/README.md new file mode 100644 index 00000000..82845a0e --- /dev/null +++ b/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 => ( +
  • + {file.path} - {file.size} bytes +
  • + )); + + const fileRejectionItems = fileRejections.map(({ file, errors }) => ( +
  • + {file.path} - {file.size} bytes + +
  • + )); + + return ( +
    +
    + +

    Drag 'n' drop some files here, or click to select files

    + (Only files with name less than 20 characters will be accepted) +
    + +
    + ); +} + + +``` + diff --git a/src/index.js b/src/index.js index 9d8ee83e..f6308703 100755 --- a/src/index.js +++ b/src/index.js @@ -60,7 +60,8 @@ const defaultProps = { noClick: false, noKeyboard: false, noDrag: false, - noDragEventsBubbling: false + noDragEventsBubbling: false, + validator: null } Dropzone.defaultProps = defaultProps @@ -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 @@ -398,7 +406,8 @@ export function useDropzone(options = {}) { noClick, noKeyboard, noDrag, - noDragEventsBubbling + noDragEventsBubbling, + validator } = { ...defaultProps, ...options @@ -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) }) } }) diff --git a/src/index.spec.js b/src/index.spec.js index bff9bc74..74e7ded4 100644 --- a/src/index.spec.js +++ b/src/index.spec.js @@ -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 = ( + + {({ getRootProps, getInputProps }) => ( +
    + +
    + )} +
    + ) + + 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) { diff --git a/styleguide.config.js b/styleguide.config.js index 96d54a4e..0f37ef2d 100644 --- a/styleguide.config.js +++ b/styleguide.config.js @@ -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' diff --git a/typings/react-dropzone.d.ts b/typings/react-dropzone.d.ts index f038ce7a..4284f353 100644 --- a/typings/react-dropzone.d.ts +++ b/typings/react-dropzone.d.ts @@ -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 { @@ -34,6 +34,7 @@ export type DropzoneOptions = Pick, PropTypes> & { onDropRejected?: (fileRejections: FileRejection[], event: DropEvent) => void; getFilesFromEvent?: (event: DropEvent) => Promise>; onFileDialogCancel?: () => void; + validator?: (file: T) => FileError | FileError[]; }; export type DropEvent = React.DragEvent | React.ChangeEvent | DragEvent | Event;