diff --git a/app/static/js/ResourceLists/ResourceList.js b/app/static/js/ResourceLists/ResourceList.js
index 6bc6ac1f1fab50d3eaf9d6601bba0b370c18e53c..97e65d707f552f28679664d9c54be7fbf06fe56d 100644
--- a/app/static/js/ResourceLists/ResourceList.js
+++ b/app/static/js/ResourceLists/ResourceList.js
@@ -1,31 +1,37 @@
-class ResourceList {
+var ResourceLists = {};
+
+ResourceLists.autoInit = () => {
+  for (let propertyName in ResourceLists) {
+    let property = ResourceLists[propertyName];
+    // Call autoInit of all properties that are subclasses of `ResourceLists.BaseList`.
+    // This does not include `ResourceLists.BaseList` itself.
+    if (property.prototype instanceof ResourceLists.BaseList) {
+      // Check if the static `htmlClass` property is defined.
+      if (property.htmlClass === undefined) {return;}
+      // Gather all HTML elements that have the `this.htmlClass` class
+      // and do not have the `no-autoinit` class.
+      let listElements = document.querySelectorAll(`.${property.htmlClass}:not(.no-autoinit)`);
+      // Create an instance of this class for each display element.
+      for (let listElement of listElements) {new property(listElement);}
+    }
+  }
+};
+
+ResourceLists.defaultOptions = {
+  page: 5,
+  pagination: {
+    innerWindow: 2,
+    outerWindow: 2
+  }
+};
+
+ResourceLists.BaseList = class BaseList {
   /* A wrapper class for the list.js list.
    * This class is not meant to be used directly, instead it should be used as
    * a base class for concrete resource list implementations.
    */
 
-  static autoInit() {
-    CorpusList.autoInit();
-    CorpusFileList.autoInit();
-    JobList.autoInit();
-    JobInputList.autoInit();
-    JobResultList.autoInit();
-    SpaCyNLPPipelineModelList.autoInit();
-    TesseractOCRPipelineModelList.autoInit();
-    UserList.autoInit();
-    AdminUserList.autoInit();
-    CorpusFollowerList.autoInit();
-    CorpusTextInfoList.autoInit();
-    CorpusTokenList.autoInit();
-  }
-
-  static defaultOptions = {
-    page: 5,
-    pagination: {
-      innerWindow: 2,
-      outerWindow: 2
-    }
-  };
+  static htmlClass;
 
   constructor(listContainerElement, options = {}) {
     if ('items' in options) {
@@ -36,7 +42,7 @@ class ResourceList {
     }
     let _options = Utils.mergeObjectsDeep(
       {item: this.item, valueNames: this.valueNames},
-      ResourceList.defaultOptions,
+      ResourceLists.defaultOptions,
       options
     );
     this.listContainerElement = listContainerElement;
diff --git a/app/static/js/forms/base-form.js b/app/static/js/forms/base-form.js
new file mode 100644
index 0000000000000000000000000000000000000000..b856048500eebc803af849aebeed136099a43082
--- /dev/null
+++ b/app/static/js/forms/base-form.js
@@ -0,0 +1,138 @@
+Forms.BaseForm = class BaseForm {
+  static htmlClass;
+
+  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.HTMLToElement(
+      `
+        <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.HTMLToElement(
+          '<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);
+        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.HTMLToElement(
+              `<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;
+      }
+    }
+  }
+};
diff --git a/app/static/js/forms/index.js b/app/static/js/forms/index.js
index 0e7529f69636dce5a8b9a321725a95fde77d52c4..0290944846e0894efb2659f64aa3a5a6b4dcae08 100644
--- a/app/static/js/forms/index.js
+++ b/app/static/js/forms/index.js
@@ -16,142 +16,3 @@ Forms.autoInit = () => {
     }
   }
 };
