Search code examples
reactjsreact-final-formfinal-formfilepond

How to use FilePond with react-final-form?


I'm trying to get FilePond to work with react-final-form. I'm not sure how to wrap the FilePond component in a react-final-form Field component adapter.

For example, for react-phone-number-input I did it like so:

// Before React.Component class declaration.
const PhoneAdapter = ({ input }) => (
    <PhoneInput
    placeholder="Phone Number"
    value={input.value}
    tabIndex="3"
    id="contact-form-id-phone"
    onChange={value => input.onChange(value)}
  />
);

// Inside React.Component class declaration.
<Field name="phone" component={PhoneAdapter} />

For FilePond, I tried this:

// Before React.Component class declaration.
const FileAdapter = ({ input }) => (
  <FilePond
    // files={files}
    allowMultiple
    // onupdatefiles={setFiles}
    server="/api/upload"
    labelIdle="Drag &amp; Drop your files or <span class=&quot;filepond--label-action&quot;>Browse</span>."
    />
);

// Inside React.Component class declaration.
<Field name="files" component={FileAdapter} />

The examples I learned from where these:

I want to be able to get the name of the file for the parent react-final-form form (filepond component is one of many other fields). As in, I want it to capture the state. Other than that, I'm not sure how to integrate the two.


Solution

  • I ended up developing it like so. Unfortunately, the other properties were not responding properly, so I used the server property to develop the solution.

    const FileAdapter = ({ input: {value, onChange}, meta: { submitError, error } }) => (
      <>
      <FilePond
        acceptedFileTypes={['notRejected'] /* Give fake accepted file type to be able to validate through returning it in fileValidateTypeDetectType once validated it is not one of the rejected file types. */} 
        fileValidateTypeDetectType={(source, type) => new Promise((resolve, reject) => {
          let rejectedExtensions = ['ade', 'adp', 'apk', 'bat', 'chm', 'cmd', 'com', 'cpl', 'dll', 'dmg', 'exe', 'hta', 'ins', 'isp', 'jar', 'js', 'jse', 'lib', 'lnk', 'mde', 'msc', 'msi', 'msp', 'mst', 'nsh', 'pif', 'scr', 'sct', 'shb', 'sys', 'vb', 'vbe', 'vbs', 'vxd', 'wsc', 'wsf', 'wsh', 'cab'];
          // If the file extension is not in our rejected list.
          if (!rejectedExtensions.some(ext => source.name.endsWith("." + ext))) {
            resolve('notRejected');
          }
          // Otherwise it is rejected.
          resolve(type);
      })}
        name="file"
        allowMultiple
        maxParallelUploads="1"
        id="contact-form-id-file"
        maxTotalFileSize="5MB"
        onremovefile={() => {onChange(value.filter(file => !file.hasOwnProperty('error') ).map(file => file))} /* Remove error messages of rejected file types once you remove on of the erroneous files. */}
        labelIdle="Drag &amp; Drop your files or <span tabindex=&quot;4&quot; class=&quot;filepond--custom-label-action&quot;>Browse</span>."
        server={{
          process: {
            withCredentials: true,
            url: '/api/contact/upload',
            headers: {
              'X-XSRF-TOKEN': axios.defaults.headers.post['X-XSRF-TOKEN'] // Give CSRF token to process header to validate source.
            },
            ondata: (formData) => { // Before sending the files to the server, append size of the current file and the order of the file; order to delete all previous files if the current file is the first, even though the session ID didn't change, and size to validate against total size of files uploaded.
              formData.append('size', formData.getAll('file')[1].size);
              formData.append('quantity', value.length);
              return formData;
            },
            onload: (response) => { // Once response is received, pushed new value to Final Form value variable, and populate through the onChange handler. 
              value.push(JSON.parse(response));
              onChange( value.map(file => file));
              return JSON.parse(response).filename;
            },
            onerror: (response) => { // If error transpires, add error to value with error message.
              value.push({"error": response})
              onChange( value.map(file => file) );
              return response;
            }
          },
          revert: {
            withCredentials: true,
            url: '/api/contact/revert',
            headers: {
              'X-XSRF-TOKEN': axios.defaults.headers.post['X-XSRF-TOKEN'] // Give CSRF token to process header to validate source.
            },
            onload: (response) => { // Update value through onChange once DELETE request sent.
              onChange(value.filter(file => file.filename !== response).map(file => file));
              return response;
            }
          }
        }}
      />
      {(error || submitError) && (<div className="message message--error">{submitError || error}</div>)}
      </>
    )
    

    As for the Final Form <Form/> component, this is how I ended incorporating the adapter like so:

    <Form
    onSubmit={this.onSubmit}
    subscription={{ submitting: true, pristine: true, submitSucceeded: true, submitError: true }} // Optimize performance through subscription.
    initialValues={{ files: [] }} // Declare files as array as doing it in FilePond's onInit fires off more than once.
    render={({ submitError, handleSubmit, submitting, pristine, submitSucceeded }) => (
        <form className="form-contact text-justify" onSubmit={handleSubmit}>
        <div className="form-contact__fieldset form-contact__fieldset--file fieldset">
            <label htmlFor="contact-form-id-file">File <small className="optional-text">(optional)</small></label>
            <Field name="files" component={FileAdapter} />
        </div>
        {submitError ? <div className="message message--error">{submitError}</div> : submitSucceeded && <div className="message message--success">Thank you for contacting us. We will soon be in touch.</div>}
        <button type="submit" disabled={submitting || pristine || submitSucceeded}>
            {submitting ? <Spinner className="spinner--button form-contact__spinner" fadeIn='quarter' name="line-scale-pulse-out-rapid" /> : submitSucceeded ? 'Message Sent' : 'Send Message'}
        </button>
        </form>
    )}
    />
    

    Hope this helps others.