diff --git a/ajax_example/ajax_example.info.yml b/ajax_example/ajax_example.info.yml
new file mode 100644
index 0000000..866e240
--- /dev/null
+++ b/ajax_example/ajax_example.info.yml
@@ -0,0 +1,8 @@
+name: 'AJAX Example'
+type: module
+description: 'An example module showing how to use Drupal AJAX forms.'
+package: 'Example modules'
+core: 8.x
+dependencies:
+  - core:node
+  - drupal:examples
diff --git a/ajax_example/ajax_example.libraries.yml b/ajax_example/ajax_example.libraries.yml
new file mode 100644
index 0000000..4df89d8
--- /dev/null
+++ b/ajax_example/ajax_example.libraries.yml
@@ -0,0 +1,7 @@
+ajax_example.library:
+  version: 1.x
+  css:
+    base:
+      css/ajax-example-base.css: {}
+  js:
+    js/ajax-example.js: {}
diff --git a/ajax_example/ajax_example.links.menu.yml b/ajax_example/ajax_example.links.menu.yml
new file mode 100644
index 0000000..b0c31d4
--- /dev/null
+++ b/ajax_example/ajax_example.links.menu.yml
@@ -0,0 +1,72 @@
+ajax_example.description:
+  title: 'AJAX Example'
+  route_name: 'ajax_example.description'
+  expanded: TRUE
+
+ajax_example.simplest:
+  title: 'Simplest AJAX example'
+  route_name: 'ajax_example.simplest'
+  parent: ajax_example.description
+  weight: 0
+
+ajax_example.submit-driven:
+  title: 'Submit-driven AJAX'
+  route_name: 'ajax_example.submit_driven_ajax'
+  parent: ajax_example.description
+  weight: 1
+
+ajax_example.render-link:
+  title: 'AJAX link in a render array'
+  route_name: 'ajax_example.ajax_link_render'
+  parent: ajax_example.description
+  weight: 2
+
+ajax_example.wizard-example:
+  title: 'Wizard example'
+  route_name: 'ajax_example.wizard'
+  parent: ajax_example.description
+  weight: 2
+
+ajax_example.wizard-examplenojs:
+  title: 'Wizard example w/JS turned off'
+  route_name: 'ajax_example.wizardnojs'
+  parent: ajax_example.description
+  weight: 3
+
+ajax_example.autocomplete-user:
+  title: 'Autocomplete user with entity_autocomplete'
+  route_name: 'ajax_example.autocomplete_user'
+  parent: ajax_example.description
+  weight: 4
+
+ajax_example.autotextfields:
+  title: 'Generate textfields'
+  route_name: 'ajax_example.autotextfields'
+  parent: ajax_example.description
+  weight: 5
+
+ajax_example.dependent-dropdown:
+  title: 'Dependent dropdown'
+  route_name: 'ajax_example.dependent_dropdown'
+  parent: ajax_example.description
+  weight: 6
+ajax_example.dependent-dropdown-nojs:
+  title: 'Dependent dropdown w/ no JS'
+  route_name: 'ajax_example.dependent_dropdown'
+  route_parameters:
+    nojs: nojs
+  parent: ajax_example.description
+  weight: 6
+
+ajax_example.dynamic-form-sections:
+  title: 'Dynamic form sections'
+  route_name: 'ajax_example.dynamic_form_sections'
+  parent: ajax_example.description
+  weight: 10
+ajax_example.dynamic-form-sections-nojs:
+  title: 'Dynamic form sections w/ no JS'
+  route_name: 'ajax_example.dynamic_form_sections'
+  route_parameters:
+    nojs: nojs
+  parent: ajax_example.description
+  weight: 10
diff --git a/ajax_example/ajax_example.module b/ajax_example/ajax_example.module
new file mode 100644
index 0000000..1c2a6de
--- /dev/null
+++ b/ajax_example/ajax_example.module
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * @file
+ * AJAX Examples module file with basic examples.
+ */
+
+/**
+ * @defgroup ajax_example Example: AJAX
+ * @ingroup examples
+ * @{
+ * These examples show basic AJAX concepts.
+ *
+ * General documentation is available at
+ * @link ajax AJAX Framework documentation @endlink and at the
+ * @link http://drupal.org/node/752056 AJAX Forms handbook page @endlink.
+ *
+ * The several examples here demonstrate basic AJAX usage.
+ */
+
+/**
+ * @} End of "defgroup ajax_example".
+ */
diff --git a/ajax_example/ajax_example.routing.yml b/ajax_example/ajax_example.routing.yml
new file mode 100644
index 0000000..b5a35d6
--- /dev/null
+++ b/ajax_example/ajax_example.routing.yml
@@ -0,0 +1,96 @@
+ajax_example.description:
+  path: 'examples/ajax-example'
+  defaults:
+    _controller: '\Drupal\ajax_example\Controller\AjaxExampleController::description'
+    _title: 'AJAX Example'
+  requirements:
+    _permission: 'access content'
+
+ajax_example.simplest:
+  path: 'examples/ajax-example/simplest'
+  defaults:
+    _form: '\Drupal\ajax_example\Form\Simplest'
+    _title: 'Simplest AJAX example'
+  requirements:
+    _permission: 'access content'
+
+ajax_example.autotextfields:
+  path: 'examples/ajax-example/autotextfields'
+  defaults:
+    _form: '\Drupal\ajax_example\Form\Autotextfields'
+    _title: 'Generate textfields'
+  requirements:
+    _permission: 'access content'
+
+ajax_example.submit_driven_ajax:
+  path: 'examples/ajax-example/submit-driven-ajax'
+  defaults:
+    _form: '\Drupal\ajax_example\Form\SubmitDriven'
+    _title: 'Submit-driven AJAX'
+  requirements:
+    _permission: 'access content'
+
+ajax_example.dependent_dropdown:
+  path: 'examples/ajax-example/dependent-dropdown/{nojs}'
+  defaults:
+    _form: '\Drupal\ajax_example\Form\DependentDropdown'
+    _title: 'Dependent dropdown'
+    nojs: ajax
+  requirements:
+    _permission: 'access content'
+
+ajax_example.dynamic_form_sections:
+  path: 'examples/ajax-example/dynamic-form-sections/{nojs}'
+  defaults:
+    _form: '\Drupal\ajax_example\Form\DynamicFormSections'
+    _title: 'Dynamic form sections'
+    nojs: 'ajax'
+  requirements:
+    _permission: 'access content'
+
+ajax_example.wizard:
+  path: 'examples/ajax-example/wizard'
+  defaults:
+    _form: '\Drupal\ajax_example\Form\Wizard'
+    _title: 'Wizard with graceful degradation'
+  requirements:
+    _permission: 'access content'
+
+ajax_example.wizardnojs:
+  path: 'examples/ajax-example/wizard-nojs/{no_js_use}'
+  defaults:
+    _form: '\Drupal\ajax_example\Form\Wizard'
+    _title: 'Wizard with graceful degradation, w/JS turned off'
+    no_js_use: TRUE
+  requirements:
+    _permission: 'access content'
+
+ajax_example.ajax_link_render:
+  path: 'examples/ajax-example/ajax-link-renderable'
+  defaults:
+    _controller: '\Drupal\ajax_example\Controller\AjaxExampleController::renderLinkRenderableArray'
+    _title: 'AJAX link from a render array'
+  requirements:
+    _permission: 'access content'
+
+# This route is for an AJAX callback. It is used by the AJAX system on
+# ajax_example.ajax_link_render. It has a {nojs} parameter, which gives us
+# a way to know whether the request is an AJAX request or is from some other
+# source.
+ajax_example.ajax_link_callback:
+  path: 'examples/ajax-example/ajax-link-callback/{nojs}'
+  defaults:
+    _controller: '\Drupal\ajax_example\Controller\AjaxExampleController::ajaxLinkCallback'
+    # We provide a default value for {nojs}, so that it can be an optional
+    # parameter.
+    nojs: 'nojs'
+  requirements:
+    _permission: 'access content'
+
+ajax_example.autocomplete_user:
+  path: 'examples/ajax_example/user_autocomplete'
+  defaults:
+    _form: '\Drupal\ajax_example\Form\EntityAutocomplete'
+    _title: 'Autocomplete users with entity_autocomplete.'
+  requirements:
+    _permission: 'access content'
diff --git a/ajax_example/css/ajax-example-base.css b/ajax_example/css/ajax-example-base.css
new file mode 100644
index 0000000..07fc129
--- /dev/null
+++ b/ajax_example/css/ajax-example-base.css
@@ -0,0 +1,9 @@
+/*
+ * @file
+ * CSS for ajax_example.
+ */
+
+/* Put some buttons inline, for nicer formatting. */
+.ajax-example-inline {
+  display: inline-block;
+}
diff --git a/ajax_example/js/ajax-example.js b/ajax_example/js/ajax-example.js
new file mode 100644
index 0000000..24040a0
--- /dev/null
+++ b/ajax_example/js/ajax-example.js
@@ -0,0 +1,19 @@
+/**
+ * @file
+ * JavaScript for ajax_example.
+ */
+
+(function ($) {
+
+  // Re-enable form elements that are disabled for non-ajax situations.
+  Drupal.behaviors.enableFormItemsForAjaxForms = {
+    attach: function () {
+      // If ajax is enabled, we want to hide items that are marked as hidden in
+      // our example.
+      if (Drupal.ajax) {
+        $('.ajax-example-hide').hide();
+      }
+    }
+  };
+
+})(jQuery);
diff --git a/ajax_example/src/Controller/AjaxExampleController.php b/ajax_example/src/Controller/AjaxExampleController.php
new file mode 100644
index 0000000..afd8c08
--- /dev/null
+++ b/ajax_example/src/Controller/AjaxExampleController.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace Drupal\ajax_example\Controller;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\AppendCommand;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Url;
+use Drupal\examples\Utility\DescriptionTemplateTrait;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Controller routines for AJAX example routes.
+ */
+class AjaxExampleController extends ControllerBase {
+
+  use DescriptionTemplateTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getModuleName() {
+    return 'ajax_example';
+  }
+
+  /**
+   * Demonstrates a clickable AJAX-enabled link using the 'use-ajax' class.
+   *
+   * Because of the 'use-ajax' class applied here, the link submission is done
+   * without a page refresh.
+   *
+   * When using the AJAX framework outside the context of a form or a renderable
+   * array of type 'link', you have to include ajax.js explicitly.
+   *
+   * @return array
+   *   Form API array.
+   *
+   * @ingroup ajax_example
+   */
+  public function renderLinkRenderableArray() {
+    $build['my_div'] = [
+      '#markup' => $this->t('
+The link below has been rendered as an element with the #ajax property, so if
+javascript is enabled, ajax.js will try to submit it via an AJAX call instead
+of a normal page load. The URL also contains the "/nojs/" magic string, which
+is stripped if javascript is enabled, allowing the server code to tell by the
+URL whether JS was enabled or not, letting it do different things based on that.'),
+    ];
+    // We'll add a nice border element for our demo.
+    $build['ajax_link'] = [
+      '#type' => 'details',
+      '#title' => $this->t('This is the AJAX link'),
+      '#open' => TRUE,
+    ];
+    // We build the AJAX link.
+    $build['ajax_link']['link'] = [
+      '#type' => 'link',
+      '#title' => $this->t('Click me'),
+      // We have to ensure that Drupal's Ajax system is loaded.
+      '#attached' => ['library' => ['core/drupal.ajax']],
+      // We add the 'use-ajax' class so that Drupal's AJAX system can spring
+      // into action.
+      '#attributes' => ['class' => ['use-ajax']],
+      // The URL for this link element is the callback. In our case, it's route
+      // ajax_example.ajax_link_callback, which maps to ajaxLinkCallback()
+      // below. The route has a /{nojs} section, which is how the callback can
+      // know whether the request was made by AJAX or some other means where
+      // JavaScript won't be able to handle the result. If the {nojs} part of
+      // the path is replaced with 'ajax', then the request was made by AJAX.
+      '#url' => Url::fromRoute('ajax_example.ajax_link_callback', ['nojs' => 'ajax']),
+    ];
+    // We provide a DIV that AJAX can append some text into.
+    $build['ajax_link']['destination'] = [
+      '#type' => 'container',
+      '#attributes' => ['id' => ['ajax-example-destination-div']],
+    ];
+    return $build;
+  }
+
+  /**
+   * Callback for link example.
+   *
+   * Takes different logic paths based on whether Javascript was enabled.
+   * If $type == 'ajax', it tells this function that ajax.js has rewritten
+   * the URL and thus we are doing an AJAX and can return an array of commands.
+   *
+   * @param string $nojs
+   *   Either 'ajax' or 'nojs. Type is simply the normal URL argument to this
+   *   URL.
+   *
+   * @return string|array
+   *   If $type == 'ajax', returns an array of AJAX Commands.
+   *   Otherwise, just returns the content, which will end up being a page.
+   */
+  public function ajaxLinkCallback($nojs = 'ajax') {
+    // Determine whether the request is coming from AJAX or not.
+    if ($nojs == 'ajax') {
+      $output = $this->t("This is some content delivered via AJAX");
+      $response = new AjaxResponse();
+      $response->addCommand(new AppendCommand('#ajax-example-destination-div', $output));
+
+      // See ajax_example_advanced.inc for more details on the available
+      // commands and how to use them.
+      // $page = array('#type' => 'ajax', '#commands' => $commands);
+      // ajax_deliver($response);
+      return $response;
+    }
+    $response = new Response($this->t("This is some content delivered via a page load."));
+    return $response;
+  }
+
+}
diff --git a/ajax_example/src/Form/Autotextfields.php b/ajax_example/src/Form/Autotextfields.php
new file mode 100644
index 0000000..207ade4
--- /dev/null
+++ b/ajax_example/src/Form/Autotextfields.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Drupal\ajax_example\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Show/hide textfields based on AJAX-enabled checkbox clicks.
+ */
+class Autotextfields extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'ajax_example_autotextfields';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['ask_first_name'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Ask me my first name'),
+      '#ajax' => [
+        'callback' => '::prompt',
+        'wrapper' => 'textfields',
+        'effect' => 'fade',
+      ],
+    ];
+    $form['ask_last_name'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Ask me my last name'),
+      '#ajax' => [
+        'callback' => '::prompt',
+        'wrapper' => 'textfields',
+        'effect' => 'fade',
+      ],
+    ];
+
+    $form['textfields'] = [
+      '#title' => $this->t("Generated text fields for first and last name"),
+      '#prefix' => '<div id="textfields">',
+      '#suffix' => '</div>',
+      '#type' => 'fieldset',
+      '#description' => t('This is where we put automatically generated textfields'),
+    ];
+
+    // Since checkboxes return TRUE or FALSE, we have to check that
+    // $form_state has been filled as well as what it contains.
+    if (!empty($form_state->getValue('ask_first_name')) && $form_state->getValue('ask_first_name')) {
+      $form['textfields']['first_name'] = [
+        '#type' => 'textfield',
+        '#title' => $this->t('First Name'),
+      ];
+    }
+    if (!empty($form_state->getValue('ask_last_name')) && $form_state->getValue('ask_last_name')) {
+      $form['textfields']['last_name'] = [
+        '#type' => 'textfield',
+        '#title' => $this->t('Last Name'),
+      ];
+    }
+
+    $form['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Click Me'),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+  }
+
+  /**
+   * Callback for autotextfields.
+   *
+   * Selects the piece of the form we want to use as replacement text and
+   * returns it as a form (renderable array).
+   */
+  public function prompt($form, FormStateInterface $form_state) {
+    return $form['textfields'];
+  }
+
+}
diff --git a/ajax_example/src/Form/DependentDropdown.php b/ajax_example/src/Form/DependentDropdown.php
new file mode 100644
index 0000000..3260812
--- /dev/null
+++ b/ajax_example/src/Form/DependentDropdown.php
@@ -0,0 +1,266 @@
+<?php
+
+namespace Drupal\ajax_example\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Link;
+
+/**
+ * Re-populate a dropdown based on form state.
+ */
+class DependentDropdown extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'ajax_example_dependentdropdown';
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * The $nojs parameter is specified as a path parameter on the route.
+   *
+   * @see ajax_example.routing.yml
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $nojs = NULL) {
+    // Add our CSS and tiny JS to hide things when they should be hidden.
+    $form['#attached']['library'][] = 'ajax_example/ajax_example.library';
+
+    // Explanatory text with helpful links.
+    $form['info'] = [
+      '#markup' =>
+      $this->t('<p>Like other examples in this module, this form has a path that
+          can be modified with /nojs to simulate its behavior without JavaScript.
+        </p><ul>
+        <li>@try_it_without_ajax</li>
+        <li>@try_it_with_ajax</li>
+      </ul>',
+        [
+          '@try_it_without_ajax' => Link::createFromRoute(
+            $this->t('Try it without AJAX'),
+            'ajax_example.dependent_dropdown', ['nojs' => 'nojs'])
+            ->toString(),
+          '@try_it_with_ajax' => Link::createFromRoute(
+            $this->t('Try it with AJAX'),
+            'ajax_example.dependent_dropdown')
+            ->toString(),
+        ]
+      ),
+    ];
+
+    // Our first dropdown lets us select a family of instruments: String,
+    // Woodwind, Brass, or Percussion.
+    $instrument_family_options = static::getFirstDropdownOptions();
+    // When the AJAX request occurs, this form will be build in order to process
+    // form state before the AJAX callback is called. We can use this
+    // opportunity to populate the form as we wish based on the changes to the
+    // form that caused the AJAX request. If the user caused the AJAX request,
+    // then it would have been setting a value for instrument_family_options.
+    // So if there's a value in that dropdown before we build it here, we grab
+    // it's value to help us build the specific instrument dropdown. Otherwise
+    // we can just use the value of the first item as the default value.
+    if (empty($form_state->getValue('instrument_family_dropdown'))) {
+      // Use a default value.
+      $selected_family = key($instrument_family_options);
+    }
+    else {
+      // Get the value if it already exists.
+      $selected_family = $form_state->getValue('instrument_family_dropdown');
+    }
+
+    $form['instrument_family_fieldset'] = [
+      '#type' => 'fieldset',
+      '#title' => $this->t('Choose an instrument family'),
+    ];
+    $form['instrument_family_fieldset']['instrument_family_dropdown'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Instrument Type'),
+      '#options' => $instrument_family_options,
+      '#default_value' => $selected_family,
+      // Bind an ajax callback to the change event (which is the default for the
+      // select form type) of the first dropdown. It will replace the second
+      // dropdown when rebuilt.
+      '#ajax' => [
+        // When 'event' occurs, Drupal will perform an ajax request in the
+        // background. Usually the default value is sufficient (eg. change for
+        // select elements), but valid values include any jQuery event,
+        // most notably 'mousedown', 'blur', and 'submit'.
+        'callback' => '::instrumentDropdownCallback',
+        'wrapper' => 'instrument-fieldset',
+      ],
+    ];
+    // Since we don't know if the user has js or not, we always need to output
+    // this element, then hide it with with css if javascript is enabled.
+    $form['instrument_family_fieldset']['choose_family'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Choose'),
+      '#attributes' => ['class' => ['ajax-example-hide', 'ajax-example-inline']],
+    ];
+    // We are using the path parameter $nojs to signal when to simulate the
+    // the user turning off JavaScript. We'll remove all the AJAX elements. This
+    // is not required, and is here so that we can demonstrate a graceful
+    // fallback without having to turn off JavaScript.
+    if ($nojs == 'nojs') {
+      // Removing the #ajax element tells the system not to use AJAX.
+      unset($form['instrument_family_fieldset']['instrument_family_dropdown']['#ajax']);
+      // Removing the ajax-example-hide class from the Choose button ensures
+      // that our JavaScript won't hide it.
+      unset($form['instrument_family_fieldset']['choose_family']['#attributes']);
+    }
+
+    // Build the instrument field set.
+    $form['instrument_fieldset'] = [
+      '#type' => 'fieldset',
+      '#title' => $this->t('Choose an instrument'),
+      // Since we're managing state for this whole fieldset (both the dropdown
+      // and enabling the Submit button), we want to replace the whole thing
+      // on AJAX requests.
+      '#prefix' => '<div id="instrument-fieldset">',
+      '#suffix' => '</div>',
+    ];
+    $form['instrument_fieldset']['instrument_dropdown'] = [
+      '#type' => 'select',
+      '#title' => $instrument_family_options[$selected_family] . ' ' . $this->t('Instruments'),
+      // When the form is rebuilt during ajax processing, the $selected_family
+      // variable will now have the new value and so the options will change.
+      '#options' => static::getSecondDropdownOptions($selected_family),
+      '#default_value' => !empty($form_state->getValue('instrument_dropdown')) ? $form_state->getValue('instrument_dropdown') : '',
+    ];
+    $form['instrument_fieldset']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Submit'),
+    ];
+    // We might normally use #state to disable the instrument fields based on
+    // the instrument family fields. But since the premise is that we don't have
+    // JavaScript running, #state won't work either. We have to set up the state
+    // of the instrument fieldset here, based on the selected instrument family.
+    if ($selected_family == 'none') {
+      $form['instrument_fieldset']['instrument_dropdown']['#title'] = $this->t('You must choose an instrument family.');
+      $form['instrument_fieldset']['instrument_dropdown']['#disabled'] = TRUE;
+      $form['instrument_fieldset']['submit']['#disabled'] = TRUE;
+    }
+    else {
+      $form['instrument_fieldset']['instrument_dropdown']['#disabled'] = FALSE;
+      $form['instrument_fieldset']['submit']['#disabled'] = FALSE;
+    }
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $trigger = (string) $form_state->getTriggeringElement()['#value'];
+    switch ($trigger) {
+      case 'Submit':
+        // Submit: We're done.
+        drupal_set_message($this->t('Your values have been submitted. Instrument family: @family, Instrument: @instrument', [
+          '@family' => $form_state->getValue('instrument_family_dropdown'),
+          '@instrument' => $form_state->getValue('instrument_dropdown'),
+        ]));
+        return;
+    }
+    // 'Choose' or anything else will cause rebuild of the form and present
+    // it again.
+    $form_state->setRebuild();
+  }
+
+  /**
+   * Provide a new dropdown based on the AJAX call.
+   *
+   * This callback will occur *after* the form has been rebuilt by buildForm().
+   * Since that's the case, the form should contain the right values for
+   * instrument_dropdown.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return array
+   *   The portion of the render structure that will replace the
+   *   instrument-dropdown-replace form element.
+   */
+  public function instrumentDropdownCallback(array $form, FormStateInterface $form_state) {
+    return $form['instrument_fieldset'];
+  }
+
+  /**
+   * Helper function to populate the first dropdown.
+   *
+   * This would normally be pulling data from the database.
+   *
+   * @return array
+   *   Dropdown options.
+   */
+  public static function getFirstDropdownOptions() {
+    return [
+      'none' => 'none',
+      'String' => 'String',
+      'Woodwind' => 'Woodwind',
+      'Brass' => 'Brass',
+      'Percussion' => 'Percussion',
+    ];
+  }
+
+  /**
+   * Helper function to populate the second dropdown.
+   *
+   * This would normally be pulling data from the database.
+   *
+   * @param string $key
+   *   This will determine which set of options is returned.
+   *
+   * @return array
+   *   Dropdown options
+   */
+  public static function getSecondDropdownOptions($key = '') {
+    switch ($key) {
+      case 'String':
+        $options = [
+          'Violin' => 'Violin',
+          'Viola' => 'Viola',
+          'Cello' => 'Cello',
+          'Double Bass' => 'Double Bass',
+        ];
+        break;
+
+      case 'Woodwind':
+        $options = [
+          'Flute' => 'Flute',
+          'Clarinet' => 'Clarinet',
+          'Oboe' => 'Oboe',
+          'Bassoon' => 'Bassoon',
+        ];
+        break;
+
+      case 'Brass':
+        $options = [
+          'Trumpet' => 'Trumpet',
+          'Trombone' => 'Trombone',
+          'French Horn' => 'French Horn',
+          'Euphonium' => 'Euphonium',
+        ];
+        break;
+
+      case 'Percussion':
+        $options = [
+          'Bass Drum' => 'Bass Drum',
+          'Timpani' => 'Timpani',
+          'Snare Drum' => 'Snare Drum',
+          'Tambourine' => 'Tambourine',
+        ];
+        break;
+
+      default:
+        $options = ['none' => 'none'];
+        break;
+    }
+    return $options;
+  }
+
+}
diff --git a/ajax_example/src/Form/DynamicFormSections.php b/ajax_example/src/Form/DynamicFormSections.php
new file mode 100644
index 0000000..3ea8978
--- /dev/null
+++ b/ajax_example/src/Form/DynamicFormSections.php
@@ -0,0 +1,223 @@
+<?php
+
+namespace Drupal\ajax_example\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Link;
+
+/**
+ * Dynamically-enabled form with graceful no-JS degradation.
+ *
+ * Example of a form with portions dynamically enabled or disabled, but with
+ * graceful degradation in the case of no JavaScript.
+ *
+ * The idea here is that certain parts of the form don't need to be displayed
+ * unless a given option is selected, but then they should be displayed and
+ * configured.
+ *
+ * The third $no_js_use argument is strictly for demonstrating operation
+ * without javascript, without making the user/developer turn off javascript.
+ */
+class DynamicFormSections extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'ajax_example_dynamicsectiondegrades';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $nojs = NULL) {
+    // Add our CSS and tiny JS to hide things when they should be hidden.
+    $form['#attached']['library'][] = 'ajax_example/ajax_example.library';
+
+    // Explanatory text with helpful links.
+    $form['info'] = [
+      '#markup' =>
+      $this->t('<p>Like other examples in this module, this form has a path that
+        can be modified with /nojs to simulate its behavior without JavaScript.
+        </p><ul>
+        <li>@try_it_without_ajax</li>
+        <li>@try_it_with_ajax</li>
+      </ul>',
+        [
+          '@try_it_without_ajax' => Link::createFromRoute(
+            $this->t('Try it without AJAX'),
+            'ajax_example.dynamic_form_sections', ['nojs' => 'nojs'])
+            ->toString(),
+          '@try_it_with_ajax' => Link::createFromRoute(
+            $this->t('Try it with AJAX'),
+            'ajax_example.dynamic_form_sections')
+            ->toString(),
+        ]
+      ),
+    ];
+
+    $form['question_type_select'] = [
+      // This is our select dropdown.
+      '#type' => 'select',
+      '#title' => t('Question style'),
+      // We have a variety of form items you can use to get input from the user.
+      '#options' => [
+        'Choose question style' => 'Choose question style',
+        'Multiple Choice' => 'Multiple Choice',
+        'True/False' => 'True/False',
+        'Fill-in-the-blanks' => 'Fill-in-the-blanks',
+      ],
+      // The #ajax section tells the AJAX system that whenever this dropdown
+      // emits an event, it should call the callback and put the resulting
+      // content into the wrapper we specify. The questions-fieldset-wrapper is
+      // defined below.
+      '#ajax' => [
+        'wrapper' => 'questions-fieldset-wrapper',
+        'callback' => '::promptCallback',
+      ],
+    ];
+    // The CSS for this module hides this next button if JS is enabled.
+    $form['question_type_submit'] = [
+      '#type' => 'submit',
+      '#value' => t('Choose'),
+      '#attributes' => ['class' => ['ajax-example-hide']],
+      // No need to validate when submitting this.
+      '#limit_validation_errors' => [],
+      '#validate' => [],
+    ];
+
+    // This simply allows us to demonstrate no-javascript use without
+    // actually turning off javascript in the browser. Removing the #ajax
+    // element turns off AJAX behaviors on that element and as a result
+    // ajax.js doesn't get loaded.
+    if ($nojs == 'nojs') {
+      // Remove #ajax from the above, so it won't perform AJAX behaviors.
+      unset($form['question_type_select']['#ajax']);
+      // Make sure question_type_submit isn't hidden by our JavaScript.
+      unset($form['question_type_submit']['#attributes']);
+    }
+
+    // This fieldset just serves as a container for the part of the form
+    // that gets rebuilt. It has a nice line around it so you can see it.
+    $form['questions_fieldset'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Stuff will appear here'),
+      '#open' => TRUE,
+      // We set the ID of this fieldset to questions-fieldset-wrapper so the
+      // AJAX command can replace it.
+      '#attributes' => ['id' => 'questions-fieldset-wrapper'],
+    ];
+
+    // When the AJAX request comes in, or when the user hit 'Submit' if there is
+    // no JavaScript, the form state will tell us what the user has selected
+    // from the dropdown. We can look at the value of the dropdown to determine
+    // which secondary form to display.
+    if (!empty($form_state->getValue('question_type_select'))) {
+
+      $form['questions_fieldset']['question'] = [
+        '#markup' => t('Who was the first president of the U.S.?'),
+      ];
+      $question_type = $form_state->getValue('question_type_select');
+
+      // Build up a secondary form, based on the type of question the user
+      // chose.
+      switch ($question_type) {
+        case 'Multiple Choice':
+          $form['questions_fieldset']['question'] = [
+            '#type' => 'radios',
+            '#title' => t('Who was the first president of the United States'),
+            '#options' => [
+              'George Bush' => 'George Bush',
+              'Adam McGuire' => 'Adam McGuire',
+              'Abraham Lincoln' => 'Abraham Lincoln',
+              'George Washington' => 'George Washington',
+            ],
+
+          ];
+          break;
+
+        case 'True/False':
+          $form['questions_fieldset']['question'] = [
+            '#type' => 'radios',
+            '#title' => $this->t('Was George Washington the first president of the United States?'),
+            '#options' => [
+              'George Washington' => 'True',
+              0 => 'False',
+            ],
+            '#description' => $this->t('Click "True" if you think George Washington was the first president of the United States.'),
+          ];
+          break;
+
+        case 'Fill-in-the-blanks':
+          $form['questions_fieldset']['question'] = [
+            '#type' => 'textfield',
+            '#title' => $this->t('Who was the first president of the United States'),
+            '#description' => $this->t('Please type the correct answer to the question.'),
+          ];
+          break;
+      }
+
+      $form['questions_fieldset']['submit'] = [
+        '#type' => 'submit',
+        '#value' => $this->t('Submit your answer'),
+      ];
+    }
+    return $form;
+  }
+
+  /**
+   * Final submit handler.
+   *
+   * Reports what values were finally set.
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    // This is only executed when a button is pressed, not when the AJAXfield
+    // select is changed.
+    // Now handle the case of the next, previous, and submit buttons.
+    // Only submit will result in actual submission, all others rebuild.
+    if ($form_state->getValue('question_type_submit') == 'Choose') {
+      $form_state->setValue('question_type_select', $form_state->getUserInput()['question_type_select']);
+      $form_state->setRebuild();
+    }
+
+    if ($form_state->getValue('submit') == 'Submit your answer') {
+      $form_state->setRebuild(FALSE);
+      $answer = $form_state->getValue('question');
+      print_r($answers);
+      // Special handling for the checkbox.
+      if ($answer == 1 && $form['questions_fieldset']['question']['#type'] == 'checkbox') {
+        $answer = $form['questions_fieldset']['question']['#title'];
+      }
+      if ($answer == $this->t('George Washington')) {
+        drupal_set_message($this->t('You got the right answer: @answer', ['@answer' => $answer]));
+      }
+      else {
+        drupal_set_message($this->t('Sorry, your answer (@answer) is wrong', ['@answer' => $answer]));
+      }
+      return;
+    }
+    // Sets the form to be rebuilt after processing.
+    $form_state->setRebuild();
+  }
+
+  /**
+   * Callback for the select element.
+   *
+   * Since the questions_fieldset part of the form has already been built during
+   * the AJAX request, we can return only that part of the form to the AJAX
+   * request, and it will insert that part into questions-fieldset-wrapper.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return array
+   *   The form structure.
+   */
+  public function promptCallback(array $form, FormStateInterface $form_state) {
+    return $form['questions_fieldset'];
+  }
+
+}
diff --git a/ajax_example/src/Form/EntityAutocomplete.php b/ajax_example/src/Form/EntityAutocomplete.php
new file mode 100644
index 0000000..53b0162
--- /dev/null
+++ b/ajax_example/src/Form/EntityAutocomplete.php
@@ -0,0 +1,126 @@
+<?php
+
+namespace Drupal\ajax_example\Form;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Form\FormInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * A simple autocomplete form which looks up usernames.
+ *
+ * @ingroup ajax_example
+ */
+class EntityAutocomplete implements FormInterface, ContainerInjectionInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The entity type manager service.
+   *
+   * We need this for the submit handler.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Container injection factory.
+   *
+   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
+   *   The service discovery container.
+   *
+   * @return self
+   *   The form object.
+   */
+  public static function create(ContainerInterface $container) {
+    $form = new static(
+      $container->get('entity_type.manager')
+    );
+    $form->setStringTranslation($container->get('string_translation'));
+    return $form;
+  }
+
+  /**
+   * Constructor.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager service.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'ajax_example_autocomplete_user';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['info'] = [
+      '#markup' => '<div>' . t("This example uses the entity_autocomplete form "
+        . "element to select users. You'll need a few users on your system for "
+        . "it to make sense.") . '</div>',
+    ];
+
+    // Here we use the delightful entity_autocomplete form element. It allows us
+    // to consistently select entities. See https://www.drupal.org/node/2418529.
+    $form['users'] = [
+      // A type of entity_autocomplete lets Drupal know it should autocomplete
+      // entities.
+      '#type' => 'entity_autocomplete',
+      // We can specify entity types to autocomplete.
+      '#target_type' => 'user',
+      // Specifying #tags as TRUE allows for multiple selections, separated by
+      // commas.
+      '#tags' => TRUE,
+      '#title' => t('Choose a user. Separate with commas.'),
+    ];
+
+    $form['actions'] = [
+      '#type' => 'actions',
+    ];
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Submit'),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * Here we validate and signal an error if there are no users selected.
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    $state_users = $form_state->getValue('users');
+    if (empty($state_users)) {
+      $form_state->setErrorByName('users', 'There were no users selected.');
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * On submit, show the user the names of the users they selected.
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $state_users = $form_state->getValue('users');
+    $users = [];
+    foreach ($state_users as $state_user) {
+      $uid = $state_user['target_id'];
+      $users[] = $this->entityTypeManager->getStorage('user')->load($uid)->getUsername();
+    }
+    drupal_set_message('These are your users: ' . implode(' ', $users));
+  }
+
+}
diff --git a/ajax_example/src/Form/Simplest.php b/ajax_example/src/Form/Simplest.php
new file mode 100644
index 0000000..4449ca5
--- /dev/null
+++ b/ajax_example/src/Form/Simplest.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace Drupal\ajax_example\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * A relatively simple AJAX demonstration form.
+ */
+class Simplest extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'ajax_example_simplest';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['changethis'] = [
+      '#title' => $this->t("Choose something and explain why"),
+      '#type' => 'select',
+      '#options' => [
+        'one' => 'one',
+        'two' => 'two',
+        'three' => 'three',
+      ],
+      '#ajax' => [
+        // #ajax has two required keys: callback and wrapper.
+        // 'callback' is a function that will be called when this element
+        // changes.
+        'callback' => '::promptCallback',
+        // 'wrapper' is the HTML id of the page element that will be replaced.
+        'wrapper' => 'replace_textfield_div',
+        // There are also several optional keys - see AjaxExampleAutoCheckboxes
+        // below for details on 'method', 'effect' and 'speed' and
+        // AjaxExampleDependentDropDown for 'event'.
+      ],
+    ];
+
+    // The 'replace_textfield_div' div will be replace whenever 'changethis' is
+    // updated.
+    $form['replace_textfield'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t("Why"),
+      // The prefix/suffix provide the div that we're replacing, named by
+      // #ajax['wrapper'] above.
+      '#prefix' => '<div id="replace_textfield_div">',
+      '#suffix' => '</div>',
+    ];
+
+    // An AJAX request calls the form builder function for every change.
+    // We can change how we build the form based on $form_state.
+    $value = $form_state->getValue('changethis');
+    if (!empty($value)) {
+      $form['replace_textfield']['#description'] = $this->t("Say why you chose '@value'", ['@value' => $value]);
+    }
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    // No-op. Our form doesn't need a submit handler, because the form is never
+    // submitted. We add the method here so we fulfill FormInterface.
+  }
+
+  /**
+   * Handles switching the available regions based on the selected theme.
+   */
+  public function promptCallback($form, FormStateInterface $form_state) {
+    return $form['replace_textfield'];
+  }
+
+}
diff --git a/ajax_example/src/Form/SubmitDriven.php b/ajax_example/src/Form/SubmitDriven.php
new file mode 100644
index 0000000..c9f042c
--- /dev/null
+++ b/ajax_example/src/Form/SubmitDriven.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Drupal\ajax_example\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Submit a form without a page reload.
+ */
+class SubmitDriven extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'ajax_example_autotextfields';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['box'] = [
+      '#type' => 'markup',
+      '#prefix' => '<div id="box">',
+      '#suffix' => '</div>',
+      '#markup' => '<h1>Initial markup for box</h1>',
+    ];
+
+    $form['submit'] = [
+      '#type' => 'submit',
+      '#ajax' => [
+        'callback' => '::prompt',
+        'wrapper' => 'box',
+      ],
+      '#value' => $this->t('Submit'),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+  }
+
+  /**
+   * Callback for submit_driven example.
+   *
+   * Select the 'box' element, change the markup in it, and return it as a
+   * renderable array.
+   *
+   * @return array
+   *   Renderable array (the box element)
+   */
+  public function prompt(array &$form, FormStateInterface $form_state) {
+    // In most cases, it is recommended that you put this logic in form
+    // generation rather than the callback. Submit driven forms are an
+    // exception, because you may not want to return the form at all.
+    $element = $form['box'];
+    $element['#markup'] = "Clicked submit ({$form_state->getValue('op')}): " . date('c');
+    return $element;
+  }
+
+}
diff --git a/ajax_example/src/Form/Wizard.php b/ajax_example/src/Form/Wizard.php
new file mode 100644
index 0000000..5a41802
--- /dev/null
+++ b/ajax_example/src/Form/Wizard.php
@@ -0,0 +1,231 @@
+<?php
+
+namespace Drupal\ajax_example\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Link;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\HtmlCommand;
+
+/**
+ * AJAX example wizard.
+ */
+class Wizard extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'ajax_example_wizard';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $no_js_use = FALSE) {
+    $url = Url::fromUri('internal:/examples/ajax-example/wizard-nojs');
+    $link = Link::fromTextAndUrl($this->t('examples/ajax-example/wizard-nojs'), $url)
+      ->toString();
+
+    // Prepare link for multiple arguments.
+    $urltwo = Url::fromUri('internal:/examples/ajax-example/wizard');
+    $linktwo = Link::fromTextAndUrl($this->t('examples/ajax-example/wizard'), $urltwo)
+      ->toString();
+
+    // $form['#prefix'] = '<div id="wizard-form-wrapper">';
+    // $form['#suffix'] = '</div>';
+    // We want to deal with hierarchical form values.
+    $form['#tree'] = TRUE;
+    $form['description'] = [
+      '#markup' => t('This example is a step-by-step wizard. The @link does it without page reloads; the @link1 is the same code but simulates a non-javascript environment, showing it with page reloads.', [
+        '@link' => $linktwo,
+        '@link1' => $link,
+      ]),
+    ];
+
+    $form['step'] = [
+      '#type' => 'hidden',
+      '#value' => !empty($form_state->getValue('step')) ? $form_state->getValue('step') : 1,
+    ];
+    print_r($form_state->getValue('step'));
+
+    if ($form['step']['#value'] == 1) {
+      $form['step1'] = [
+        '#type' => 'fieldset',
+        '#title' => $this->t('Step 1: Personal details'),
+      ];
+      $form['step1']['name'] = [
+        '#type' => 'textfield',
+        '#title' => $this->t('Your name'),
+        '#default_value' => empty($form_state->getValue([
+          'step1',
+          'name',
+        ]) ? '' : $form_state->getValue(['step1', 'name'])),
+        '#required' => TRUE,
+      ];
+
+      $form['next'] = [
+        '#type' => 'submit',
+        '#value' => $this->t('Next step'),
+        '#ajax' => [
+          'wrapper' => 'ajax-example-wizard',
+          'callback' => '::prompt',
+        ],
+      ];
+    }
+
+    // This simply allows us to demonstrate no-javascript use without
+    // actually turning off javascript in the browser. Removing the #ajax
+    // element turns off AJAX behaviors on that element and as a result
+    // ajax.js doesn't get loaded.
+    // For demonstration only! You don't need this.
+    if ($no_js_use) {
+      // Remove the #ajax from the above, so ajax.js won't be loaded.
+      // For demonstration only.
+      unset($form['next']['#ajax']);
+      unset($form['prev']['#ajax']);
+    }
+
+    return $form;
+  }
+
+  /**
+   * Wizard callback function.
+   *
+   * @param array $form
+   *   Form API form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   Form API form.
+   *
+   * @return array
+   *   Form array.
+   */
+  public function prompt(array $form, FormStateInterface $form_state) {
+    return $form;
+  }
+
+  /**
+   * Save away the current information.
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    if ($form_state->getTriggeringElement()['#value'] == $this->t('Submit your information')) {
+      $value_message = $this->t('Your information has been submitted:') . ' ';
+      foreach ($form_state->getValue('value') as $step => $values) {
+        $value_message .= "$step: ";
+        foreach ($values as $key => $value) {
+          $value_message .= "$key=$value, ";
+        }
+      }
+      drupal_set_message($value_message);
+      $form_state->setRebuild(FALSE);
+      // Redirect to #action, else return.
+      return;
+    }
+    else {
+      $step = $form_state->getValue('step');
+      // Increment or decrement the step as needed. Recover values if they
+      // exist.
+      if ($form_state->getTriggeringElement()['#value']->__toString() == $this->t('Next step')) {
+        $step++;
+      }
+      elseif ($form_state->getTriggeringElement()['#value']->__toString() == $this->t('Previous step')) {
+        $step--;
+      }
+
+      switch ($step) {
+        case 1:
+          $form['step1'] = [
+            '#type' => 'fieldset',
+            '#title' => $this->t('Step 1: Personal details'),
+          ];
+          $form['step1']['name'] = [
+            '#type' => 'textfield',
+            '#title' => $this->t('Your name'),
+            '#default_value' => empty($form_state->getValue([
+              'step1',
+              'name',
+            ]) ? '' : $form_state->getValue(['step1', 'name'])),
+            '#required' => TRUE,
+          ];
+          $form_state->setValue('step', 1);
+          break;
+
+        case 2:
+          unset($form['step1']);
+          unset($form['next']);
+          $form['step2'] = [
+            '#type' => 'fieldset',
+            '#title' => t('Step 2: Street address info'),
+          ];
+          $form['step2']['address'] = [
+            '#type' => 'textfield',
+            '#title' => $this->t('Your street address'),
+            '#default_value' => empty($form_state->getValue([
+              'step2',
+              'address',
+            ]) ? '' : $form_state->getValue(['step2', 'address'])),
+            '#required' => TRUE,
+          ];
+          $form_state->setValue('step', $step);
+          break;
+
+        case 3:
+
+          $form['step3'] = [
+            '#type' => 'fieldset',
+            '#title' => $this->t('Step 3: City info'),
+          ];
+          $form['step3']['city'] = [
+            '#type' => 'textfield',
+            '#title' => $this->t('Your city'),
+            '#default_value' => empty($form_state->getValue([
+              'step3',
+              'city',
+            ]) ? '' : $form_state->getValue(['step3', 'city'])),
+            '#required' => TRUE,
+          ];
+          $form_state->setValue('step', $step);
+          break;
+      }
+      if ($step == 3) {
+
+        $form['submit'] = [
+          '#type' => 'submit',
+          '#value' => $this->t("Submit your information"),
+        ];
+      }
+      if ($step > 1 && !isset($form['prev'])) {
+        $form['prev'] = [
+          '#type' => 'submit',
+          '#value' => t("Previous step"),
+          // Since all info will be discarded, don't validate on 'prev'.
+          '#limit_validation_errors' => [],
+          // #submit is required to use #limit_validation_errors.
+          '#submit' => ['ajax_example_wizard_submit'],
+          '#ajax' => [
+            'wrapper' => 'ajax-example-wizard',
+            'callback' => '::prompt',
+          ],
+        ];
+      }
+      if ($step < 3 && !isset($form['next'])) {
+        $form['next'] = [
+          '#type' => 'submit',
+          '#value' => $this->t('Next step'),
+          '#limit_validation_errors' => [],
+          '#ajax' => [
+            'wrapper' => 'ajax-example-wizard',
+            'callback' => '::prompt',
+          ],
+        ];
+      }
+      $response = new AjaxResponse();
+      $response->addCommand(new HtmlCommand('#ajax-example-wizard', $form));
+      return $response;
+    }
+
+  }
+
+}
diff --git a/ajax_example/templates/description.html.twig b/ajax_example/templates/description.html.twig
new file mode 100644
index 0000000..76ed8e5
--- /dev/null
+++ b/ajax_example/templates/description.html.twig
@@ -0,0 +1,29 @@
+{#
+
+Description text for the Ajax Example.
+
+#}
+
+{% set simple_ajax_example = path('ajax_example.simplest') %}
+{% set ajax_generate_textfields = path('ajax_example.autotextfields') %}
+{% set ajax_submit = path('ajax_example.submit_driven_ajax') %}
+{% set ajax_dependent_dropdown = path('ajax_example.dependent_dropdown') %}
+{% set ajax_dependent_dropdown_nojs = path('ajax_example.dependent_dropdown', {'nojs': 'nojs'}) %}
+{% set ajax_dynamic_form = path('ajax_example.dynamic_form_sections') %}
+{% set ajax_dynamic_form_nojs = path('ajax_example.dynamic_form_sections', {'nojs': 'nojs'}) %}
+{% set ajax_wizard_example = path('ajax_example.wizard') %}
+{% set ajax_wizard_example_nojs = path('ajax_example.wizardnojs') %}
+
+{% trans %}
+
+<p>The AJAX example module provides many examples of AJAX including forms, links, and AJAX commands.</p>
+<p><a href={{ simple_ajax_example }}>Simplest AJAX Example</a></p>
+<p><a href={{ ajax_generate_textfields }}>Generate textfields</a></p>
+<p><a href={{ ajax_submit }}>Submit-driven AJAX</a></p>
+<p><a href={{ ajax_dependent_dropdown }}>Dependent dropdown</a></p>
+<p><a href={{ ajax_dependent_dropdown_nojs }}>Dependent dropdown w/ no JS</a></p>
+<p><a href={{ ajax_dynamic_form }}>Dynamic form sections</a></p>
+<p><a href={{ ajax_dynamic_form_nojs }}>Dynamic form sections w/ no JS</a></p>
+<p><a href={{ ajax_wizard_example }}>AJAX Wizard Example</a></p>
+<p><a href={{ ajax_wizard_example_nojs }}>AJAX Wizard Example w/JS turned off</a></p>
+{% endtrans %}
diff --git a/ajax_example/tests/src/Functional/AjaxExampleMenuTest.php b/ajax_example/tests/src/Functional/AjaxExampleMenuTest.php
new file mode 100644
index 0000000..2e07d9b
--- /dev/null
+++ b/ajax_example/tests/src/Functional/AjaxExampleMenuTest.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Drupal\Tests\ajax_example\Functional;
+
+use Drupal\Core\Url;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Verify functionalities of ajax_example.
+ *
+ * @group ajax_example
+ * @group examples
+ *
+ * @ingroup ajax_example
+ */
+class AjaxExampleMenuTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['ajax_example'];
+
+  /**
+   * The installation profile to use with this test.
+   *
+   * @var string
+   */
+  protected $profile = 'minimal';
+
+  /**
+   * Tests links.
+   */
+  public function testAjaxExampleLinks() {
+    // Login a user that can access content.
+    $this->drupalLogin(
+      $this->createUser(['access content', 'access user profiles'])
+    );
+
+    $assertion = $this->assertSession();
+
+    // Routes with menu links, and their form buttons.
+    $routes_with_menu_links = [
+      'ajax_example.description' => [],
+      'ajax_example.simplest' => [],
+      'ajax_example.autotextfields' => ['Click Me'],
+      'ajax_example.submit_driven_ajax' => ['Submit'],
+      'ajax_example.dependent_dropdown' => ['Submit'],
+      'ajax_example.dynamic_form_sections' => ['Choose'],
+      'ajax_example.wizard' => ['Next step'],
+      'ajax_example.wizardnojs' => ['Next step'],
+      'ajax_example.ajax_link_render' => [],
+      'ajax_example.autocomplete_user' => ['Submit'],
+    ];
+
+    // Ensure the links appear in the tools menu sidebar.
+    $this->drupalGet('');
+    foreach (array_keys($routes_with_menu_links) as $route) {
+      $assertion->linkByHrefExists(Url::fromRoute($route)->getInternalPath());
+    }
+
+    // All our routes with their form buttons.
+    $routes = [
+      'ajax_example.ajax_link_callback' => [],
+    ];
+
+    // Go to all the routes and click all the buttons.
+    $routes = array_merge($routes_with_menu_links, $routes);
+    foreach ($routes as $route => $buttons) {
+      $path = Url::fromRoute($route);
+      $this->drupalGet($path);
+      $assertion->statusCodeEquals(200);
+      foreach ($buttons as $button) {
+        $this->drupalPostForm($path, [], $button);
+        $assertion->statusCodeEquals(200);
+      }
+    }
+  }
+
+}
diff --git a/ajax_example/tests/src/FunctionalJavascript/EntityAutocompleteTest.php b/ajax_example/tests/src/FunctionalJavascript/EntityAutocompleteTest.php
new file mode 100644
index 0000000..e1588dc
--- /dev/null
+++ b/ajax_example/tests/src/FunctionalJavascript/EntityAutocompleteTest.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Drupal\Tests\ajax_example\FunctionalJavascript;
+
+use Drupal\Core\Url;
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+
+/**
+ * Tests the behavior of the entity_autocomplete example.
+ *
+ * @group ajax_example
+ */
+class EntityAutocompleteTest extends JavascriptTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['ajax_example'];
+
+  /**
+   * Test the behavior of the submit-driven AJAX example.
+   *
+   * Behaviors to test:
+   * - GET the route ajax_example.autocomplete_user.
+   * - Examine the DOM to make sure our change hasn't happened yet.
+   * - Send an event to the DOM to trigger the autocomplete.
+   * - Wait for the autocomplete request to complete.
+   * - Examine the DOM to see if our expected change happened.
+   * - Submit some names to see if our form processed the user properly.
+   */
+  public function testSubmitDriven() {
+    // Set up some accounts with known names.
+    $names = ['bb', 'bc'];
+    foreach ($names as $name) {
+      $this->createUser([], $name);
+    }
+
+    // Get our various Mink elements.
+    $assert = $this->assertSession();
+    $session = $this->getSession();
+    $page = $session->getPage();
+    // We'll be using the users field quite a bit, so let's make it a variable.
+    $users_field_name = 'edit-users';
+
+    // Get the form.
+    $this->drupalGet(Url::fromRoute('ajax_example.autocomplete_user'));
+    // Examine the DOM to make sure our change hasn't happened yet.
+    $assert->fieldValueEquals($users_field_name, '');
+
+    // Send an event to the DOM. This will start the autocomplete process.
+    $autocomplete_field = $page->findField($users_field_name);
+    $session->getDriver()->keyDown($autocomplete_field->getXpath(), 'b');
+
+    // Wait for the autocomplete request to complete.
+    $assert->waitOnAutocomplete();
+
+    // Examine the DOM to see if our expected change happened.
+    $results = $page->findAll('css', '.ui-autocomplete li');
+    $this->assertCount(2, $results);
+    foreach ($results as $result) {
+      $this->assertContains($result->getText(), $names);
+    }
+
+    // Submit to see if our form processed the user properly.
+    $this->submitForm([$users_field_name => 'bb, bc'], 'Submit');
+    $assert->pageTextContains('These are your users: bb bc');
+  }
+
+}
diff --git a/ajax_example/tests/src/FunctionalJavascript/SubmitDrivenTest.php b/ajax_example/tests/src/FunctionalJavascript/SubmitDrivenTest.php
new file mode 100644
index 0000000..e13d199
--- /dev/null
+++ b/ajax_example/tests/src/FunctionalJavascript/SubmitDrivenTest.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Drupal\Tests\ajax_example\FunctionalJavascript;
+
+use Drupal\Core\Url;
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+
+/**
+ * Tests the behavior of the submit-driven AJAX example.
+ *
+ * @group ajax_example
+ */
+class SubmitDrivenTest extends JavascriptTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['ajax_example'];
+
+  /**
+   * Test the behavior of the submit-driven AJAX example.
+   *
+   * Behaviors to test:
+   * - GET the route ajax_example.submit_driven_ajax.
+   * - Examine the DOM to make sure our change hasn't happened yet.
+   * - Submit the form.
+   * - Wait for the AJAX request to complete.
+   * - Examine the DOM to see if our expected change happened.
+   */
+  public function testSubmitDriven() {
+    // Get the session assertion object.
+    $assert = $this->assertSession();
+    // Get the page.
+    $this->drupalGet(Url::fromRoute('ajax_example.submit_driven_ajax'));
+    // Examine the DOM to make sure our change hasn't happened yet.
+    $assert->pageTextNotContains('Clicked submit (Submit):');
+    // Submit the form.
+    $this->submitForm([], 'Submit');
+    // Wait on the AJAX request.
+    $assert->assertWaitOnAjaxRequest();
+    // Compare DOM to our expectations.
+    $assert->pageTextContains('Clicked submit (Submit):');
+  }
+
+}
diff --git a/examples.module b/examples.module
index 7a6b6a9..1f7cd9a 100644
--- a/examples.module
+++ b/examples.module
@@ -35,6 +35,7 @@ function examples_toolbar() {
   // First, build an array of all example modules and their routes.
   // We resort to this hard-coded way so as not to muck up each example.
   $examples = [
+    'ajax_example' => 'ajax_example.description',
     'batch_example' => 'batch_example.form',
     'block_example' => 'block_example.description',
     'cache_example' => 'cache_example.description',
