diff --git a/app/static/js/Forms/CreateCorpusFileForm.js b/app/static/js/Forms/CreateCorpusFileForm.js
new file mode 100644
index 0000000000000000000000000000000000000000..ae8dba3b1a317720408a09e12366d270cae54420
--- /dev/null
+++ b/app/static/js/Forms/CreateCorpusFileForm.js
@@ -0,0 +1,18 @@
+class CreateCorpusFileForm extends Form {
+  static autoInit() {
+    let createCorpusFileFormElements = document.querySelectorAll('.create-corpus-file-form');
+    for (let createCorpusFileFormElement of createCorpusFileFormElements) {
+      new CreateCorpusFileForm(createCorpusFileFormElement);
+    }
+  }
+
+  constructor(formElement) {
+    super(formElement);
+
+    this.addEventListener('requestLoad', (event) => {
+      if (event.target.status === 201) {
+        window.location.href = event.target.getResponseHeader('Location');
+      }
+    });
+  }
+}
diff --git a/app/static/js/Forms/CreateJobForm.js b/app/static/js/Forms/CreateJobForm.js
new file mode 100644
index 0000000000000000000000000000000000000000..6aa2d1b6eb2165d2306bf75d2206a1975dfa9c57
--- /dev/null
+++ b/app/static/js/Forms/CreateJobForm.js
@@ -0,0 +1,25 @@
+class CreateJobForm extends Form {
+  static autoInit() {
+    let createJobFormElements = document.querySelectorAll('.create-job-form');
+    for (let createJobFormElement of createJobFormElements) {
+      new CreateJobForm(createJobFormElement);
+    }
+  }
+
+  constructor(formElement) {
+    super(formElement);
+
+    let versionField = this.formElement.querySelector('#create-job-form-version');
+    versionField.addEventListener('change', (event) => {
+      let url = new URL(window.location.href);
+      url.search = `?version=${event.target.value}`;
+      window.location.href = url.toString();
+    });
+
+    this.addEventListener('requestLoad', (event) => {
+      if (event.target.status === 201) {
+        window.location.href = event.target.getResponseHeader('Location');
+      }
+    });
+  }
+}
diff --git a/app/static/js/Forms/Form.js b/app/static/js/Forms/Form.js
new file mode 100644
index 0000000000000000000000000000000000000000..9a21e98661a2e2ba8c64a7d9c846af4af25fd5a3
--- /dev/null
+++ b/app/static/js/Forms/Form.js
@@ -0,0 +1,141 @@
+class Form {
+  static autoInit() {
+    CreateCorpusFileForm.autoInit();
+    CreateJobForm.autoInit();
+  }
+
+  constructor(formElement) {
+    this.formElement = formElement;
+    this.eventListeners = {
+      'requestLoad': []
+    };
+    this.afterRequestListeners = [];
+
+    for (let selectElement of this.formElement.querySelectorAll('select')) {
+      selectElement.removeAttribute('required');
+    }
+
+    this.formElement.addEventListener('submit', (event) => {
+      event.preventDefault();
+      this.submit(event);
+    });
+  }
+
+  addEventListener(eventType, listener) {
+    if (eventType in this.eventListeners) {
+      this.eventListeners[eventType].push(listener);
+    } else {
+      throw `Unknown event type ${eventType}`;
+    }
+  }
+
+  submit(event) {
+    let request = new XMLHttpRequest();
+    let modalElement = Utils.elementFromString(
+      `
+        <div class="modal">
+          <div class="modal-content">
+            <h4><i class="material-icons left">file_upload</i>Submitting...</h4>
+            <div class="progress">
+              <div class="determinate" style="width: 0%"></div>
+            </div>
+          </div>
+          <div class="modal-footer">
+            <a class="action-button btn red waves-effect waves-light modal-close" data-action="cancel">Cancel</a>
+          </div>
+        </div>
+      `
+    );
+    document.querySelector('#modals').appendChild(modalElement);
+    let modal = M.Modal.init(
+      modalElement,
+      {
+        dismissible: false,
+        onCloseEnd: () => {
+          modal.destroy();
+          modalElement.remove();
+        }
+      }
+    );
+    modal.open();
+
+    // Remove all previous helper text elements that indicate errors
+    let errorHelperTextElements = this.formElement
+      .querySelectorAll('.helper-text[data-helper-text-type="error"]');
+    for (let errorHelperTextElement of errorHelperTextElements) {
+      errorHelperTextElement.remove();
+    }
+
+    // Check if select elements are filled out properly
+    for (let selectElement of this.formElement.querySelectorAll('select')) {
+      if (selectElement.value === '') {
+        let inputFieldElement = selectElement.closest('.input-field');
+        let errorHelperTextElement = Utils.elementFromString(
+          '<span class="helper-text error-color-text" data-helper-text-type="error">Please select an option.</span>'
+        );
+        inputFieldElement.appendChild(errorHelperTextElement);
+        inputFieldElement.querySelector('.select-dropdown').classList.add('invalid');
+        modal.close();
+        return;
+      }
+    }
+
+    // Setup abort handling
+    let cancelElement = modalElement.querySelector('.action-button[data-action="cancel"]');
+    cancelElement.addEventListener('click', (event) => {request.abort();});
+
+    // Setup load handling (after the request completed)
+    request.addEventListener('load', (event) => {
+      for (let listener of this.eventListeners['requestLoad']) {
+        listener(event);
+      }
+      if (request.status === 400) {
+        let responseJson = JSON.parse(request.responseText);
+        console.log(responseJson);
+        for (let [inputName, inputErrors] of Object.entries(responseJson.errors)) {
+          let inputFieldElement = this.formElement
+            .querySelector(`input[name$="${inputName}"], select[name$="${inputName}"]`)
+            .closest('.input-field');
+          for (let inputError of inputErrors) {
+            let errorHelperTextElement = Utils.elementFromString(
+              `<span class="helper-text error-color-text" data-helper-type="error">${inputError}</span>`
+            );
+            inputFieldElement.appendChild(errorHelperTextElement);
+          }
+        }
+      }
+      if (request.status === 500) {
+        app.flash('Internal Server Error', 'error');
+      }
+      modal.close();
+    });
+
+    // Setup progress handling
+    let progressBarElement = modalElement.querySelector('.progress > .determinate');
+    request.upload.addEventListener('progress', (event) => {
+      let progress = Math.floor(100 * event.loaded / event.total);
+      progressBarElement.style.width = `${progress}%`;
+    });
+
+    request.open(this.formElement.method, this.formElement.action);
+    request.setRequestHeader('Accept', 'application/json');
+    let formData = new FormData(this.formElement);
+    switch (this.formElement.enctype) {
+      case 'application/x-www-form-urlencoded':
+        let urlSearchParams = new URLSearchParams(formData);
+        request.send(urlSearchParams);
+        break;
+      case 'multipart/form-data': {
+        request.send(formData);
+        break;
+      }
+      case 'text/plain': {
+        throw 'enctype "text/plain" is not supported';
+        break;
+      }
+      default: {
+        break;
+      }
+    }
+  }
+}