diff --git a/core/misc/form.js b/core/misc/form.js
deleted file mode 100644
index 2fae940..0000000
--- a/core/misc/form.js
+++ /dev/null
@@ -1,139 +0,0 @@
-(function ($) {
-
-"use strict";
-
-/**
- * Retrieves the summary for the first element.
- */
-$.fn.drupalGetSummary = function () {
-  var callback = this.data('summaryCallback');
-  return (this[0] && callback) ? $.trim(callback(this[0])) : '';
-};
-
-/**
- * Sets the summary for all matched elements.
- *
- * @param callback
- *   Either a function that will be called each time the summary is
- *   retrieved or a string (which is returned each time).
- */
-$.fn.drupalSetSummary = function (callback) {
-  var self = this;
-
-  // To facilitate things, the callback should always be a function. If it's
-  // not, we wrap it into an anonymous function which just returns the value.
-  if (typeof callback !== 'function') {
-    var val = callback;
-    callback = function () { return val; };
-  }
-
-  return this
-    .data('summaryCallback', callback)
-    // To prevent duplicate events, the handlers are first removed and then
-    // (re-)added.
-    .off('formUpdated.summary')
-    .on('formUpdated.summary', function () {
-      self.trigger('summaryUpdated');
-    })
-    // The actual summaryUpdated handler doesn't fire when the callback is
-    // changed, so we have to do this manually.
-    .trigger('summaryUpdated');
-};
-
-/**
- * Sends a 'formUpdated' event each time a form element is modified.
- */
-Drupal.behaviors.formUpdated = {
-  attach: function (context) {
-    // These events are namespaced so that we can remove them later.
-    var events = 'change.formUpdated click.formUpdated blur.formUpdated keyup.formUpdated';
-    $(context)
-      // Since context could be an input element itself, it's added back to
-      // the jQuery object and filtered again.
-      .find(':input').addBack().filter(':input')
-      // To prevent duplicate events, the handlers are first removed and then
-      // (re-)added.
-      .off(events).on(events, function () {
-        $(this).trigger('formUpdated');
-      });
-  }
-};
-
-/**
- * Prevents consecutive form submissions of identical form values.
- *
- * Repetitive form submissions that would submit the identical form values are
- * prevented, unless the form values are different to the previously submitted
- * values.
- *
- * This is a simplified re-implementation of a user-agent behavior that should
- * be natively supported by major web browsers, but at this time, only Firefox
- * has a built-in protection.
- *
- * A form value-based approach ensures that the constraint is triggered for
- * consecutive, identical form submissions only. Compared to that, a form
- * button-based approach would (1) rely on [visible] buttons to exist where
- * technically not required and (2) require more complex state management if
- * there are multiple buttons in a form.
- *
- * This implementation is based on form-level submit events only and relies on
- * jQuery's serialize() method to determine submitted form values. As such, the
- * following limitations exist:
- *
- * - Event handlers on form buttons that preventDefault() do not receive a
- *   double-submit protection. That is deemed to be fine, since such button
- *   events typically trigger reversible client-side or server-side operations
- *   that are local to the context of a form only.
- * - Changed values in advanced form controls, such as file inputs, are not part
- *   of the form values being compared between consecutive form submits (due to
- *   limitations of jQuery.serialize()). That is deemed to be acceptable,
- *   because if the user forgot to attach a file, then the size of HTTP payload
- *   will most likely be small enough to be fully passed to the server endpoint
- *   within (milli)seconds. If a user mistakenly attached a wrong file and is
- *   technically versed enough to cancel the form submission (and HTTP payload)
- *   in order to attach a different file, then that edge-case is not supported
- *   here.
- *
- * Lastly, all forms submitted via HTTP GET are idempotent by definition of HTTP
- * standards, so excluded in this implementation.
- */
-Drupal.behaviors.formSingleSubmit = {
-  attach: function () {
-    function onFormSubmit (e) {
-      var $form = $(e.currentTarget);
-      var formValues = $form.serialize();
-      var previousValues = $form.attr('data-drupal-form-submit-last');
-      if (previousValues === formValues) {
-        e.preventDefault();
-      }
-      else {
-        $form.attr('data-drupal-form-submit-last', formValues);
-      }
-    }
-
-    $('body').once('form-single-submit')
-      .on('submit.singleSubmit', 'form:not([method~="GET"])', onFormSubmit);
-  }
-};
-
-/**
- * Prepopulate form fields with information from the visitor cookie.
- */
-Drupal.behaviors.fillUserInfoFromCookie = {
-  attach: function (context, settings) {
-    var userInfo = ['name', 'mail', 'homepage'];
-    $('form.user-info-from-cookie').once('user-info-from-cookie', function () {
-      var $formContext = $(this);
-      var i, il, $element, cookie;
-      for (i = 0, il = userInfo.length; i < il; i += 1) {
-        $element = $formContext.find('[name=' + userInfo[i] + ']');
-        cookie = $.cookie('Drupal.visitor.' + userInfo[i]);
-        if ($element.length && cookie) {
-          $element.val(cookie);
-        }
-      }
-    });
-  }
-};
-
-})(jQuery);
diff --git a/core/misc/form/form.behaviors.js b/core/misc/form/form.behaviors.js
new file mode 100644
index 0000000..1012384
--- /dev/null
+++ b/core/misc/form/form.behaviors.js
@@ -0,0 +1,79 @@
+/**
+ * @file
+ * Attaches behaviors for forms.
+ */
+
+(function ($, Drupal) {
+
+"use strict";
+
+/**
+ * Triggers the 'formUpdated' event on form elements when they are modified.
+ */
+Drupal.behaviors.formUpdated = {
+  attach: function (context) {
+    var $context = $(context);
+    var contextIsForm = $context.is('form');
+    var $forms = $context.find('form').once('form-updated');
+
+    if (contextIsForm) {
+      $forms = $context;
+    }
+
+    if ($forms.length) {
+      // Initialize form behaviors, use $.makeArray to be able to use native
+      // forEach array method and have the callback parameters in the right order.
+      $.makeArray($forms).forEach(Drupal.form.initialize);
+    }
+    // On ajax requests context is the form element.
+    if (contextIsForm) {
+      Drupal.form.update(context);
+    }
+
+  },
+  detach: function (context, settings, trigger) {
+    var $context = $(context);
+    if (trigger === 'unload') {
+      var $forms = $context.find('form').removeOnce('form-updated');
+      if ($forms.length) {
+        $.makeArray($forms).forEach(Drupal.form.destroy);
+      }
+    }
+  }
+};
+
+
+/**
+ * Prevents consecutive form submissions of identical form values.
+ *
+ * All forms submitted via HTTP GET are idempotent by definition of HTTP
+ * standards, so should be excluded in this implementation.
+ */
+Drupal.behaviors.formSingleSubmit = {
+  attach: function () {
+    $('body').once('form-single-submit')
+      .on('submit.singleSubmit', 'form:not([method~="GET"])', Drupal.form.singleSubmit);
+  }
+};
+
+/**
+ * Prepopulate form fields with information from the visitor cookie.
+ */
+Drupal.behaviors.fillUserInfoFromCookie = {
+  attach: function (context, settings) {
+    var userInfo = ['name', 'mail', 'homepage'];
+    $('form.user-info-from-cookie').once('user-info-from-cookie', function () {
+      var $formContext = $(this);
+      var i, il, $element, cookie;
+      for (i = 0, il = userInfo.length; i < il; i += 1) {
+        $element = $formContext.find('[name=' + userInfo[i] + ']');
+        cookie = $.cookie('Drupal.visitor.' + userInfo[i]);
+        if ($element.length && cookie) {
+          $element.val(cookie);
+        }
+      }
+    });
+  }
+};
+
+})(jQuery, Drupal);
diff --git a/core/misc/form/form.js b/core/misc/form/form.js
new file mode 100644
index 0000000..22aff84
--- /dev/null
+++ b/core/misc/form/form.js
@@ -0,0 +1,135 @@
+/**
+ * @file
+ * Defines Drupal.form object.
+ */
+
+(function ($, Drupal, debounce) {
+
+"use strict";
+
+/**
+ * Sends a 'formUpdated' event each time a form element is modified.
+ */
+function triggerFormUpdated (element) {
+  $(element).trigger('formUpdated');
+}
+
+/**
+ * Library for managing forms.
+ */
+Drupal.form = {
+
+  /**
+   * Binds events to trigger formUpdated and initialize fields list.
+   *
+   * Store the list of field ids in data-drupal-form-fields attribute to compare
+   * fields when an ajax request is triggered.
+   *
+   * @param {HTMLFormElement} form
+   */
+  initialize: function (form) {
+    var events = 'change.formUpdated keypress.formUpdated';
+    var eventHandler = debounce(function (event) { triggerFormUpdated(event.target); }, 300);
+    var formFields = Drupal.form.fields(form).join(',');
+
+    form.setAttribute('data-drupal-form-fields', formFields);
+    $(form).on(events, eventHandler);
+  },
+
+  /**
+   * Check for addition or removal of fields and triggers formUpdated on change.
+   *
+   * @param {HTMLFormElement} form
+   */
+  update: function (form) {
+    var formFields = Drupal.form.fields(form).join(',');
+    // @todo replace with form.getAttribute() when #1979468 is in.
+    var currentFields = $(form).attr('data-drupal-form-fields');
+    // if there has been a change in the fields or their order, trigger
+    // formUpdated.
+    if (formFields !== currentFields) {
+      triggerFormUpdated(form);
+    }
+  },
+
+  /**
+   * Collects the IDs of all form fields in the given form.
+   *
+   * @param {HTMLFormElement} form
+   * @return {Array}
+   */
+  fields: function (form) {
+    var $fieldList = $(form).find('[name]').map(function (index, element) {
+      // We use id to avoid name duplicates on radio fields and filter out
+      // elements with a name but no id.
+      return element.getAttribute('id');
+    });
+    // Return a true array.
+    return $.makeArray($fieldList);
+  },
+
+  /**
+   * Remove extra data attribute and event listeners.
+   *
+   * @param {HTMLFormElement} form
+   */
+  destroy: function (form) {
+    form.removeAttribute('data-drupal-form-fields');
+    $(form).off('.formUpdated');
+  },
+
+  /**
+   * Prevents consecutive form submissions of identical form values.
+   *
+   * Repetitive form submissions that would submit the identical form values are
+   * prevented, unless the form values are different to the previously submitted
+   * values.
+   *
+   * This is a simplified re-implementation of a user-agent behavior that should
+   * be natively supported by major web browsers, but at this time, only Firefox
+   * has a built-in protection.
+   *
+   * A form value-based approach ensures that the constraint is triggered for
+   * consecutive, identical form submissions only. Compared to that, a form
+   * button-based approach would (1) rely on [visible] buttons to exist where
+   * technically not required and (2) require more complex state management if
+   * there are multiple buttons in a form.
+   *
+   * This implementation is based on form-level submit events only and relies on
+   * jQuery's serialize() method to determine submitted form values. As such, the
+   * following limitations exist:
+   *
+   * - Event handlers on form buttons that preventDefault() do not receive a
+   *   double-submit protection. That is deemed to be fine, since such button
+   *   events typically trigger reversible client-side or server-side operations
+   *   that are local to the context of a form only.
+   * - Changed values in advanced form controls, such as file inputs, are not part
+   *   of the form values being compared between consecutive form submits (due to
+   *   limitations of jQuery.serialize()). That is deemed to be acceptable,
+   *   because if the user forgot to attach a file, then the size of HTTP payload
+   *   will most likely be small enough to be fully passed to the server endpoint
+   *   within (milli)seconds. If a user mistakenly attached a wrong file and is
+   *   technically versed enough to cancel the form submission (and HTTP payload)
+   *   in order to attach a different file, then that edge-case is not supported
+   *   here.
+   *
+   * Lastly, all forms submitted via HTTP GET are idempotent by definition of HTTP
+   * standards, so should be excluded in this implementation.
+   *
+   * @param {Event} event
+   */
+  singleSubmit: function (event) {
+    var $form = $(event.currentTarget);
+    var formValues = $form.serialize();
+    var previousValues = $form.attr('data-drupal-form-submit-last');
+    if (previousValues === formValues) {
+      event.preventDefault();
+    }
+    else {
+      $form.attr('data-drupal-form-submit-last', formValues);
+    }
+  }
+
+};
+
+})(jQuery, Drupal, Drupal.debounce);
diff --git a/core/misc/form/jquery.summary.js b/core/misc/form/jquery.summary.js
new file mode 100644
index 0000000..4f3ba9d
--- /dev/null
+++ b/core/misc/form/jquery.summary.js
@@ -0,0 +1,48 @@
+/**
+ * @file
+ * Provides summary-handling logic for Vertical Tabs and collapsible <details>.
+ */
+
+(function ($) {
+
+"use strict";
+
+/**
+ * Retrieves the summary for the first element.
+ */
+$.fn.drupalGetSummary = function () {
+  var callback = this.data('summaryCallback');
+  return (this[0] && callback) ? $.trim(callback(this[0])) : '';
+};
+
+/**
+ * Sets the summary for all matched elements.
+ *
+ * @param callback
+ *   Either a function that will be called each time the summary is
+ *   retrieved or a string (which is returned each time).
+ */
+$.fn.drupalSetSummary = function (callback) {
+  var self = this;
+
+  // To facilitate things, the callback should always be a function. If it's
+  // not, we wrap it into an anonymous function which just returns the value.
+  if (typeof callback !== 'function') {
+    var val = callback;
+    callback = function () { return val; };
+  }
+
+  return this
+    .data('summaryCallback', callback)
+    // To prevent duplicate events, the handlers are first removed and then
+    // (re-)added.
+    .off('formUpdated.summary')
+    .on('formUpdated.summary', function () {
+      self.trigger('summaryUpdated');
+    })
+    // The actual summaryUpdated handler doesn't fire when the callback is
+    // changed, so we have to do this manually.
+    .trigger('summaryUpdated');
+};
+
+})(jQuery);
diff --git a/core/modules/file/file.js b/core/modules/file/file.js
index c7c6362..d5a4ba8 100644
--- a/core/modules/file/file.js
+++ b/core/modules/file/file.js
@@ -17,28 +17,32 @@
 Drupal.behaviors.fileValidateAutoAttach = {
   attach: function (context, settings) {
     var $context = $(context);
-    var validateExtension = Drupal.file.validateExtension;
-    var selector, elements;
+    var elements;
+
+    function initFileValidation (selector) {
+      $context.find(selector)
+        .once('fileValidate')
+        .on('change.fileValidate', { extensions: elements[selector] }, Drupal.file.validateExtension);
+    }
+
     if (settings.file && settings.file.elements) {
       elements = settings.file.elements;
-      for (selector in elements) {
-        if (elements.hasOwnProperty(selector)) {
-          $context.find(selector).on('change', {extensions: elements[selector]}, validateExtension);
-        }
-      }
+      Object.keys(elements).forEach(initFileValidation);
     }
   },
-  detach: function (context, settings) {
+  detach: function (context, settings, trigger) {
     var $context = $(context);
-    var validateExtension = Drupal.file.validateExtension;
-    var selector, elements;
-    if (settings.file && settings.file.elements) {
+    var elements;
+
+    function removeFileValidation (selector) {
+      $context.find(selector)
+        .removeOnce('fileValidate')
+        .off('.fileValidate', Drupal.file.validateExtension);
+    }
+
+    if (trigger === 'unload' && settings.file && settings.file.elements) {
       elements = settings.file.elements;
-      for (selector in elements) {
-        if (elements.hasOwnProperty(selector)) {
-          $context.find(selector).off('change', validateExtension);
-        }
-      }
+      Object.keys(elements).forEach(removeFileValidation);
     }
   }
 };
@@ -88,12 +92,11 @@ Drupal.behaviors.filePreviewLinks = {
 /**
  * File upload utility functions.
  */
-Drupal.file = Drupal.file || {
+Drupal.file = {
   /**
    * Client-side file input validation of file extensions.
    */
   validateExtension: function (event) {
-    event.preventDefault();
     // Remove any previous errors.
     $('.file-upload-js-error').remove();
 
@@ -115,6 +118,8 @@ Drupal.file = Drupal.file || {
         });
         $(this).closest('div.form-managed-file').prepend('<div class="messages messages--error file-upload-js-error" aria-live="polite">' + error + '</div>');
         this.value = '';
+        // Cancel all other change event handlers.
+        event.stopImmediatePropagation();
       }
     }
   },
diff --git a/core/modules/file/file.module b/core/modules/file/file.module
index 892b214..67aac2a 100644
--- a/core/modules/file/file.module
+++ b/core/modules/file/file.module
@@ -1264,7 +1264,7 @@ function file_managed_file_process($element, &$form_state, $form) {
     $element['upload']['#attached']['js'] = array(
       array(
         'type' => 'setting',
-        'data' => array('file' => array('elements' => array('#' . $element['#id'] . '-upload' => $extension_list)))
+        'data' => array('file' => array('elements' => array('#' . $element['#id'] => $extension_list)))
       )
     );
   }
diff --git a/core/modules/filter/filter.js b/core/modules/filter/filter.js
index c9e8388..2bab50b 100644
--- a/core/modules/filter/filter.js
+++ b/core/modules/filter/filter.js
@@ -12,15 +12,22 @@
  */
 Drupal.behaviors.filterGuidelines = {
   attach: function (context) {
+
+    function updateFilterGuidelines (event) {
+      var $this = $(event.target);
+      var value = $this.val();
+      $this.closest('.filter-wrapper')
+        .find('.filter-guidelines-item').hide()
+        .filter('.filter-guidelines-' + value).show();
+    }
+
     $(context).find('.filter-guidelines').once('filter-guidelines')
       .find(':header').hide()
       .closest('.filter-wrapper').find('select.filter-list')
-      .on('change', function () {
-        $(this).closest('.filter-wrapper')
-          .find('.filter-guidelines-item').hide()
-          .filter('.filter-guidelines-' + this.value).show();
-      })
-      .trigger('change');
+      .on('change.filterGuidelines', updateFilterGuidelines)
+      // Need to trigger the namespaced event to avoid triggering formUpdated
+      // when initializing the select.
+      .trigger('change.filterGuidelines');
   }
 };
 
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index fc6f61c..898c3c7 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -954,11 +954,14 @@ function system_library_info() {
     'title' => 'Drupal form library',
     'version' => \Drupal::VERSION,
     'js' => array(
-      'core/misc/form.js' => array('group' => JS_LIBRARY, 'weight' => 1),
+      'core/misc/form/form.js' => array('group' => JS_LIBRARY),
+      'core/misc/form/jquery.summary.js' => array('group' => JS_LIBRARY),
+      'core/misc/form/form.behaviors.js' => array('group' => JS_LIBRARY),
     ),
     'dependencies' => array(
       array('system', 'jquery'),
       array('system', 'drupal'),
+      array('system', 'drupal.debounce'),
       array('system', 'jquery.cookie'),
       array('system', 'jquery.once'),
     ),
