diff --git a/src/Element/DynamicEntityAutocomplete.php b/src/Element/DynamicEntityAutocomplete.php
new file mode 100644
index 0000000..735ffc5
--- /dev/null
+++ b/src/Element/DynamicEntityAutocomplete.php
@@ -0,0 +1,363 @@
+<?php
+
+namespace Drupal\dynamic_entity_reference\Element;
+
+use Drupal\Component\Utility\Crypt;
+use Drupal\Component\Utility\Tags;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface;
+use Drupal\Core\Entity\EntityReferenceSelection\SelectionWithAutocreateInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element\Textfield;
+use Drupal\Core\Site\Settings;
+
+/**
+ * Provides an dynamic entity autocomplete form element.
+ *
+ * The #default_value accepted by this element is either an entity object or an
+ * array of entity objects.
+ *
+ * @FormElement("dynamic_entity_autocomplete")
+ */
+class DynamicEntityAutocomplete extends Textfield {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    $info = parent::getInfo();
+    $class = get_class($this);
+
+    // Apply default form element properties.
+    $info['#target_types'] = [];
+    // Handler setting of all the target types.
+    $info['#settings'] = [];
+    $info['#autocreate_settings'] = [];
+    // This should only be set to FALSE if proper validation by the selection
+    // handler is performed at another level on the extracted form values.
+    $info['#validate_reference'] = TRUE;
+    // IMPORTANT! This should only be set to FALSE if the #default_value
+    // property is processed at another level (e.g. by a Field API widget) and
+    // it's value is properly checked for access.
+    $info['#process_default_value'] = TRUE;
+
+    $info['#element_validate'] = array(array($class, 'validateDynamicEntityAutocomplete'));
+    array_unshift($info['#process'], array($class, 'processDynamicEntityAutocomplete'));
+
+    return $info;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
+    // Process the #default_value property.
+    if ($input === FALSE && isset($element['#default_value']) && $element['#process_default_value']) {
+      if (is_array($element['#default_value']) && $element['#tags'] !== TRUE) {
+        throw new \InvalidArgumentException('The #default_value property is an array but the form element does not allow multiple values.');
+      }
+      elseif (!empty($element['#default_value']) && !is_array($element['#default_value'])) {
+        // Convert the default value into an array for easier processing in
+        // static::getEntityLabels().
+        $element['#default_value'] = array($element['#default_value']);
+      }
+
+      if ($element['#default_value']) {
+        if (!(reset($element['#default_value']) instanceof EntityInterface)) {
+          throw new \InvalidArgumentException('The #default_value property has to be an entity object or an array of entity objects.');
+        }
+
+        // Extract the labels from the passed-in entity objects, taking access
+        // checks into account.
+        return static::getEntityLabels($element['#default_value']);
+      }
+    }
+
+    // Potentially the #value is set directly, so it contains the 'target_id'
+    // array structure instead of a string.
+    if ($input !== FALSE && is_array($input)) {
+      $entity_ids = array_map(function(array $item) {
+        return $item['target_id'];
+      }, $input);
+
+      $entities = \Drupal::entityTypeManager()->getStorage($element['#target_type'])->loadMultiple($entity_ids);
+
+      return static::getEntityLabels($entities);
+    }
+  }
+
+  /**
+   * Adds entity autocomplete functionality to a form element.
+   *
+   * @param array $element
+   *   The form element to process. Properties used:
+   *   - #target_type: The ID of the target entity type.
+   *   - #selection_handler: The plugin ID of the entity reference selection
+   *     handler.
+   *   - #selection_settings: An array of settings that will be passed to the
+   *     selection handler.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   * @param array $complete_form
+   *   The complete form structure.
+   *
+   * @return array
+   *   The form element.
+   *
+   * @throws \InvalidArgumentException
+   *   Exception thrown when the #target_type or #autocreate['bundle'] are
+   *   missing.
+   */
+  public static function processDynamicEntityAutocomplete(array &$element, FormStateInterface $form_state, array &$complete_form) {
+    // Nothing to do if there is no target entity type.
+    if (empty($element['#target_types'])) {
+      throw new \InvalidArgumentException('Missing required #target_type parameter.');
+    }
+
+    // Provide default values and sanity checks for the #autocreate parameter.
+    if ($element['#autocreate']) {
+      if (!isset($element['#autocreate']['bundle'])) {
+        throw new \InvalidArgumentException("Missing required #autocreate['bundle'] parameter.");
+      }
+      // Default the autocreate user ID to the current user.
+      $element['#autocreate']['uid'] = isset($element['#autocreate']['uid']) ? $element['#autocreate']['uid'] : \Drupal::currentUser()->id();
+    }
+
+    // Store the selection settings in the key/value store and pass a hashed key
+    // in the route parameters.
+    $selection_settings = isset($element['#selection_settings']) ? $element['#selection_settings'] : [];
+    $data = serialize($selection_settings) . $element['#target_type'] . $element['#selection_handler'];
+    $selection_settings_key = Crypt::hmacBase64($data, Settings::getHashSalt());
+
+    $key_value_storage = \Drupal::keyValue('entity_autocomplete');
+    if (!$key_value_storage->has($selection_settings_key)) {
+      $key_value_storage->set($selection_settings_key, $selection_settings);
+    }
+
+    $element['#autocomplete_route_name'] = 'system.entity_autocomplete';
+    $element['#autocomplete_route_parameters'] = array(
+      'target_type' => $element['#target_type'],
+      'selection_handler' => $element['#selection_handler'],
+      'selection_settings_key' => $selection_settings_key,
+    );
+
+    return $element;
+  }
+
+  /**
+   * Form element validation handler for entity_autocomplete elements.
+   */
+  public static function validateDynamicEntityAutocomplete(array &$element, FormStateInterface $form_state, array &$complete_form) {
+    $value = NULL;
+
+    if (!empty($element['#value'])) {
+      $options = array(
+        'target_type' => $element['#target_type'],
+        'handler' => $element['#selection_handler'],
+        'handler_settings' => $element['#selection_settings'],
+      );
+      /** @var /Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface $handler */
+      $handler = \Drupal::service('plugin.manager.entity_reference_selection')->getInstance($options);
+      $autocreate = (bool) $element['#autocreate'] && $handler instanceof SelectionWithAutocreateInterface;
+
+      // GET forms might pass the validated data around on the next request, in
+      // which case it will already be in the expected format.
+      if (is_array($element['#value'])) {
+        $value = $element['#value'];
+      }
+      else {
+        $input_values = $element['#tags'] ? Tags::explode($element['#value']) : array($element['#value']);
+
+        foreach ($input_values as $input) {
+          $match = static::extractEntityIdFromAutocompleteInput($input);
+          if ($match === NULL) {
+            // Try to get a match from the input string when the user didn't use
+            // the autocomplete but filled in a value manually.
+            $match = static::matchEntityByTitle($handler, $input, $element, $form_state, !$autocreate);
+          }
+
+          if ($match !== NULL) {
+            $value[] = array(
+              'target_id' => $match,
+            );
+          }
+          elseif ($autocreate) {
+            /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionWithAutocreateInterface $handler */
+            // Auto-create item. See an example of how this is handled in
+            // \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::presave().
+            $value[] = array(
+              'entity' => $handler->createNewEntity($element['#target_type'], $element['#autocreate']['bundle'], $input, $element['#autocreate']['uid']),
+            );
+          }
+        }
+      }
+
+      // Check that the referenced entities are valid, if needed.
+      if ($element['#validate_reference'] && !empty($value)) {
+        // Validate existing entities.
+        $ids = array_reduce($value, function ($return, $item) {
+          if (isset($item['target_id'])) {
+            $return[] = $item['target_id'];
+          }
+          return $return;
+        });
+
+        if ($ids) {
+          $valid_ids = $handler->validateReferenceableEntities($ids);
+          if ($invalid_ids = array_diff($ids, $valid_ids)) {
+            foreach ($invalid_ids as $invalid_id) {
+              $form_state->setError($element, t('The referenced entity (%type: %id) does not exist.', array('%type' => $element['#target_type'], '%id' => $invalid_id)));
+            }
+          }
+        }
+
+        // Validate newly created entities.
+        $new_entities = array_reduce($value, function ($return, $item) {
+          if (isset($item['entity'])) {
+            $return[] = $item['entity'];
+          }
+          return $return;
+        });
+
+        if ($new_entities) {
+          if ($autocreate) {
+            $valid_new_entities = $handler->validateReferenceableNewEntities($new_entities);
+            $invalid_new_entities = array_diff_key($new_entities, $valid_new_entities);
+          }
+          else {
+            // If the selection handler does not support referencing newly
+            // created entities, all of them should be invalidated.
+            $invalid_new_entities = $new_entities;
+          }
+
+          foreach ($invalid_new_entities as $entity) {
+            /** @var \Drupal\Core\Entity\EntityInterface $entity */
+            $form_state->setError($element, t('This entity (%type: %label) cannot be referenced.', array('%type' => $element['#target_type'], '%label' => $entity->label())));
+          }
+        }
+      }
+
+      // Use only the last value if the form element does not support multiple
+      // matches (tags).
+      if (!$element['#tags'] && !empty($value)) {
+        $last_value = $value[count($value) - 1];
+        $value = isset($last_value['target_id']) ? $last_value['target_id'] : $last_value;
+      }
+    }
+
+    $form_state->setValueForElement($element, $value);
+  }
+
+  /**
+   * Finds an entity from an autocomplete input without an explicit ID.
+   *
+   * The method will return an entity ID if one single entity unambuguously
+   * matches the incoming input, and sill assign form errors otherwise.
+   *
+   * @param \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface $handler
+   *   Entity reference selection plugin.
+   * @param string $input
+   *   Single string from autocomplete element.
+   * @param array $element
+   *   The form element to set a form error.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current form state.
+   * @param bool $strict
+   *   Whether to trigger a form error if an element from $input (eg. an entity)
+   *   is not found.
+   *
+   * @return int|null
+   *   Value of a matching entity ID, or NULL if none.
+   */
+  protected static function matchEntityByTitle(SelectionInterface $handler, $input, array &$element, FormStateInterface $form_state, $strict) {
+    $entities_by_bundle = $handler->getReferenceableEntities($input, '=', 6);
+    $entities = array_reduce($entities_by_bundle, function ($flattened, $bundle_entities) {
+      return $flattened + $bundle_entities;
+    }, []);
+    $params = array(
+      '%value' => $input,
+      '@value' => $input,
+    );
+    if (empty($entities)) {
+      if ($strict) {
+        // Error if there are no entities available for a required field.
+        $form_state->setError($element, t('There are no entities matching "%value".', $params));
+      }
+    }
+    elseif (count($entities) > 5) {
+      $params['@id'] = key($entities);
+      // Error if there are more than 5 matching entities.
+      $form_state->setError($element, t('Many entities are called %value. Specify the one you want by appending the id in parentheses, like "@value (@id)".', $params));
+    }
+    elseif (count($entities) > 1) {
+      // More helpful error if there are only a few matching entities.
+      $multiples = array();
+      foreach ($entities as $id => $name) {
+        $multiples[] = $name . ' (' . $id . ')';
+      }
+      $params['@id'] = $id;
+      $form_state->setError($element, t('Multiple entities match this reference; "%multiple". Specify the one you want by appending the id in parentheses, like "@value (@id)".', array('%multiple' => implode('", "', $multiples))));
+    }
+    else {
+      // Take the one and only matching entity.
+      return key($entities);
+    }
+  }
+
+  /**
+   * Converts an array of entity objects into a string of entity labels.
+   *
+   * This method is also responsible for checking the 'view label' access on the
+   * passed-in entities.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface[] $entities
+   *   An array of entity objects.
+   *
+   * @return string
+   *   A string of entity labels separated by commas.
+   */
+  public static function getEntityLabels(array $entities) {
+    $entity_labels = array();
+    foreach ($entities as $entity) {
+      // Use the special view label, since some entities allow the label to be
+      // viewed, even if the entity is not allowed to be viewed.
+      $label = ($entity->access('view label')) ? $entity->label() : t('- Restricted access -');
+
+      // Take into account "autocreated" entities.
+      if (!$entity->isNew()) {
+        $label .= ' (' . $entity->id() . ')';
+      }
+
+      // Labels containing commas or quotes must be wrapped in quotes.
+      $entity_labels[] = Tags::encode($label);
+    }
+
+    return implode(', ', $entity_labels);
+  }
+
+  /**
+   * Extracts the entity ID from the autocompletion result.
+   *
+   * @param string $input
+   *   The input coming from the autocompletion result.
+   *
+   * @return mixed|null
+   *   An entity ID or NULL if the input does not contain one.
+   */
+  public static function extractEntityIdFromAutocompleteInput($input) {
+    $match = NULL;
+
+    // Take "label (entity id)', match the ID from parenthesis when it's a
+    // number.
+    if (preg_match("/.+\s\((\d+)\)/", $input, $matches)) {
+      $match = $matches[1];
+    }
+    // Match the ID when it's a string (e.g. for config entity types).
+    elseif (preg_match("/.+\s\(([\w.]+)\)/", $input, $matches)) {
+      $match = $matches[1];
+    }
+
+    return $match;
+  }
+
+}
diff --git a/tests/src/Kernel/DynamicEntityAutocompleteElementFormTest.php b/tests/src/Kernel/DynamicEntityAutocompleteElementFormTest.php
new file mode 100644
index 0000000..7db14cd
--- /dev/null
+++ b/tests/src/Kernel/DynamicEntityAutocompleteElementFormTest.php
@@ -0,0 +1,362 @@
+<?php
+
+namespace Drupal\Tests\dynamic_entity_reference\Kernel;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\FormInterface;
+use Drupal\Core\Form\FormState;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\dynamic_entity_reference\Element\DynamicEntityAutocomplete;
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
+use Drupal\user\Entity\User;
+
+/**
+ * Tests the EntityAutocomplete Form API element.
+ *
+ * @group Form
+ */
+class DynamicEntityAutocompleteElementFormTest extends EntityKernelTestBase implements FormInterface {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['dynamic_entity_reference'];
+
+  /**
+   * User for testing.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $testUser;
+
+  /**
+   * User for autocreate testing.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $testAutocreateUser;
+
+  /**
+   * An array of entities to be referenced in this test.
+   *
+   * @var \Drupal\Core\Entity\EntityInterface[]
+   */
+  protected $referencedEntities;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installSchema('system', ['key_value_expire']);
+    \Drupal::service('router.builder')->rebuild();
+
+    $this->testUser = User::create(array(
+      'name' => 'foobar1',
+      'mail' => 'foobar1@example.com',
+    ));
+    $this->testUser->save();
+    \Drupal::service('current_user')->setAccount($this->testUser);
+
+    $this->testAutocreateUser = User::create(array(
+      'name' => 'foobar2',
+      'mail' => 'foobar2@example.com',
+    ));
+    $this->testAutocreateUser->save();
+
+    for ($i = 1; $i < 3; $i++) {
+      $entity = EntityTest::create(array(
+        'name' => $this->randomMachineName(),
+      ));
+      $entity->save();
+      $this->referencedEntities[] = $entity;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'test_dynamic_entity_autocomplete';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['single'] = array(
+      '#type' => 'dynamic_entity_autocomplete',
+      '#target_type' => 'entity_test',
+    );
+    $form['single_autocreate'] = array(
+      '#type' => 'dynamic_entity_autocomplete',
+      '#target_type' => 'entity_test',
+      '#autocreate' => array(
+        'bundle' => 'entity_test',
+      ),
+    );
+    $form['single_autocreate_specific_uid'] = array(
+      '#type' => 'dynamic_entity_autocomplete',
+      '#target_type' => 'entity_test',
+      '#autocreate' => array(
+        'bundle' => 'entity_test',
+        'uid' => $this->testAutocreateUser->id(),
+      ),
+    );
+
+    $form['tags'] = array(
+      '#type' => 'dynamic_entity_autocomplete',
+      '#target_type' => 'entity_test',
+      '#tags' => TRUE,
+    );
+    $form['tags_autocreate'] = array(
+      '#type' => 'dynamic_entity_autocomplete',
+      '#target_type' => 'entity_test',
+      '#tags' => TRUE,
+      '#autocreate' => array(
+        'bundle' => 'entity_test',
+      ),
+    );
+    $form['tags_autocreate_specific_uid'] = array(
+      '#type' => 'dynamic_entity_autocomplete',
+      '#target_type' => 'entity_test',
+      '#tags' => TRUE,
+      '#autocreate' => array(
+        'bundle' => 'entity_test',
+        'uid' => $this->testAutocreateUser->id(),
+      ),
+    );
+
+    $form['single_no_validate'] = array(
+      '#type' => 'dynamic_entity_autocomplete',
+      '#target_type' => 'entity_test',
+      '#validate_reference' => FALSE,
+    );
+    $form['single_autocreate_no_validate'] = array(
+      '#type' => 'dynamic_entity_autocomplete',
+      '#target_type' => 'entity_test',
+      '#validate_reference' => FALSE,
+      '#autocreate' => array(
+        'bundle' => 'entity_test',
+      ),
+    );
+
+    $form['single_access'] = array(
+      '#type' => 'dynamic_entity_autocomplete',
+      '#target_type' => 'entity_test',
+      '#default_value' => $this->referencedEntities[0],
+    );
+    $form['tags_access'] = array(
+      '#type' => 'dynamic_entity_autocomplete',
+      '#target_type' => 'entity_test',
+      '#tags' => TRUE,
+      '#default_value' => array($this->referencedEntities[0], $this->referencedEntities[1]),
+    );
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) { }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) { }
+
+  /**
+   * Tests valid entries in the EntityAutocomplete Form API element.
+   */
+  public function testValidEntityAutocompleteElement() {
+    $form_state = (new FormState())
+      ->setValues([
+        'single' => $this->getAutocompleteInput($this->referencedEntities[0]),
+        'single_autocreate' => 'single - autocreated entity label',
+        'single_autocreate_specific_uid' => 'single - autocreated entity label with specific uid',
+        'tags' => $this->getAutocompleteInput($this->referencedEntities[0]) . ', ' . $this->getAutocompleteInput($this->referencedEntities[1]),
+        'tags_autocreate' =>
+          $this->getAutocompleteInput($this->referencedEntities[0])
+          . ', tags - autocreated entity label, '
+          . $this->getAutocompleteInput($this->referencedEntities[1]),
+        'tags_autocreate_specific_uid' =>
+          $this->getAutocompleteInput($this->referencedEntities[0])
+          . ', tags - autocreated entity label with specific uid, '
+          . $this->getAutocompleteInput($this->referencedEntities[1]),
+      ]);
+    $form_builder = $this->container->get('form_builder');
+    $form_builder->submitForm($this, $form_state);
+
+    // Valid form state.
+    $this->assertEquals(count($form_state->getErrors()), 0);
+
+    // Test the 'single' element.
+    $this->assertEquals($form_state->getValue('single'), $this->referencedEntities[0]->id());
+
+    // Test the 'single_autocreate' element.
+    $value = $form_state->getValue('single_autocreate');
+    $this->assertEquals($value['entity']->label(), 'single - autocreated entity label');
+    $this->assertEquals($value['entity']->bundle(), 'entity_test');
+    $this->assertEquals($value['entity']->getOwnerId(), $this->testUser->id());
+
+    // Test the 'single_autocreate_specific_uid' element.
+    $value = $form_state->getValue('single_autocreate_specific_uid');
+    $this->assertEquals($value['entity']->label(), 'single - autocreated entity label with specific uid');
+    $this->assertEquals($value['entity']->bundle(), 'entity_test');
+    $this->assertEquals($value['entity']->getOwnerId(), $this->testAutocreateUser->id());
+
+    // Test the 'tags' element.
+    $expected = array(
+      array('target_id' => $this->referencedEntities[0]->id()),
+      array('target_id' => $this->referencedEntities[1]->id()),
+    );
+    $this->assertEquals($form_state->getValue('tags'), $expected);
+
+    // Test the 'single_autocreate' element.
+    $value = $form_state->getValue('tags_autocreate');
+    // First value is an existing entity.
+    $this->assertEquals($value[0]['target_id'], $this->referencedEntities[0]->id());
+    // Second value is an autocreated entity.
+    $this->assertTrue(!isset($value[1]['target_id']));
+    $this->assertEquals($value[1]['entity']->label(), 'tags - autocreated entity label');
+    $this->assertEquals($value[1]['entity']->getOwnerId(), $this->testUser->id());
+    // Third value is an existing entity.
+    $this->assertEquals($value[2]['target_id'], $this->referencedEntities[1]->id());
+
+    // Test the 'tags_autocreate_specific_uid' element.
+    $value = $form_state->getValue('tags_autocreate_specific_uid');
+    // First value is an existing entity.
+    $this->assertEquals($value[0]['target_id'], $this->referencedEntities[0]->id());
+    // Second value is an autocreated entity.
+    $this->assertTrue(!isset($value[1]['target_id']));
+    $this->assertEquals($value[1]['entity']->label(), 'tags - autocreated entity label with specific uid');
+    $this->assertEquals($value[1]['entity']->getOwnerId(), $this->testAutocreateUser->id());
+    // Third value is an existing entity.
+    $this->assertEquals($value[2]['target_id'], $this->referencedEntities[1]->id());
+  }
+
+  /**
+   * Tests invalid entries in the EntityAutocomplete Form API element.
+   */
+  public function testInvalidEntityAutocompleteElement() {
+    $form_builder = $this->container->get('form_builder');
+
+    // Test 'single' with a entity label that doesn't exist
+    $form_state = (new FormState())
+      ->setValues([
+        'single' => 'single - non-existent label',
+      ]);
+    $form_builder->submitForm($this, $form_state);
+    $this->assertEquals(count($form_state->getErrors()), 1);
+    $this->assertEquals($form_state->getErrors()['single'], t('There are no entities matching "%value".', array('%value' => 'single - non-existent label')));
+
+    // Test 'single' with a entity ID that doesn't exist.
+    $form_state = (new FormState())
+      ->setValues([
+        'single' => 'single - non-existent label (42)',
+      ]);
+    $form_builder->submitForm($this, $form_state);
+    $this->assertEquals(count($form_state->getErrors()), 1);
+    $this->assertEquals($form_state->getErrors()['single'], t('The referenced entity (%type: %id) does not exist.', array('%type' => 'entity_test', '%id' => 42)));
+
+    // Do the same tests as above but on an element with '#validate_reference'
+    // set to FALSE.
+    $form_state = (new FormState())
+      ->setValues([
+        'single_no_validate' => 'single - non-existent label',
+        'single_autocreate_no_validate' => 'single - autocreate non-existent label',
+      ]);
+    $form_builder->submitForm($this, $form_state);
+
+    // The element without 'autocreate' support still has to emit a warning when
+    // the input doesn't end with an entity ID enclosed in parentheses.
+    $this->assertEquals(count($form_state->getErrors()), 1);
+    $this->assertEquals($form_state->getErrors()['single_no_validate'], t('There are no entities matching "%value".', array('%value' => 'single - non-existent label')));
+
+    $form_state = (new FormState())
+      ->setValues([
+        'single_no_validate' => 'single - non-existent label (42)',
+        'single_autocreate_no_validate' => 'single - autocreate non-existent label (43)',
+      ]);
+    $form_builder->submitForm($this, $form_state);
+
+    // The input is complete (i.e. contains an entity ID at the end), no errors
+    // are triggered.
+    $this->assertEquals(count($form_state->getErrors()), 0);
+  }
+
+  /**
+   * Tests that access is properly checked by the EntityAutocomplete element.
+   */
+  public function testEntityAutocompleteAccess() {
+    $form_builder = $this->container->get('form_builder');
+    $form = $form_builder->getForm($this);
+
+    // Check that the current user has proper access to view entity labels.
+    $expected = $this->referencedEntities[0]->label() . ' (' . $this->referencedEntities[0]->id() . ')';
+    $this->assertEquals($form['single_access']['#value'], $expected);
+
+    $expected .= ', ' . $this->referencedEntities[1]->label() . ' (' . $this->referencedEntities[1]->id() . ')';
+    $this->assertEquals($form['tags_access']['#value'], $expected);
+
+    // Set up a non-admin user that is *not* allowed to view test entities.
+    \Drupal::currentUser()->setAccount($this->createUser(array(), array()));
+
+    // Rebuild the form.
+    $form = $form_builder->getForm($this);
+
+    $expected = t('- Restricted access -');
+    $expected .= ' (' . $this->referencedEntities[0]->id() . ')';
+    $this->assertEquals($form['single_access']['#value'], $expected);
+
+    $expected .= ', ';
+    $expected .= t('- Restricted access -');
+    $expected .= ' (' . $this->referencedEntities[1]->id() . ')';
+    $this->assertEquals($form['tags_access']['#value'], $expected);
+  }
+
+  /**
+   * Tests ID input is handled correctly.
+   *
+   * E.g. This can happen with GET form parameters.
+   */
+  public function testEntityAutocompleteIdInput() {
+    /** @var \Drupal\Core\Form\FormBuilderInterface $form_builder */
+    $form_builder = $this->container->get('form_builder');
+    //$form = $form_builder->getForm($this);
+    $form_state = (new FormState())
+      ->setMethod('GET')
+      ->setValues([
+        'single' => [['target_id' => $this->referencedEntities[0]->id()]],
+        'single_no_validate' => [['target_id' => $this->referencedEntities[0]->id()]],
+      ]);
+
+    $form_builder->submitForm($this, $form_state);
+
+    $form = $form_state->getCompleteForm();
+
+    $expected_label = $this->getAutocompleteInput($this->referencedEntities[0]);
+    $this->assertSame($expected_label, $form['single']['#value']);
+    $this->assertSame($expected_label, $form['single_no_validate']['#value']);
+  }
+
+  /**
+   * Returns an entity label in the format needed by the EntityAutocomplete
+   * element.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   A Drupal entity.
+   *
+   * @return string
+   *   A string that can be used as a value for EntityAutocomplete elements.
+   */
+  protected function getAutocompleteInput(EntityInterface $entity) {
+    return DynamicEntityAutocomplete::getEntityLabels(array($entity));
+  }
+
+}
