Letting Website Visitors Upload Files

Enabling website visitors to upload files is a common use case in web development. Think of photos, avatars, receipts and other documents, or zipped directories you want to accept from users as part of whichever procedure.

In this tutorial, we are going to show you how to create a file upload widget, a form that allows visitors to upload any kind of files. After submitting the form, the data is posted to a request inspection service, hookbin.com, so you can check if the data was properly collected and sent. You can later create your own endpoint, e.g. using an AWS Lambda function, to persist the form data, process it, or forward it to wherever you need.

We will be using a well-recognized third-party package, react-filepond, for collecting the files from the visitor. The filepond library is not only highly configurable but can also be extended by plugins for processing and displaying images – in case that handling user-provided images on your website is what you have in mind.

Getting started

Like most of our hands-on tutorials, this one, too, is based on the Scrivito Example App, but you can, of course, also just use the code in any other website project based on Scrivito.

To begin, add the react-filepond and filepond packages to your project:

Copy
~$ npm install react-filepond filepond --save

Add the FileUploadWidget

Essentially, our FileUploadWidget renders a static form consisting of two input fields, one for the visitor’s email address and one for the files to upload, plus a submit button. To keep it simple, we omitted fields for the user’s name or a message, etc.

Provide the widget class definition

After successfully submitting the form, as well as in case of an error, we want to be able to render additional information, e.g. corresponding messages. For this, we’ll provide the widget with two widgetlist attributes, successContent and failureContent, enabling editors to freely decide on what to display in these two cases. Here’s the widget class definition:

src/Widgets/FileUploadWidget/FileUploadWidgetClass.js
Copy
import * as Scrivito from "scrivito";

const FileUploadWidget = Scrivito.provideWidgetClass("FileUploadWidget", {
  attributes: {
    successContent: "widgetlist",
    failureContent: "widgetlist",
  },
});

export default FileUploadWidget;

Provide the editing configuration

Strictly speaking, we don’t require an editing configuration for this widget because there’s nothing that needs to be specified in the widget’s properties dialog. We’ll render the contents of both successContent and failureContent into the form if editing mode is on, so that editors can easily add widgets to them.

However, to make it even more comfortable to use the FileUploadWidget, we will preset each of those two widgets with a TextWidget containing an appropriate message:

src/Widgets/FileUploadWidget/FileUploadWidgetEditingConfig.js
Copy
import * as Scrivito from "scrivito";
import contactFormWidgetIcon from "../../assets/images/contact_form_widget.svg";
import TextWidget from "../TextWidget/TextWidgetClass";

Scrivito.provideEditingConfig("FileUploadWidget", {
  title: "FileUpload",
  thumbnail: contactFormWidgetIcon,
  initialContent: {
    successContent: [
      new TextWidget({
        text: "<p><b>Thank you for sending in your files! Send more?</b></p>",
      }),
    ],
    failureContent: [
      new TextWidget({
        text: "<p><b>Form submission failed. Please try again!</b></p>",
      }),
    ],
  },
});

Provide the widget component

As indicated, our FileUploadWidget component renders a form. It includes a FilePond component for uploading files, and quite a bit of logic for activating and deactivating the submit button, as well as posting the form contents and handling the response.

SHOW WIDGET COMPONENT CODE
src/Widgets/FileUploadWidget/FileUploadWidgetComponent.js
Copy
import * as React from "react";
import * as Scrivito from "scrivito";

import { FilePond } from "react-filepond";
import "filepond/dist/filepond.min.css";

class FileUploadWidgetComponent extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      email: "",
      files: [],
      inSubmit: false,
      renderSuccess: false,
      renderFailure: false,
    };
  }

  onChange(e) {
    this.setState({
      email: e.target.value,
    });
  }

  onFormSubmit(e) {
    e.preventDefault();
    this.setState({ inSubmit: true });
    var formData = new FormData();
    formData.append("email", this.state.email);
    for (var i = 0; i < this.state.files.length; i++) {
      formData.append("files", this.state.files[i]);
    }

    fetch("https://hookb.in/your_path", {
      method: "POST",
      body: formData,
    })
      .then(response => response.json())
      .then(response => {
        this.pond.removeFiles();
        this.setState({
          renderSuccess: true,
          renderFailure: false,
          inSubmit: false,
        });
      })
      .catch(error => {
        this.setState({
          renderSuccess: false,
          renderFailure: true,
          inSubmit: false,
        });
      });
  }

  submitSuccess() {
    if (Scrivito.isInPlaceEditingActive() || this.state.renderSuccess)
      return (
        <div className="form-group">
          <Scrivito.ContentTag
            content={this.props.widget}
            attribute="successContent"
          />
        </div>
      );
  }
  submitFailure() {
    if (Scrivito.isInPlaceEditingActive() || this.state.renderFailure)
      return (
        <div className="form-group">
          <Scrivito.ContentTag
            content={this.props.widget}
            attribute="failureContent"
          />
        </div>
      );
  }

  render() {
    var submitDisabled =
      this.state.files.length===0 || !this.state.email || this.state.inSubmit;
    return (
      <div className="container">
        <form
          encType="multipart/form-data"
          onSubmit={e => this.onFormSubmit(e)}
        >
          <div className="form-group floating-label">
            <label htmlFor="email">E-mail address</label>
            <input
              className="form-control form-control-lg"
              placeholder="E-mail"
              type="email"
              name="email"
              id="email"
              required
              onChange={e => this.onChange(e)}
            />
          </div>
          <div className="form-group floating-label">
            <div className="form-control form-control-lg">
              <FilePond
                labelIdle='<span class="filepond--label-action">Browse files</span> or drag &amp; drop them here'
                ref={ref => (this.pond = ref)}
                allowMultiple={true}
                maxFiles={3}
                onupdatefiles={fileItems => {
                  this.setState({
                    files: fileItems.map(fileItem => fileItem.file),
                  });
                }}
              />
            </div>
          </div>
          {this.submitFailure()}
          <div className="form-group">
            <button
              className={`btn btn-block btn-primary ${
                submitDisabled ? " disabled" : ""
              }`}
              disabled={submitDisabled}
              type="submit"
            >
              Submit
            </button>
          </div>
          {this.submitSuccess()}
        </form>
      </div>
    );
  }
}

Scrivito.provideComponent("FileUploadWidget", FileUploadWidgetComponent);

FilePond features several callbacks, one of which is onupdatefiles we are using here to determine whether or not the file list is empty. Also, we use this callback to transfer the files to a state variable, files. If the file list is empty, the submit button is disabled, which is also the case if no email address has been entered or the submit action has been triggered.

Clicking the submit button causes the form data to be added to a FormData instance which is then posted to our test endpoint on hookbin.com, using fetch. On success and failure, the renderSuccess and renderFailure flags for displaying the corresponding widgetlist (successContent or, respectively, failureContent) are set. As indicated, in editing mode, both widget lists are rendered to avoid having to use the properties dialog to modify their content.

What’s next?

It would be worthwhile to invest some time into filepond. We found it to be extremely flexible, and, regarding file uploads, it will definitely improve your website visitors’ experience.

If you are looking for a guide about using the form contents productively, you may want to take a look at Using an AWS Lambda Function to Send an Email After Form Submission or Posting Form Content to a Slack Channel via an AWS Lambda Function.