diff --git a/core/misc/machine-name.js b/core/misc/machine-name.js
index bbad2e3..3aac825 100644
--- a/core/misc/machine-name.js
+++ b/core/misc/machine-name.js
@@ -12,7 +12,8 @@
    *
    * @type {Drupal~behavior}
    *
-   * @prop {Drupal~behaviorAttach} attach
+   * @prop {Drupal~behaviorAttach} a
+   *ttach
    *   Attaches machine-name behaviors.
    */
   Drupal.behaviors.machineName = {
@@ -43,165 +44,233 @@
      *   - field_suffix: The #field_suffix of the form element.
      */
     attach: function (context, settings) {
-      var self = this;
       var $context = $(context);
-      var timeout = null;
-      var xhr = null;
-
-      function clickEditHandler(e) {
-        var data = e.data;
-        data.$wrapper.removeClass('visually-hidden');
-        data.$target.trigger('focus');
-        data.$suffix.hide();
-        data.$source.off('.machineName');
-      }
 
-      function machineNameHandler(e) {
-        var data = e.data;
-        var options = data.options;
-        var baseValue = $(e.target).val();
+      Object.keys(settings.machineName).forEach(function (source_id) {
+        if (settings.machineName.hasOwnProperty(source_id)) {
+          var $source = $context.find(source_id).once('machine-name');
 
-        var rx = new RegExp(options.replace_pattern, 'g');
-        var expected = baseValue.toLowerCase().replace(rx, options.replace).substr(0, options.maxlength);
+          if ($source.length) {
+            settings.machineName[source_id].instance = new Drupal.machineName(settings.machineName[source_id]);
 
-        // Abort the last pending request because the label has changed and it
-        // is no longer valid.
-        if (xhr && xhr.readystate !== 4) {
-          xhr.abort();
-          xhr = null;
+            settings.machineName[source_id].instance.attachUI($context, $source);
+          }
         }
+      });
+    },
+    /**
+     * Detach machine name functionality from the provided scope.
+     *
+     * @param context
+     * @param settings
+     */
+    detach: function (context, settings) {
+      Object.keys(settings.machineName).forEach(function (source_id) {
+        var $source = $context.find(source_id);
 
-        // Wait 300 milliseconds since the last event to update the machine name
-        // i.e., after the user has stopped typing.
-        if (timeout) {
-          clearTimeout(timeout);
-          timeout = null;
+        // Field within scope and there is currently a machine name instance.
+        if ($source.length && settings.machineName[source_id].instance) {
+          // Detach UI and unset the machine name instance.
+          settings.machineName[source_id].instance.detachUI();
+          delete settings.machineName[source_id].instance;
         }
-        timeout = setTimeout(function () {
-          if (baseValue.toLowerCase() !== expected) {
-            xhr = self.transliterate(baseValue, options).done(function (machine) {
-              self.showMachineName(machine.substr(0, options.maxlength), data);
-            });
-          }
-          else {
-            self.showMachineName(expected, data);
-          }
-        }, 300);
-      }
+      });
+    }
+  };
 
-      Object.keys(settings.machineName).forEach(function (source_id) {
-        var machine = '';
-        var eventData;
-        var options = settings.machineName[source_id];
-
-        var $source = $context.find(source_id).addClass('machine-name-source').once('machine-name');
-        var $target = $context.find(options.target).addClass('machine-name-target');
-        var $suffix = $context.find(options.suffix);
-        var $wrapper = $target.closest('.js-form-item');
-        // All elements have to exist.
-        if (!$source.length || !$target.length || !$suffix.length || !$wrapper.length) {
-          return;
-        }
-        // Skip processing upon a form validation error on the machine name.
-        if ($target.hasClass('error')) {
-          return;
-        }
-        // Figure out the maximum length for the machine name.
-        options.maxlength = $target.attr('maxlength');
-        // Hide the form item container of the machine name form element.
-        $wrapper.addClass('visually-hidden');
-        // Determine the initial machine name value. Unless the machine name
-        // form element is disabled or not empty, the initial default value is
-        // based on the human-readable form element value.
-        if ($target.is(':disabled') || $target.val() !== '') {
-          machine = $target.val();
-        }
-        else if ($source.val() !== '') {
-          machine = self.transliterate($source.val(), options);
-        }
-        // Append the machine name preview to the source field.
-        var $preview = $('<span class="machine-name-value">' + options.field_prefix + Drupal.checkPlain(machine) + options.field_suffix + '</span>');
-        $suffix.empty();
-        if (options.label) {
-          $suffix.append('<span class="machine-name-label">' + options.label + ': </span>');
-        }
-        $suffix.append($preview);
+  /**
+   * Constructor for the machine name.
+   *
+   * @see Drupal.behaviors.machineName.attach().
+   */
+  Drupal.machineName = function(options) {
+    this.options = options;
+  };
 
-        // If the machine name cannot be edited, stop further processing.
-        if ($target.is(':disabled')) {
-          return;
-        }
+  /**
+   * Attach the UI to the source field within the provided context.
+   *
+   * @param $context
+   *   The jQuery object the machine name field is contained in.
+   * @param $source
+   *   The jQuery object for the source field.
+   */
+  Drupal.machineName.prototype.attachUI = function($context, $source) {
+    var machine = '';
 
-        eventData = {
-          $source: $source,
-          $target: $target,
-          $suffix: $suffix,
-          $wrapper: $wrapper,
-          $preview: $preview,
-          options: options
-        };
-        // If it is editable, append an edit link.
-        var $link = $('<span class="admin-link"><button type="button" class="link">' + Drupal.t('Edit') + '</button></span>').on('click', eventData, clickEditHandler);
-        $suffix.append($link);
-
-        // Preview the machine name in realtime when the human-readable name
-        // changes, but only if there is no machine name yet; i.e., only upon
-        // initial creation, not when editing.
-        if ($target.val() === '') {
-          $source.on('formUpdated.machineName', eventData, machineNameHandler)
-            // Initialize machine name preview.
-            .trigger('formUpdated.machineName');
-        }
+    this.$source = $source;
 
-        // Add a listener for an invalid event on the machine name input
-        // to show its container and focus it.
-        $target.on('invalid', eventData, clickEditHandler);
-      });
-    },
+    this.$target = $context.find(this.options.target).addClass('machine-name-target');
+    this.$suffix = $context.find(this.options.suffix);
+    this.$wrapper = this.$target.closest('.js-form-item');
 
-    showMachineName: function (machine, data) {
-      var settings = data.options;
-      // Set the machine name to the transliterated value.
-      if (machine !== '') {
-        if (machine !== settings.replace) {
-          data.$target.val(machine);
-          data.$preview.html(settings.field_prefix + Drupal.checkPlain(machine) + settings.field_suffix);
-        }
-        data.$suffix.show();
+    // All elements have to exist.
+    if (!$source.length || !this.$target.length || !this.$suffix.length || !this.$wrapper.length) {
+      return;
+    }
+    // Skip processing upon a form validation error on the machine name.
+    if (this.$target.hasClass('error')) {
+      return;
+    }
+
+    $source.addClass('machine-name-source');
+
+    // Figure out the maximum length for the machine name.
+    this.options.maxlength = this.$target.attr('maxlength');
+    // Hide the form item container of the machine name form element.
+    this.$wrapper.addClass('visually-hidden');
+    // Determine the initial machine name value. Unless the machine name
+    // form element is disabled or not empty, the initial default value is
+    // based on the human-readable form element value.
+    if (this.$target.is(':disabled') || this.$target.val() !== '') {
+      machine = this.$target.val();
+    }
+    else if ($source.val() !== '') {
+      machine = this.convert($source.val(), this.options);
+    }
+    // Append the machine name preview to the source field.
+    this.$preview = $('<span class="machine-name-value">' + this.options.field_prefix + Drupal.checkPlain(machine) + this.options.field_suffix + '</span>');
+    this.$suffix.empty();
+
+    if (this.options.label) {
+      this.$preview.prepend($('<span class="machine-name-label">' + this.options.label + ': </span>'));
+    }
+    this.$suffix.append(this.$preview);
+
+    // If the machine name cannot be edited, stop further processing.
+    if (this.$target.is(':disabled')) {
+      return;
+    }
+
+    // If it is editable, append an edit link.
+    this.$link = $('<span class="admin-link"><button type="button" class="link">' + Drupal.t('Edit') + '</button></span>').on('click', this, this.editManually);
+    this.$suffix.append(this.$link);
+
+    // Preview the machine name in realtime when the human-readable name
+    // changes, but only if there is no machine name yet; i.e., only upon
+    // initial creation, not when editing.
+    if (this.$target.val() === '') {
+      $source.on('formUpdated.machineName', this, this.update);
+
+      // Initialize machine name preview.
+      this.update({data: this});
+    }
+
+    // Add a listener for an invalid event on the machine name input
+    // to show its container and focus it.
+    this.$target.on('invalid', this, this.editManually);
+  };
+
+  /**
+   * Destroy the machineName instance and revert the field to it's original state.
+   */
+  Drupal.machineName.prototype.detachUI = function() {
+    if (this.$wrapper) {
+      this.$wrapper.removeClass('visually-hidden');
+      this.$target.trigger('focus');
+      this.$suffix.hide();
+      this.$source.off('.machineName');
+    }
+  };
+
+  /**
+   * Switch to manual editing mode.
+   *
+   * @param {jQuery.Event} e
+   *   The event triggered.
+   */
+  Drupal.machineName.prototype.editManually = function(e) {
+    var machineName = e.data;
+    machineName.detachUI();
+  };
+
+  /**
+   * Act on a machine name.
+   *
+   * Either shows the machine name or if empty, hide the machine name container.
+   *
+   * @param {string} machine
+   *   The machine name.
+   *
+   * @param {Drupal.machineName} machineName
+   *   The machineName instance to show the machine name for.
+   */
+  Drupal.machineName.prototype.showMachineName = function (machine) {
+    // Set the machine name to the transliterated value.
+    if (machine !== '') {
+      if (machine !== this.options.replace) {
+        this.$target.val(machine);
+        this.$preview.html(this.options.field_prefix + Drupal.checkPlain(machine) + this.options.field_suffix);
       }
-      else {
-        data.$suffix.hide();
-        data.$target.val(machine);
-        data.$preview.empty();
+      this.$suffix.show();
+    }
+    else {
+      this.$suffix.hide();
+      this.$target.val(machine);
+      this.$preview.empty();
+    }
+  };
+
+  /**
+   * Act on a change on the input field and update the machine name.
+   *
+   * @param {jQuery.Event} e
+   *   The event triggered.
+   */
+  Drupal.machineName.prototype.update = function(e) {
+    var machineName = e.data;
+    var baseValue = machineName.$source.val();
+
+    machineName.convert(baseValue, function(machine) {
+      machineName.showMachineName(machine.substr(0, machineName.options.maxlength));
+    });
+  };
+
+  /**
+   * Convert a human-readable name to a machine name.
+   *
+   * @param {string} source
+   *   A string to transliterate.
+   * @param {function} callback
+   *   The callback to run once the source is converted to a machine name.
+   */
+  Drupal.machineName.prototype.convert = function (source, callback) {
+    var rx = new RegExp(this.options.replace_pattern, 'g');
+    var expected = source.toLowerCase().replace(rx, this.options.replace).substr(0, this.options.maxlength);
+    var machineName = this;
+
+    // Abort the last pending request because the label has changed and it
+    // is no longer valid.
+    if (this.xhr && this.xhr.readystate !== 4) {
+      this.xhr.abort();
+      this.xhr = null;
+    }
+
+    if (source.toLowerCase() !== expected) {
+      // Wait 300 milliseconds since the last event to update the machine name
+      // i.e., after the user has stopped typing.
+      if (this.timeout) {
+        clearTimeout(this.timeout);
+        this.timeout = null;
       }
-    },
+      this.timeout = setTimeout(function () {
+        this.xhr = $.get(Drupal.url('machine_name/transliterate'), {
+          text: source,
+          langcode: drupalSettings.langcode,
+          replace_pattern: machineName.options.replace_pattern,
+          replace: machineName.options.replace,
+          lowercase: true
+        });
 
-    /**
-     * Transliterate a human-readable name to a machine name.
-     *
-     * @param {string} source
-     *   A string to transliterate.
-     * @param {object} settings
-     *   The machine name settings for the corresponding field.
-     * @param {string} settings.replace_pattern
-     *   A regular expression (without modifiers) matching disallowed characters
-     *   in the machine name; e.g., '[^a-z0-9]+'.
-     * @param {string} settings.replace
-     *   A character to replace disallowed characters with; e.g., '_' or '-'.
-     * @param {number} settings.maxlength
-     *   The maximum length of the machine name.
-     *
-     * @return {jQuery}
-     *   The transliterated source string.
-     */
-    transliterate: function (source, settings) {
-      return $.get(Drupal.url('machine_name/transliterate'), {
-        text: source,
-        langcode: drupalSettings.langcode,
-        replace_pattern: settings.replace_pattern,
-        replace: settings.replace,
-        lowercase: true
-      });
+        //
+        this.xhr.done(function(machine) {
+          callback.call(machineName, machine.substr(0, machineName.options.maxlength));
+        });
+      }, 300);
+    }
+    // No server side transliteration needed, directly update the machine name.
+    else {
+      callback.call(this, expected);
     }
   };
 
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Core/MachineNameTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Core/MachineNameTest.php
new file mode 100644
index 0000000..bf4649f
--- /dev/null
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Core/MachineNameTest.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Drupal\FunctionalJavascriptTests\Core;
+
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+use Drupal\Component\Transliteration\TransliterationInterface;
+use Drupal\Component\Utility\Unicode;
+
+/**
+ * Tests for the machine name field.
+ *
+ * @group session
+ */
+class MachineNameTest extends JavascriptTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['system', 'node', 'user'];
+
+  /**
+   * The transliteration helper.
+   *
+   * @var \Drupal\Component\Transliteration\TransliterationInterface
+   */
+  protected $transliteration;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $account = $this->drupalCreateUser(array(
+      'access content',
+      'administer permissions',
+    ));
+    $this->drupalLogin($account);
+
+    // Get the transliteration server for validating the generated machine name.
+    $this->transliteration = \Drupal::service('transliteration');
+  }
+
+  /**
+   * Tests that machine name field functions.
+   *
+   * Makes sure that the machine name field automatically provides a valid
+   * machine name and that the manual editing mode functions.
+   */
+  public function testMachineName() {
+    // Visit the add role page which contains a machine name field.
+    $this->drupalGet('admin/people/roles/add');
+
+    // Test values for conversion.
+    $test_values = array(
+      'Test value !0-9@' => 'A title that should be transliterated must be equal to the php generated machine name',
+      'Test value' => 'A title that should not be transliterated must be equal to the php generated machine name',
+    );
+
+    // Find relevant elements on the page for this test.
+    $page = $this->getSession()->getPage();
+    $assert_session = $this->assertSession();
+    $element = $page->findField('label');
+    $machine_name = $page->find('css', '#edit-label-machine-name-suffix .machine-name-value');
+    $button = $page->find('css', '#edit-label-machine-name-suffix button.link');
+    $id = $page->findField('id');
+
+    // Test each value for conversion to a machine name.
+    foreach ($test_values as $test_value => $message) {
+      $transliterated = $this->transliteration->transliterate($test_value);
+      $transliterated = Unicode::strtolower($transliterated);
+      $transliterated = preg_replace('@' . strtr('[^a-z0-9_]+', [
+          '@' => '\@',
+          chr(0) => ''
+        ]) . '@', '_', $transliterated);
+
+      // Set the value for the role, triggering the machine name update.
+      $element->setValue($test_value);
+
+      // Wait the set timeout for fetching the machine name.
+      $this->getSession()->wait(300);
+
+      // And wait for the autocomplete to finish.
+      $assert_session->waitOnAutocomplete();
+
+      // Get the generated machine name.
+      $generated_value = $machine_name->getHTML();
+
+      // Validate the generated machine name.
+      $this->assertEquals($transliterated, $generated_value, $message);
+    }
+
+    // Test switching back to the manual editing mode by clicking the edit link.
+    $button->click();
+
+    // Validate the visibility of the machine name field.
+    $this->assertTrue($id->isVisible(), 'The ID field must now be visible');
+
+    // Validate if the element contains the correct value.
+    $this->assertEquals($transliterated, $id->getValue(), 'The ID field value must be equal to the php generated machine name');
+  }
+
+}
