diff --git a/core/lib/Drupal/Core/Entity/ContentEntityForm.php b/core/lib/Drupal/Core/Entity/ContentEntityForm.php
index 35834ab..c0882a2 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityForm.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityForm.php
@@ -50,6 +50,12 @@ public static function create(ContainerInterface $container) {
    */
   public function form(array $form, FormStateInterface $form_state) {
     $form = parent::form($form, $form_state);
+
+    // Content entity forms only need to rebuild the entity in the validation
+    // and the submit handler because Field API uses its own #after_build
+    // callback for its widgets.
+    unset($form['#after_build']);
+
     $this->getFormDisplay($form_state)->buildForm($this->entity, $form, $form_state);
     // Allow modules to act before and after form language is updated.
     $form['#entity_builders']['update_form_langcode'] = [$this, 'updateFormLangcode'];
diff --git a/core/lib/Drupal/Core/Entity/EntityForm.php b/core/lib/Drupal/Core/Entity/EntityForm.php
index 1a137c4..d5b2c7a 100644
--- a/core/lib/Drupal/Core/Entity/EntityForm.php
+++ b/core/lib/Drupal/Core/Entity/EntityForm.php
@@ -131,13 +131,13 @@ protected function init(FormStateInterface $form_state) {
   /**
    * Returns the actual form array to be built.
    *
-   * @see \Drupal\Core\Entity\EntityForm::build()
+   * @see \Drupal\Core\Entity\EntityForm::processForm()
+   * @see \Drupal\Core\Entity\EntityForm::afterBuild()
    */
   public function form(array $form, FormStateInterface $form_state) {
-    $entity = $this->entity;
-
-    // Add a process callback.
+    // Add #process and #after_build callbacks.
     $form['#process'][] = '::processForm';
+    $form['#after_build'][] = '::afterBuild';
 
     return $form;
   }
@@ -156,6 +156,22 @@ public function processForm($element, FormStateInterface $form_state, $form) {
   }
 
   /**
+   * Form element #after_build callback: Updates the entity with submitted data.
+   *
+   * This is the entity object builder function that allows rebuilt forms (e.g.
+   * submitted via AJAX) to keep relying on entity values instead of form state.
+   */
+  public function afterBuild(array $element, FormStateInterface $form_state) {
+    // If the form is processing user input, rebuild the entity with the current
+    // form values.
+    if ($form_state->isProcessingInput()) {
+      $this->entity = $this->buildEntity($element, $form_state);
+    }
+
+    return $element;
+  }
+
+  /**
    * Returns the action form element for the current entity form.
    */
   protected function actionsElement(array $form, FormStateInterface $form_state) {
diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index b67a37b..8eb78dd 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -801,8 +801,8 @@ public function doBuildForm($form_id, &$element, FormStateInterface &$form_state
     // The #after_build flag allows any piece of a form to be altered
     // after normal input parsing has been completed.
     if (isset($element['#after_build']) && !isset($element['#after_build_done'])) {
-      foreach ($element['#after_build'] as $callable) {
-        $element = call_user_func_array($callable, array($element, &$form_state));
+      foreach ($element['#after_build'] as $callback) {
+        $element = call_user_func_array($form_state->prepareCallback($callback), array($element, &$form_state));
       }
       $element['#after_build_done'] = TRUE;
     }
diff --git a/core/modules/config/src/Tests/ConfigEntityTest.php b/core/modules/config/src/Tests/ConfigEntityTest.php
index 7cf16e6..ac93e87 100644
--- a/core/modules/config/src/Tests/ConfigEntityTest.php
+++ b/core/modules/config/src/Tests/ConfigEntityTest.php
@@ -319,6 +319,44 @@ function testCRUDUI() {
     $this->drupalPostForm('admin/structure/config_test/manage/0/delete', array(), 'Delete');
     $this->assertFalse(entity_load('config_test', '0'), 'Test entity deleted');
 
+    // Create a configuration entity with a property that uses AJAX to show
+    // extra form elements.
+    $this->drupalGet('admin/structure/config_test/add');
+
+    // Test that the dependent element is not shown initially.
+    $this->assertFieldByName('size');
+    $this->assertNoFieldByName('size_value');
+
+    $id = strtolower($this->randomMachineName());
+    $edit = [
+      'id' => $id,
+      'label' => $this->randomString(),
+      'size' => 'custom',
+    ];
+    $this->drupalPostAjaxForm(NULL, $edit, 'size');
+
+    // Check that the dependent element is shown after selecting a 'size' value.
+    $this->assertFieldByName('size');
+    $this->assertFieldByName('size_value');
+
+    // Test the same scenario but it in a non-JS case by using a 'js-hidden'
+    // submit button.
+    $this->drupalGet('admin/structure/config_test/add');
+    $this->assertFieldByName('size');
+    $this->assertNoFieldByName('size_value');
+
+    $this->drupalPostForm(NULL, $edit, 'Change size');
+    $this->assertFieldByName('size');
+    $this->assertFieldByName('size_value');
+
+    // Submit the form with the regular 'Save' button and check that the entity
+    // values are correct.
+    $edit += ['size_value' => 'medium'];
+    $this->drupalPostForm(NULL, $edit, 'Save');
+
+    $entity = entity_load('config_test', $id);
+    $this->assertEqual($entity->get('size'), 'custom');
+    $this->assertEqual($entity->get('size_value'), 'medium');
   }
 
 }
diff --git a/core/modules/config/src/Tests/ConfigImportUITest.php b/core/modules/config/src/Tests/ConfigImportUITest.php
index 9bc30e3..4f3aea5 100644
--- a/core/modules/config/src/Tests/ConfigImportUITest.php
+++ b/core/modules/config/src/Tests/ConfigImportUITest.php
@@ -68,6 +68,8 @@ function testImport() {
       'label' => 'New',
       'weight' => 0,
       'style' => '',
+      'size' => '',
+      'size_value' => '',
       'protected_property' => '',
     );
     $staging->write($dynamic_name, $original_dynamic_data);
@@ -373,7 +375,9 @@ function testImportErrorLog() {
       'label' => 'Primary',
       'weight' => 0,
       'style' => NULL,
-      'protected_property' => null,
+      'size' => NULL,
+      'size_value' => NULL,
+      'protected_property' => NULL,
     );
     $staging->write($name_primary, $values_primary);
     $values_secondary = array(
@@ -388,7 +392,9 @@ function testImportErrorLog() {
       'label' => 'Secondary Sync',
       'weight' => 0,
       'style' => NULL,
-      'protected_property' => null,
+      'size' => NULL,
+      'size_value' => NULL,
+      'protected_property' => NULL,
     );
     $staging->write($name_secondary, $values_secondary);
     // Verify that there are configuration differences to import.
diff --git a/core/modules/config/src/Tests/ConfigImporterTest.php b/core/modules/config/src/Tests/ConfigImporterTest.php
index 00c5a1f..1be23de 100644
--- a/core/modules/config/src/Tests/ConfigImporterTest.php
+++ b/core/modules/config/src/Tests/ConfigImporterTest.php
@@ -173,6 +173,8 @@ function testNew() {
       'label' => 'New',
       'weight' => 0,
       'style' => '',
+      'size' => '',
+      'size_value' => '',
       'protected_property' => '',
     );
     $staging->write($dynamic_name, $original_dynamic_data);
diff --git a/core/modules/config/tests/config_test/config/schema/config_test.schema.yml b/core/modules/config/tests/config_test/config/schema/config_test.schema.yml
index 1657750..e964db3 100644
--- a/core/modules/config/tests/config_test/config/schema/config_test.schema.yml
+++ b/core/modules/config/tests/config_test/config/schema/config_test.schema.yml
@@ -15,6 +15,12 @@ config_test_dynamic:
     style:
       type: string
       label: 'style'
+    size:
+      type: string
+      label: 'Size'
+    size_value:
+      type: string
+      label: 'Size value'
     protected_property:
       type: string
       label: 'Protected property'
diff --git a/core/modules/config/tests/config_test/src/ConfigTestForm.php b/core/modules/config/tests/config_test/src/ConfigTestForm.php
index e77ddb0..56195e2 100644
--- a/core/modules/config/tests/config_test/src/ConfigTestForm.php
+++ b/core/modules/config/tests/config_test/src/ConfigTestForm.php
@@ -54,6 +54,50 @@ public function form(array $form, FormStateInterface $form_state) {
       $form['style']['#options'] = image_style_options();
     }
 
+    // The main premise of entity forms is that we get to work with an entity
+    // object at all times instead of checking submitted values from the form
+    // state.
+    $size = $entity->get('size');
+
+    $form['size_wrapper'] = array(
+      '#type' => 'container',
+      '#attributes' => array(
+        'id' => 'size-wrapper',
+      ),
+    );
+    $form['size_wrapper']['size'] = array(
+      '#type' => 'select',
+      '#title' => 'Size',
+      '#options' => array(
+        'custom' => 'Custom',
+      ),
+      '#empty_option' => '- None -',
+      '#default_value' => $size,
+      '#ajax' => array(
+        'callback' => '::updateSize',
+        'wrapper' => 'size-wrapper',
+      ),
+    );
+    $form['size_wrapper']['size_submit'] = array(
+      '#type' => 'submit',
+      '#value' => t('Change size'),
+      '#attributes' => array(
+        'class' => array('js-hide'),
+      ),
+      '#submit' => array(array(get_class($this), 'changeSize')),
+    );
+    $form['size_wrapper']['size_value'] = array(
+      '#type' => 'select',
+      '#title' => 'Custom size value',
+      '#options' => array(
+        'small' => 'Small',
+        'medium' => 'Medium',
+        'large' => 'Large',
+      ),
+      '#default_value' => $entity->get('size_value'),
+      '#access' => !empty($size),
+    );
+
     $form['actions'] = array('#type' => 'actions');
     $form['actions']['submit'] = array(
       '#type' => 'submit',
@@ -68,6 +112,20 @@ public function form(array $form, FormStateInterface $form_state) {
   }
 
   /**
+   * Ajax callback for the size selection element.
+   */
+  public static function updateSize(array $form, FormStateInterface $form_state) {
+    return $form['size_wrapper'];
+  }
+
+  /**
+   * Element submit handler for non-JS testing.
+   */
+  public static function changeSize(array $form, FormStateInterface $form_state) {
+    $form_state->setRebuild();
+  }
+
+  /**
    * {@inheritdoc}
    */
   public function save(array $form, FormStateInterface $form_state) {
diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module
index 928c4da..647b91f 100644
--- a/core/modules/editor/editor.module
+++ b/core/modules/editor/editor.module
@@ -92,7 +92,8 @@ function editor_form_filter_admin_overview_alter(&$form, FormStateInterface $for
 function editor_form_filter_format_form_alter(&$form, FormStateInterface $form_state) {
   $editor = $form_state->get('editor');
   if ($editor === NULL) {
-    $format_id = $form_state->getFormObject()->getEntity()->id();
+    $format = $form_state->getFormObject()->getEntity();
+    $format_id = $format->isNew() ? NULL : $format->id();
     $editor = editor_load($format_id);
     $form_state->set('editor', $editor);
   }
@@ -175,8 +176,9 @@ function editor_form_filter_admin_format_editor_configure($form, FormStateInterf
       $form_state->set('editor', FALSE);
     }
     elseif (empty($editor) || $editor_value !== $editor->getEditor()) {
+      $format = $form_state->getFormObject()->getEntity();
       $editor = entity_create('editor', array(
-        'format' => $form_state->getFormObject()->getEntity()->id(),
+        'format' => $format->isNew() ? NULL : $format->id(),
         'editor' => $editor_value,
       ));
       $form_state->set('editor', $editor);
@@ -215,7 +217,8 @@ function editor_form_filter_admin_format_validate($form, FormStateInterface $for
  */
 function editor_form_filter_admin_format_submit($form, FormStateInterface $form_state) {
   // Delete the existing editor if disabling or switching between editors.
-  $format_id = $form_state->getFormObject()->getEntity()->id();
+  $format = $form_state->getFormObject()->getEntity();
+  $format_id = $format->isNew() ? NULL : $format->id();
   $original_editor = editor_load($format_id);
   if ($original_editor && $original_editor->getEditor() != $form_state->getValue(array('editor', 'editor'))) {
     $original_editor->delete();