-
-Forms.BaseForm = class BaseForm {
-  static htmlClass;
-
-  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.HTMLToElement(
-      `
-        <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.HTMLToElement(
-          '<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);
-        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.HTMLToElement(
-              `<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;
-      }
-    }
-  }
-};
diff --git a/app/static/js/resource-displays/index.js b/app/static/js/resource-displays/index.js
index 1f795c4405f4c249cdbd0189137a4d6fd7c2d578..4ec7e997d091af2213b2e0377cbb6f9a6f6bc7bc 100644
--- a/app/static/js/resource-displays/index.js
+++ b/app/static/js/resource-displays/index.js
@@ -16,50 +16,3 @@ ResourceDisplays.autoInit = () => {
     }
   }
 }
-
-ResourceDisplays.BaseDisplay = class BaseDisplay {
-  static htmlClass;
-
-  constructor(displayElement) {
-    this.displayElement = displayElement;
-    this.userId = this.displayElement.dataset.userId;
-    this.isInitialized = false;
-    if (this.userId) {
-      app.subscribeUser(this.userId)
-        .then((response) => {
-          app.socket.on('PATCH', (patch) => {
-            if (this.isInitialized) {this.onPatch(patch);}
-          });
-        });
-      app.getUser(this.userId)
-        .then((user) => {
-          this.init(user);
-          this.isInitialized = true;
-        });
-    }
-  }
-
-  init(user) {throw 'Not implemented';}
-
-  onPatch(patch) {throw 'Not implemented';}
-
-  setElement(element, value) {
-    switch (element.tagName) {
-      case 'INPUT': {
-        element.value = value;
-        M.updateTextFields();
-        break;
-      }
-      default: {
-        element.innerText = value;
-        break;
-      }
-    }
-  }
-
-  setElements(elements, value) {
-    for (let element of elements) {
-      this.setElement(element, value);
-    }
-  }
-};
diff --git a/app/static/js/resource-displays/resource-display.js b/app/static/js/resource-displays/resource-display.js
new file mode 100644
index 0000000000000000000000000000000000000000..81fcda5dabc0a005a571b68b12af664549bf16d9
--- /dev/null
+++ b/app/static/js/resource-displays/resource-display.js
@@ -0,0 +1,46 @@
+ResourceDisplays.BaseDisplay = class BaseDisplay {
+  static htmlClass;
+
+  constructor(displayElement) {
+    this.displayElement = displayElement;
+    this.userId = this.displayElement.dataset.userId;
+    this.isInitialized = false;
+    if (this.userId) {
+      app.subscribeUser(this.userId)
+        .then((response) => {
+          app.socket.on('PATCH', (patch) => {
+            if (this.isInitialized) {this.onPatch(patch);}
+          });
+        });
+      app.getUser(this.userId)
+        .then((user) => {
+          this.init(user);
+          this.isInitialized = true;
+        });
+    }
+  }
+
+  init(user) {throw 'Not implemented';}
+
+  onPatch(patch) {throw 'Not implemented';}
+
+  setElement(element, value) {
+    switch (element.tagName) {
+      case 'INPUT': {
+        element.value = value;
+        M.updateTextFields();
+        break;
+      }
+      default: {
+        element.innerText = value;
+        break;
+      }
+    }
+  }
+
+  setElements(elements, value) {
+    for (let element of elements) {
+      this.setElement(element, value);
+    }
+  }
+};
diff --git a/app/templates/_scripts.html.j2 b/app/templates/_scripts.html.j2
index 6213e3dfc36e63f6fd9f0847d2fb013f3ebb6b9b..6d3495c9429843f1d293209bdb10d194fc80d420 100644
--- a/app/templates/_scripts.html.j2
+++ b/app/templates/_scripts.html.j2
@@ -36,6 +36,7 @@
   filters='rjsmin',
   output='gen/Forms.%(version)s.js',
   'js/forms/index.js',
+  'js/forms/form.js'
   'js/forms/create-contribution-form.js',
   'js/forms/create-corpus-file-form.js',
   'js/forms/create-job-form.js'
@@ -47,6 +48,7 @@
   filters='rjsmin',
   output='gen/resource-displays.%(version)s.js',
   'js/resource-displays/index.js',
+  'js/resource-displays/base-display.js',
   'js/resource-displays/corpus-display.js',
   'js/resource-displays/job-display.js'
 %}