FileInput

The file input can be used to drag & drop or select files to upload. Unlike the basic HTML file input, FileInput can be either controlled or uncontrolled.

Try it out

Basic usage

FileInput behaves much like a standard HTML file input, taking the same props for the most part. The main differences are in the accept prop, which is an array instead of a comma-delimited list, and the value prop, which the standard input doesn’t have because it is always uncontrolled.

Additionally, in events files are accessed through target.value instead of target.files. Changes to the file list are always reflected via the onChange event, regardless of whether they were added using the normal file dialog, by drag-and-drop, or removed using the file list’s delete button.

Once files have been selected they are displayed in a list, styled according to their status and with any file-specific error messages displayed. Each file in the list also has a delete button that can be used to remove it from the value.

Controlled Input: value prop & event.target.value

In order to make working with files easier, the value prop and change/focus/blur event target values are an array of the following type:

interface FileObject {
  /* A global Javascript File object. */
  file: File

  /* Defaults to 'unloaded', unless the file violates the `accept` constraint. */
  status: 'unloaded' | 'loading' | 'loaded' | 'error'

  /* File contents, if the file has been loaded. */
  contents?: unknown

  /* Any Error object raised during loading, or violation of the `accept` constraint. */
  error?: {
    name: string
    message: string
  }

  /* A custom error message (possibly localized) that will be displayed to the user instead of `error.message`. */
  errorMsg?: React.ReactNode

  /* Loads the file and sets the status, contents, and error properties as appropriate. */
  load: (l: (f: File) => Promise<unknown>) => Promise<unknown>
}

value is always an array, though if multiple is not true, it should never have more than one object in it. For smoother interop with certain form libraries and other input types, passing value as the empty string is also considered equivalent to an empty array.

If you get File objects from another source and want to pass them into a FileInput, you can use the FileInput.toFileObj(file: File) function to create compatible FileObject wrappers.

Loading Files

The FileObject.load() method can be used to asynchronously load the contents of a file. It takes a single optional argument, a function that does most of the work of actually loading the file: this load function should accept a File object and return a Promise that resolves to the file contents, or rejects to an Error describing why the file could not be loaded. The default load function utilizes a FileReader to read the file contents as a base64-encoded string.

Although the passed-in load function does most of the work, the load() method is still recommended for its extra functionality:

  • It only loads the file if the current status is unloaded; otherwise it returns a Promise with the results of the previous load attempt.
  • It automatically sets the status, contents, and error properties of the FileObject it’s called on, depending on whether the Promise resolves or rejects. (It still returns the Promise in case you need to do additional or custom handling.)
  • When setting the status, it updates the visual styles in the file list without needing to rerender the whole FileInput component or pass in a new value prop.

Note that because file loading is typically async, care should be taken to avoid race conditions between load events and change events. Here’s an example where a form is submitted with only the file contents:

import React, { useCallback, useState } from 'react'
import { FileInput, Button } from '@repay/cactus-web'

const FileForm = () => {
  const [state, setState] = useState({})
  const [files, setFiles] = useState([])

  const handleChange = useCallback(
    (event) => {
      const { name, value } = event.target
      setFiles(value)
      const promises = value.map(file => file.load().catch(console.error))
      Promise.all(promises).then(files => {
        setState(s => ({ ...s, [name]: files }))
      })
    },
    [],
  )

  const handleSubmit = useCallback(
    event => {
      event.preventDefault()
      sendDataToAPI(state)
    },
    [state]
  )

  return (
    <form onSubmit={handleSubmit}>
      <FileInput
        name="file-input"
        value={files}
        accept={['.txt', '.doc']}
        onChange={handleChange}
        multiple
      />
      <Button type="submit" variant="action">
        Submit
      </Button>
    </form>
  )
}

File Errors

File errors, either violations of the accept prop or errors occurring during a load attempt, are displayed in the file list. By default this means the FileObject.error.message, but you may feel some errors are too technical for the average user; or you may wish to localize error messages. In any case, you can override the error message by setting FileObject.errorMsg, which will then be displayed to the user instead.

For example, you might write something like this:

const onChange = (e) => {
  setFiles(e.target.value)
  const loading = e.target.value.map(f => f.load().then(
    () => f,
    (e) => {
      if (e.name === 'SecurityError')
        f.errorMsg = "You don't have permission to be aboard there, mate."
      return f
    },
  ))
  // Compare to avoid race conditions, in case the files have changed since load started.
  Promise.all(loading).then((new) => setFiles((old) => old === new ? [...new] : old))
}

Localization/Labels

FileInput has several props for controlling text content & labels on the component. They can be used for simple customization, or localization.

  • buttonText (“Select Files…“) - Used as the contents of the button that opens the file select dialog.
  • prompt (“Drag files here or”) - Text displayed just above the file select button, but only when the file list is empty.
  • labels.delete (“Delete File”) - Used as the aria-label for the X icons which delete individual files.
  • labels.[status] - Used as the aria-label on list items in the file list, depending on the file’s current status:
    • labels.unloaded (“Not Loaded”)
    • labels.loading (“Loading”)
    • labels.loaded (“Successful”)
    • labels.error (“Error Loading File”) - In addition to this label, the FileObject.errorMsg is linked using aria-describedby.

The labels must all be strings because they’re used directly in aria-label attributes, but buttonText, prompt, and FileObject.errorMsg can all be React nodes (e.g. an I18nText component from @repay/cactus-i18n).

Properties

Cactus Props

NameRequiredDefault ValueTypeDescription
acceptNstring[] | undefined
buttonTextNReactNode
labelsNFileInfoLabels | undefined
nameYstring
onBlurNFocusEventHandler<Target> | undefined
onChangeNChangeEventHandler<Target> | undefined
onFocusNFocusEventHandler<Target> | undefined
promptNReactNode
statusN"success" | "warning" | "error" | null | undefined
valueN"" | FileObject[] | undefined

Styling Props

NameRequiredTypeDescription
mNResponsiveValue<string | number | symbol, Required<Theme<TLengthStyledSystem>>> | undefinedMargin on top, left, bottom and right
marginNResponsiveValue<string | number | symbol, Required<Theme<TLengthStyledSystem>>> | undefinedMargin on top, left, bottom and right
marginBottomNResponsiveValue<string | number | symbol, Required<Theme<TLengthStyledSystem>>> | undefinedMargin on bottom
marginLeftNResponsiveValue<string | number | symbol, Required<Theme<TLengthStyledSystem>>> | undefinedMargin on left
marginRightNResponsiveValue<string | number | symbol, Required<Theme<TLengthStyledSystem>>> | undefinedMargin on right
marginTopNResponsiveValue<string | number | symbol, Required<Theme<TLengthStyledSystem>>> | undefinedMargin on top
marginXNResponsiveValue<string | number | symbol, Required<Theme<TLengthStyledSystem>>> | undefinedMargin on left and right
marginYNResponsiveValue<string | number | symbol, Required<Theme<TLengthStyledSystem>>> | undefinedMargin on top and bottom
maxWidthNResponsiveValue<MaxWidth<TLengthStyledSystem>, Required<Theme<TLengthStyledSystem>>> | undefinedThe max-width CSS property sets the maximum width of an element. It prevents the used value of the width property from becoming larger than the value specified by max-width. [MDN reference](https://developer.mozilla.org/en-US/docs/Web/CSS/max-width)
mbNResponsiveValue<string | number | symbol, Required<Theme<TLengthStyledSystem>>> | undefinedMargin on bottom
mlNResponsiveValue<string | number | symbol, Required<Theme<TLengthStyledSystem>>> | undefinedMargin on left
mrNResponsiveValue<string | number | symbol, Required<Theme<TLengthStyledSystem>>> | undefinedMargin on right
mtNResponsiveValue<string | number | symbol, Required<Theme<TLengthStyledSystem>>> | undefinedMargin on top
mxNResponsiveValue<string | number | symbol, Required<Theme<TLengthStyledSystem>>> | undefinedMargin on left and right
myNResponsiveValue<string | number | symbol, Required<Theme<TLengthStyledSystem>>> | undefinedMargin on top and bottom
widthNResponsiveValue<Width<TLengthStyledSystem>, Required<Theme<TLengthStyledSystem>>> | undefinedThe width utility parses a component's `width` prop and converts it into a CSS width declaration. - Numbers from 0-1 are converted to percentage widths. - Numbers greater than 1 are converted to pixel values. - String values are passed as raw CSS values. - And arrays are converted to responsive width styles.