From 64f9bb959b30c26870c7a7daad48d6dc78b66105 Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Fri, 30 Sep 2016 11:46:40 +0100
Subject: [PATCH] Issue #2796581 by tim.plunkett, swentel, amateescu: Fields
 must store their region in entity displays

---
 core/config/schema/core.entity.schema.yml     |   6 +
 .../Drupal/Core/Entity/EntityDisplayBase.php  |  24 +-
 .../Drupal/Core/Field/BaseFieldDefinition.php |   3 +-
 ...ggregator_feed.aggregator_feed.default.yml |   6 +
 ...ggregator_feed.aggregator_feed.summary.yml |   2 +
 ...ggregator_item.aggregator_item.summary.yml |   1 +
 ....entity_form_display.node.book.default.yml |   6 +
 ....entity_view_display.node.book.default.yml |   2 +
 ...e.entity_view_display.node.book.teaser.yml |   2 +
 core/modules/field/src/Entity/FieldConfig.php |   3 +-
 .../d6/MigrateFieldFormatterSettingsTest.php  |   1 +
 .../d6/MigrateFieldWidgetSettingsTest.php     |   1 +
 core/modules/field_ui/field_ui.js             |  32 +-
 .../src/Form/EntityDisplayFormBase.php        | 157 ++++++++--
 .../src/Form/EntityFormDisplayEditForm.php    |   1 +
 .../src/Form/EntityViewDisplayEditForm.php    |   1 +
 .../field_ui/src/Tests/ManageDisplayTest.php  |  22 ++
 .../EntityDisplayTest.php                     |  95 ++++++
 .../tests/src/Kernel/EntityDisplayTest.php    |  16 +-
 .../src/Kernel/EntityFormDisplayTest.php      |   5 +-
 ..._display.comment.comment_forum.default.yml |   3 +
 ...entity_form_display.node.forum.default.yml |   8 +
 ...m_display.taxonomy_term.forums.default.yml |   2 +
 ..._display.comment.comment_forum.default.yml |   2 +
 ...entity_view_display.node.forum.default.yml |   4 +
 ....entity_view_display.node.forum.teaser.yml |   3 +
 ...w_display.taxonomy_term.forums.default.yml |   1 +
 ...play.node.options_install_test.default.yml |   6 +
 ...play.node.options_install_test.default.yml |   2 +
 ...splay.node.options_install_test.teaser.yml |   2 +
 .../Tests/Update/UpdateEntityDisplayTest.php  |  49 +++
 core/modules/system/system.post_update.php    |  19 ++
 ...rm_display.block_content.basic.default.yml |   2 +
 ...y_form_display.comment.comment.default.yml |   3 +
 ...tity_form_display.node.article.default.yml |  10 +
 ....entity_form_display.node.page.default.yml |   7 +
 ....entity_form_display.user.user.default.yml |   5 +
 ...ew_display.block_content.basic.default.yml |   1 +
 ...y_view_display.comment.comment.default.yml |   2 +
 ...tity_view_display.node.article.default.yml |   5 +
 ...e.entity_view_display.node.article.rss.yml |   1 +
 ...ntity_view_display.node.article.teaser.yml |   4 +
 ....entity_view_display.node.page.default.yml |   2 +
 ...e.entity_view_display.node.page.teaser.yml |   2 +
 ....entity_view_display.user.user.compact.yml |   1 +
 ....entity_view_display.user.user.default.yml |   2 +
 .../Core/Entity/EntityDisplayFormBaseTest.php | 296 ++++++++++++++++++
 47 files changed, 787 insertions(+), 43 deletions(-)
 create mode 100644 core/modules/field_ui/tests/src/FunctionalJavascript/EntityDisplayTest.php
 create mode 100644 core/modules/system/src/Tests/Update/UpdateEntityDisplayTest.php
 create mode 100644 core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayFormBaseTest.php

diff --git a/core/config/schema/core.entity.schema.yml b/core/config/schema/core.entity.schema.yml
index bf0e12d939cb..711ca0f7c7e0 100644
--- a/core/config/schema/core.entity.schema.yml
+++ b/core/config/schema/core.entity.schema.yml
@@ -64,6 +64,9 @@ core.entity_view_display.*.*.*:
           weight:
             type: integer
             label: 'Weight'
+          region:
+            type: string
+            label: 'Region'
           label:
              type: string
              label: 'Label setting machine name'
@@ -115,6 +118,9 @@ core.entity_form_display.*.*.*:
           weight:
             type: integer
             label: 'Weight'
+          region:
+            type: string
+            label: 'Region'
           settings:
             type: field.widget.settings.[%parent.type]
             label: 'Settings'
diff --git a/core/lib/Drupal/Core/Entity/EntityDisplayBase.php b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php
index 4de891a825c5..0835aa06eea0 100644
--- a/core/lib/Drupal/Core/Entity/EntityDisplayBase.php
+++ b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php
@@ -154,6 +154,7 @@ public function __construct(array $values, $entity_type) {
   protected function init() {
     // Only populate defaults for "official" view modes and form modes.
     if ($this->mode !== static::CUSTOM_MODE) {
+      $default_region = $this->getDefaultRegion();
       // Fill in defaults for extra fields.
       $context = $this->displayContext == 'view' ? 'display' : $this->displayContext;
       $extra_fields = \Drupal::entityManager()->getExtraFields($this->targetEntityType, $this->bundle);
@@ -163,6 +164,8 @@ protected function init() {
           // Extra fields are visible by default unless they explicitly say so.
           if (!isset($definition['visible']) || $definition['visible'] == TRUE) {
             $this->content[$name] = array(
+              'type' => 'visible',
+              'region' => $default_region,
               'weight' => $definition['weight']
             );
           }
@@ -178,10 +181,13 @@ protected function init() {
         if (!$definition->isDisplayConfigurable($this->displayContext) || (!isset($this->content[$name]) && !isset($this->hidden[$name]))) {
           $options = $definition->getDisplayOptions($this->displayContext);
 
-          if (!empty($options['type']) && $options['type'] == 'hidden') {
+          // Check if either 'type' or 'region' is set to hidden.
+          // @todo Remove handling of 'type' in https://www.drupal.org/node/2799641.
+          if ((!empty($options['type']) && $options['type'] === 'hidden') || (!empty($options['region']) && $options['region'] === 'hidden')) {
             $this->hidden[$name] = TRUE;
           }
           elseif ($options) {
+            $options += ['region' => $default_region];
             $this->content[$name] = $this->pluginManager->prepareConfiguration($definition->getType(), $options);
           }
           // Note: (base) fields that do not specify display options are not
@@ -334,6 +340,12 @@ public function setComponent($name, array $options = array()) {
     // Ensure we always have an empty settings and array.
     $options += ['settings' => [], 'third_party_settings' => []];
 
+    // Ensure that a region is set.
+    // @todo Make 'region' required in https://www.drupal.org/node/2799641.
+    if (!isset($options['region'])) {
+      $options['region'] = (isset($options['type']) && $options['type'] === 'hidden') ? 'hidden' : $this->getDefaultRegion();
+    }
+
     $this->content[$name] = $options;
     unset($this->hidden[$name]);
     unset($this->plugins[$name]);
@@ -504,6 +516,16 @@ protected function getPluginRemovedDependencies(array $plugin_dependencies, arra
     return $intersect;
   }
 
+  /**
+   * Gets the default region.
+   *
+   * @return string
+   *   The default region for this display.
+   */
+  protected function getDefaultRegion() {
+    return 'content';
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
index 594f31d14fbf..dd96fc94a5d9 100644
--- a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
+++ b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
@@ -414,7 +414,8 @@ public function setDisplayOptions($display_context, array $options) {
   public function setDisplayConfigurable($display_context, $configurable) {
     // If no explicit display options have been specified, default to 'hidden'.
     if (empty($this->definition['display'][$display_context])) {
-      $this->definition['display'][$display_context]['options'] = array('type' => 'hidden');
+      // @todo Remove handling of 'type' in https://www.drupal.org/node/2799641.
+      $this->definition['display'][$display_context]['options'] = array('type' => 'hidden', 'region' => 'hidden');
     }
     $this->definition['display'][$display_context]['configurable'] = $configurable;
     return $this;
diff --git a/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.default.yml b/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.default.yml
index e3c23cfb1370..e0232cfb51b7 100644
--- a/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.default.yml
+++ b/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.default.yml
@@ -11,20 +11,26 @@ content:
   checked:
     type: timestamp_ago
     weight: 1
+    region: content
     settings: {  }
     third_party_settings: {  }
     label: inline
   description:
     weight: 3
+    region: content
   feed_icon:
     weight: 5
+    region: content
   image:
     weight: 2
+    region: content
   items:
     weight: 0
+    region: content
   link:
     type: uri_link
     weight: 4
+    region: content
     settings: {  }
     third_party_settings: {  }
     label: inline
diff --git a/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.summary.yml b/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.summary.yml
index 40425f2f5431..5e5e468ae597 100644
--- a/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.summary.yml
+++ b/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.summary.yml
@@ -12,8 +12,10 @@ mode: summary
 content:
   items:
     weight: 0
+    region: content
   more_link:
     weight: 1
+    region: content
 hidden:
   checked: true
   description: true
diff --git a/core/modules/aggregator/config/install/core.entity_view_display.aggregator_item.aggregator_item.summary.yml b/core/modules/aggregator/config/install/core.entity_view_display.aggregator_item.aggregator_item.summary.yml
index 837bee0b6cb9..8e29395a8112 100644
--- a/core/modules/aggregator/config/install/core.entity_view_display.aggregator_item.aggregator_item.summary.yml
+++ b/core/modules/aggregator/config/install/core.entity_view_display.aggregator_item.aggregator_item.summary.yml
@@ -12,6 +12,7 @@ mode: summary
 content:
   timestamp:
     weight: 0
+    region: content
 hidden:
   author: true
   description: true
diff --git a/core/modules/book/config/install/core.entity_form_display.node.book.default.yml b/core/modules/book/config/install/core.entity_form_display.node.book.default.yml
index 1ec4eb11b917..58aba45d3629 100644
--- a/core/modules/book/config/install/core.entity_form_display.node.book.default.yml
+++ b/core/modules/book/config/install/core.entity_form_display.node.book.default.yml
@@ -14,6 +14,7 @@ content:
   body:
     type: text_textarea_with_summary
     weight: 26
+    region: content
     settings:
       rows: 9
       summary_rows: 3
@@ -22,6 +23,7 @@ content:
   created:
     type: datetime_timestamp
     weight: 10
+    region: content
     settings: {  }
     third_party_settings: {  }
   promote:
@@ -29,16 +31,19 @@ content:
     settings:
       display_label: true
     weight: 15
+    region: content
     third_party_settings: {  }
   sticky:
     type: boolean_checkbox
     settings:
       display_label: true
     weight: 16
+    region: content
     third_party_settings: {  }
   title:
     type: string_textfield
     weight: -5
+    region: content
     settings:
       size: 60
       placeholder: ''
@@ -46,6 +51,7 @@ content:
   uid:
     type: entity_reference_autocomplete
     weight: 5
+    region: content
     settings:
       match_operator: CONTAINS
       size: 60
diff --git a/core/modules/book/config/install/core.entity_view_display.node.book.default.yml b/core/modules/book/config/install/core.entity_view_display.node.book.default.yml
index 729516eb8b4b..d6ef64df8621 100644
--- a/core/modules/book/config/install/core.entity_view_display.node.book.default.yml
+++ b/core/modules/book/config/install/core.entity_view_display.node.book.default.yml
@@ -16,8 +16,10 @@ content:
     label: hidden
     type: text_default
     weight: 100
+    region: content
     settings: {  }
     third_party_settings: {  }
   links:
     weight: 101
+    region: content
 hidden: {  }
diff --git a/core/modules/book/config/install/core.entity_view_display.node.book.teaser.yml b/core/modules/book/config/install/core.entity_view_display.node.book.teaser.yml
index fb22db652389..77a62c35ab9d 100644
--- a/core/modules/book/config/install/core.entity_view_display.node.book.teaser.yml
+++ b/core/modules/book/config/install/core.entity_view_display.node.book.teaser.yml
@@ -17,9 +17,11 @@ content:
     label: hidden
     type: text_summary_or_trimmed
     weight: 100
+    region: content
     settings:
       trim_length: 600
     third_party_settings: {  }
   links:
     weight: 101
+    region: content
 hidden: {  }
diff --git a/core/modules/field/src/Entity/FieldConfig.php b/core/modules/field/src/Entity/FieldConfig.php
index d7ccc30360ec..1b79b6bd7164 100644
--- a/core/modules/field/src/Entity/FieldConfig.php
+++ b/core/modules/field/src/Entity/FieldConfig.php
@@ -306,7 +306,8 @@ public function isDisplayConfigurable($context) {
    */
   public function getDisplayOptions($display_context) {
     // Hide configurable fields by default.
-    return array('type' => 'hidden');
+    // @todo Remove handling of 'type' in https://www.drupal.org/node/2799641.
+    return array('type' => 'hidden', 'region' => 'hidden');
   }
 
   /**
diff --git a/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldFormatterSettingsTest.php b/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldFormatterSettingsTest.php
index 4c9cbf10ba9d..2ee3c779bc04 100644
--- a/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldFormatterSettingsTest.php
+++ b/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldFormatterSettingsTest.php
@@ -32,6 +32,7 @@ public function testEntityDisplaySettings() {
       'type' => 'text_trimmed',
       'settings' => array('trim_length' => 600),
       'third_party_settings' => array(),
+      'region' => 'content',
     );
 
     // Can we load any entity display.
diff --git a/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldWidgetSettingsTest.php b/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldWidgetSettingsTest.php
index a16040fe951a..e7bb63621a0b 100644
--- a/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldWidgetSettingsTest.php
+++ b/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldWidgetSettingsTest.php
@@ -33,6 +33,7 @@ public function testWidgetSettings() {
     $expected = array('weight' => 1, 'type' => 'text_textfield');
     $expected['settings'] = array('size' => 60, 'placeholder' => '');
     $expected['third_party_settings'] = array();
+    $expected['region'] = 'content';
     $this->assertIdentical($expected, $component, 'Text field settings are correct.');
 
     // Integer field.
diff --git a/core/modules/field_ui/field_ui.js b/core/modules/field_ui/field_ui.js
index a53553dc4841..c9ff216c5786 100644
--- a/core/modules/field_ui/field_ui.js
+++ b/core/modules/field_ui/field_ui.js
@@ -128,9 +128,14 @@
       var refreshRows = {};
       refreshRows[rowHandler.name] = $trigger.get(0);
 
-      // Handle region change.
+      // Handle region or type change.
       var region = rowHandler.getRegion();
-      if (region !== rowHandler.region) {
+      // @todo Remove handling of 'type' in https://www.drupal.org/node/2799641.
+      var typeRegion = rowHandler.getType();
+      if (region !== rowHandler.region || typeRegion !== rowHandler.region) {
+        if (region === rowHandler.region) {
+          region = typeRegion;
+        }
         // Remove parenting.
         $row.find('select.js-field-parent').val('');
         // Let the row handler deal with the region change.
@@ -270,6 +275,10 @@
     this.$pluginSelect = $(row).find('select.field-plugin-type');
     this.$pluginSelect.on('change', Drupal.fieldUIOverview.onChange);
 
+    // Attach change listener to the 'region' select.
+    this.$regionSelect = $(row).find('select.field-region');
+    this.$regionSelect.on('change', Drupal.fieldUIOverview.onChange);
+
     return this;
   };
 
@@ -282,6 +291,16 @@
      *   Either 'hidden' or 'content'.
      */
     getRegion: function () {
+      return this.$regionSelect.val();
+    },
+
+    /**
+     * Returns the region corresponding to the current form values of the row.
+     *
+     * @return {string}
+     *   Either 'hidden' or 'content'.
+     */
+    getType: function () {
       return (this.$pluginSelect.val() === 'hidden') ? 'hidden' : 'content';
     },
 
@@ -305,14 +324,17 @@
      *   {@link Drupal.fieldUIOverview.AJAXRefreshRows}.
      */
     regionChange: function (region) {
+      // Replace dashes with underscores.
+      region = region.replace(/-/g, '_');
+
+      // Set the region of the select list.
+      this.$regionSelect.val(region);
 
       // When triggered by a row drag, the 'format' select needs to be adjusted
       // to the new region.
       var currentValue = this.$pluginSelect.val();
       var value;
-      // @TODO Check if this couldn't just be like
-      // if (region !== 'hidden') {
-      if (region === 'content') {
+      if (region !== 'hidden') {
         if (currentValue === 'hidden') {
           // Restore the formatter back to the default formatter. Pseudo-fields
           // do not have default formatters, we just return to 'visible' for
diff --git a/core/modules/field_ui/src/Form/EntityDisplayFormBase.php b/core/modules/field_ui/src/Form/EntityDisplayFormBase.php
index 586f1ef39190..f4fb39c310fd 100644
--- a/core/modules/field_ui/src/Form/EntityDisplayFormBase.php
+++ b/core/modules/field_ui/src/Form/EntityDisplayFormBase.php
@@ -4,6 +4,7 @@
 
 use Drupal\Component\Plugin\Factory\DefaultFactory;
 use Drupal\Component\Plugin\PluginManagerBase;
+use Drupal\Core\Entity\Display\EntityDisplayInterface;
 use Drupal\Core\Entity\EntityForm;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
@@ -172,6 +173,13 @@ public function form(array $form, FormStateInterface $form_state) {
           'subgroup' => 'field-parent',
           'source' => 'field-name',
         ),
+        array(
+          'action' => 'match',
+          'relationship' => 'parent',
+          'group' => 'field-region',
+          'subgroup' => 'field-region',
+          'source' => 'field-name',
+        ),
       ),
     );
 
@@ -309,6 +317,15 @@ protected function buildFieldRow(FieldDefinitionInterface $field_definition, arr
           '#attributes' => array('class' => array('field-name')),
         ),
       ),
+      'region' => array(
+        '#type' => 'select',
+        '#title' => $this->t('Region for @title', array('@title' => $label)),
+        '#title_display' => 'invisible',
+        '#options' => $this->getRegionOptions(),
+        '#empty_value' => 'hidden',
+        '#default_value' => isset($display_options['region']) ? $display_options['region'] : 'hidden',
+        '#attributes' => array('class' => array('field-region')),
+      ),
     );
 
     $field_row['plugin'] = array(
@@ -474,6 +491,15 @@ protected function buildExtraFieldRow($field_id, $extra_field) {
           '#attributes' => array('class' => array('field-name')),
         ),
       ),
+      'region' => array(
+        '#type' => 'select',
+        '#title' => $this->t('Region for @title', array('@title' => $extra_field['label'])),
+        '#title_display' => 'invisible',
+        '#options' => $this->getRegionOptions(),
+        '#empty_value' => 'hidden',
+        '#default_value' => $display_options ? $display_options['region'] : 'hidden',
+        '#attributes' => array('class' => array('field-region')),
+      ),
       'plugin' => array(
         'type' => array(
           '#type' => 'select',
@@ -548,44 +574,119 @@ protected function copyFormValuesToEntity(EntityInterface $entity, array $form,
 
     // Collect data for 'regular' fields.
     foreach ($form['#fields'] as $field_name) {
-      $values = $form_values['fields'][$field_name];
+      $this->processFieldUpdates($field_name, $form_values['fields'][$field_name], $entity, $form_state);
+    }
+
+    // Collect data for 'extra' fields.
+    foreach ($form['#extra'] as $name) {
+      $this->processFieldUpdates($name, $form_values['fields'][$name], $entity, $form_state);
+    }
 
-      if ($values['type'] == 'hidden') {
-        $entity->removeComponent($field_name);
+    $form_state->setTemporaryValue('entity_display_components_updated', TRUE);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $form, FormStateInterface $form_state) {
+    $form_state->setTemporaryValue('entity_display_components_updated', NULL);
+    return parent::save($form, $form_state);
+  }
+
+  /**
+   * Processes updates to the components for a given field.
+   *
+   * @param string $field_name
+   *   The field name being processed.
+   * @param array $values
+   *   The submitted form values.
+   * @param \Drupal\Core\Entity\Display\EntityDisplayInterface $entity
+   *   The entity being updated.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  protected function processFieldUpdates($field_name, array $values, EntityDisplayInterface $entity, FormStateInterface $form_state) {
+    // If the component is not found, it is initially hidden.
+    $options = $entity->getComponent($field_name) ?: ['type' => 'hidden', 'region' => 'hidden'];
+    $remove_component = $options['region'] === 'hidden';
+    if ($form_state->getTemporaryValue('entity_display_components_updated')) {
+      // Since the component has already been updated, replace $values with the
+      // relevant parts of $options.
+      $values = array_intersect_key($options, $values) + $values;
+    }
+    // @todo In https://www.drupal.org/node/2799641, remove this else statement.
+    else {
+      $remove_component = $this->determineComponentAction($options, $values);
+    }
+
+    if ($remove_component) {
+      $entity->removeComponent($field_name);
+    }
+    else {
+      // Update field settings only if the submit handler told us to.
+      if ($form_state->get('plugin_settings_update') === $field_name) {
+        // Only store settings actually used by the selected plugin.
+        $default_settings = $this->pluginManager->getDefaultSettings($options['type']);
+        $options['settings'] = isset($values['settings_edit_form']['settings']) ? array_intersect_key($values['settings_edit_form']['settings'], $default_settings) : [];
+        $options['third_party_settings'] = isset($values['settings_edit_form']['third_party_settings']) ? $values['settings_edit_form']['third_party_settings'] : [];
+        $form_state->set('plugin_settings_update', NULL);
       }
-      else {
-        $options = $entity->getComponent($field_name);
-
-        // Update field settings only if the submit handler told us to.
-        if ($form_state->get('plugin_settings_update') === $field_name) {
-          // Only store settings actually used by the selected plugin.
-          $default_settings = $this->pluginManager->getDefaultSettings($options['type']);
-          $options['settings'] = isset($values['settings_edit_form']['settings']) ? array_intersect_key($values['settings_edit_form']['settings'], $default_settings) : [];
-          $options['third_party_settings'] = isset($values['settings_edit_form']['third_party_settings']) ? $values['settings_edit_form']['third_party_settings'] : [];
-          $form_state->set('plugin_settings_update', NULL);
-        }
 
+      if (isset($values['type'])) {
         $options['type'] = $values['type'];
-        $options['weight'] = $values['weight'];
-        // Only formatters have configurable label visibility.
-        if (isset($values['label'])) {
-          $options['label'] = $values['label'];
-        }
-        $entity->setComponent($field_name, $options);
       }
+      $options['weight'] = $values['weight'];
+      if (isset($values['region'])) {
+        $options['region'] = $values['region'];
+      }
+      // Only formatters have configurable label visibility.
+      if (isset($values['label'])) {
+        $options['label'] = $values['label'];
+      }
+      $entity->setComponent($field_name, $options);
     }
+  }
 
-    // Collect data for 'extra' fields.
-    foreach ($form['#extra'] as $name) {
-      if ($form_values['fields'][$name]['type'] == 'hidden') {
-        $entity->removeComponent($name);
+  /**
+   * Determines whether a component should be updated or removed.
+   *
+   * @todo Remove handling of 'type' in https://www.drupal.org/node/2799641.
+   *
+   * @param array $old_values
+   *   An array of the old values for a given component.
+   * @param array $new_values
+   *   An array of the new values for a given component.
+   *
+   * @return bool
+   *   TRUE if the component should be removed, FALSE if it should be updated.
+   */
+  protected function determineComponentAction(array &$old_values, array &$new_values) {
+    $has_type_change = $new_values['type'] !== $old_values['type'];
+    $has_region_change = $new_values['region'] !== $old_values['region'];
+    // If the type and region both changed or neither changed, the action will
+    // be the same. Base the decision on whether the region is hidden.
+    if ($has_type_change === $has_region_change) {
+      $remove_component = $new_values['region'] === 'hidden';
+    }
+    else {
+      if ($has_region_change) {
+        // If only the region changed, remove the component if it is now hidden.
+        $remove_component = $new_values['region'] === 'hidden';
+        // If the region and type mismatch, remove the invalid type.
+        if ($new_values['region'] !== 'hidden' && $new_values['type'] === 'hidden') {
+          unset($new_values['type'], $old_values['type']);
+        }
       }
       else {
-        $entity->setComponent($name, array(
-          'weight' => $form_values['fields'][$name]['weight'],
-        ));
+        // If only the type changed, remove the component if it is now hidden.
+        $remove_component = $new_values['type'] === 'hidden';
+        // If the region and type mismatch, remove the invalid region.
+        if ($new_values['region'] === 'hidden' && $new_values['type'] !== 'hidden') {
+          unset($new_values['region'], $old_values['region']);
+        }
       }
     }
+    return $remove_component;
   }
 
   /**
@@ -813,7 +914,7 @@ public function getRowRegion($row) {
     switch ($row['#row_type']) {
       case 'field':
       case 'extra_field':
-        return ($row['plugin']['type']['#value'] == 'hidden' ? 'hidden' : 'content');
+        return $row['region']['#value'] ?: 'hidden';
     }
   }
 
diff --git a/core/modules/field_ui/src/Form/EntityFormDisplayEditForm.php b/core/modules/field_ui/src/Form/EntityFormDisplayEditForm.php
index 741b98daea6c..af8e2edfc307 100644
--- a/core/modules/field_ui/src/Form/EntityFormDisplayEditForm.php
+++ b/core/modules/field_ui/src/Form/EntityFormDisplayEditForm.php
@@ -94,6 +94,7 @@ protected function getTableHeader() {
       $this->t('Field'),
       $this->t('Weight'),
       $this->t('Parent'),
+      $this->t('Region'),
       array('data' => $this->t('Widget'), 'colspan' => 3),
     );
   }
diff --git a/core/modules/field_ui/src/Form/EntityViewDisplayEditForm.php b/core/modules/field_ui/src/Form/EntityViewDisplayEditForm.php
index f273325f6739..174726f2c60b 100644
--- a/core/modules/field_ui/src/Form/EntityViewDisplayEditForm.php
+++ b/core/modules/field_ui/src/Form/EntityViewDisplayEditForm.php
@@ -127,6 +127,7 @@ protected function getTableHeader() {
       $this->t('Field'),
       $this->t('Weight'),
       $this->t('Parent'),
+      $this->t('Region'),
       $this->t('Label'),
       array('data' => $this->t('Format'), 'colspan' => 3),
     );
diff --git a/core/modules/field_ui/src/Tests/ManageDisplayTest.php b/core/modules/field_ui/src/Tests/ManageDisplayTest.php
index 4cd29016364d..eaedda661214 100644
--- a/core/modules/field_ui/src/Tests/ManageDisplayTest.php
+++ b/core/modules/field_ui/src/Tests/ManageDisplayTest.php
@@ -3,6 +3,7 @@
 namespace Drupal\field_ui\Tests;
 
 use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Entity\Entity\EntityFormDisplay;
 use Drupal\Core\Entity\Entity\EntityViewDisplay;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Language\LanguageInterface;
@@ -98,6 +99,19 @@ function testFormatterUI() {
     );
     $this->assertEqual($options, $expected_options, 'The expected formatter ordering is respected.');
 
+    // Ensure that fields can be hidden directly by changing the region.
+    $this->drupalGet($manage_display);
+    $this->assertFieldByName('fields[field_test][region]', 'content');
+    $edit = ['fields[field_test][region]' => 'hidden'];
+    $this->drupalPostForm($manage_display, $edit, t('Save'));
+    $this->assertFieldByName('fields[field_test][region]', 'hidden');
+    $display = EntityViewDisplay::load("node.{$this->type}.default");
+    $this->assertNull($display->getComponent('field_test'));
+
+    // Restore the field to the content region.
+    $edit = ['fields[field_test][region]' => 'content'];
+    $this->drupalPostForm($manage_display, $edit, t('Save'));
+
     // Change the formatter and check that the summary is updated.
     $edit = array('fields[field_test][type]' => 'field_test_multiple', 'refresh_rows' => 'field_test');
     $this->drupalPostAjaxForm(NULL, $edit, array('op' => t('Refresh')));
@@ -284,6 +298,14 @@ public function testWidgetUI() {
     // Checks if the select elements contain the specified options.
     $this->assertFieldSelectOptions('fields[field_test][type]', array('test_field_widget', 'test_field_widget_multiple', 'hidden'));
     $this->assertFieldSelectOptions('fields[field_onewidgetfield][type]', array('test_field_widget', 'hidden'));
+
+    // Ensure that fields can be hidden directly by changing the region.
+    $this->assertFieldByName('fields[field_test][region]', 'content');
+    $edit = ['fields[field_test][region]' => 'hidden'];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertFieldByName('fields[field_test][region]', 'hidden');
+    $display = EntityFormDisplay::load("node.{$this->type}.default");
+    $this->assertNull($display->getComponent('field_test'));
   }
 
   /**
diff --git a/core/modules/field_ui/tests/src/FunctionalJavascript/EntityDisplayTest.php b/core/modules/field_ui/tests/src/FunctionalJavascript/EntityDisplayTest.php
new file mode 100644
index 000000000000..7cde00cc46bb
--- /dev/null
+++ b/core/modules/field_ui/tests/src/FunctionalJavascript/EntityDisplayTest.php
@@ -0,0 +1,95 @@
+<?php
+
+namespace Drupal\Tests\field_ui\FunctionalJavascript;
+
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+
+/**
+ * Tests the UI for entity displays.
+ *
+ * @group field_ui
+ */
+class EntityDisplayTest extends JavascriptTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['field_ui', 'entity_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $entity = EntityTest::create([
+      'name' => 'The name for this entity',
+      'field_test_text' => [[
+        'value' => 'The field test text value',
+      ]],
+    ]);
+    $entity->save();
+    $this->drupalLogin($this->drupalCreateUser([
+      'access administration pages',
+      'view test entity',
+      'administer entity_test content',
+      'administer entity_test fields',
+      'administer entity_test display',
+      'administer entity_test form display',
+      'view the administration theme',
+    ]));
+  }
+
+  /**
+   * Tests the use of regions for entity form displays.
+   */
+  public function testEntityForm() {
+    $this->drupalGet('entity_test/manage/1/edit');
+    $this->assertSession()->fieldExists('field_test_text[0][value]');
+
+    $this->drupalGet('entity_test/structure/entity_test/form-display');
+    $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
+    $this->assertFalse($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->isSelected());
+
+    $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'hidden');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
+    $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->isSelected());
+
+    $this->submitForm([], 'Save');
+    $this->assertSession()->pageTextContains('Your settings have been saved.');
+    $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
+    $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->isSelected());
+
+    $this->drupalGet('entity_test/manage/1/edit');
+    $this->assertSession()->fieldNotExists('field_test_text[0][value]');
+  }
+
+  /**
+   * Tests the use of regions for entity view displays.
+   */
+  public function testEntityView() {
+    $this->drupalGet('entity_test/1');
+    $this->assertSession()->elementNotExists('css', '.field--name-field-test-text');
+
+    $this->drupalGet('entity_test/structure/entity_test/display');
+    $this->assertSession()->elementExists('css', '.region-content-message.region-empty');
+    $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
+    $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->isSelected());
+
+    $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'content');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
+    $this->assertFalse($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->isSelected());
+
+    $this->submitForm([], 'Save');
+    $this->assertSession()->pageTextContains('Your settings have been saved.');
+    $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
+    $this->assertFalse($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->isSelected());
+
+    $this->drupalGet('entity_test/1');
+    $this->assertSession()->elementExists('css', '.field--name-field-test-text');
+  }
+
+}
diff --git a/core/modules/field_ui/tests/src/Kernel/EntityDisplayTest.php b/core/modules/field_ui/tests/src/Kernel/EntityDisplayTest.php
index be188db4f1b3..756b9a79cf8f 100644
--- a/core/modules/field_ui/tests/src/Kernel/EntityDisplayTest.php
+++ b/core/modules/field_ui/tests/src/Kernel/EntityDisplayTest.php
@@ -53,15 +53,15 @@ public function testEntityDisplayCRUD() {
     // Check that providing no 'weight' results in the highest current weight
     // being assigned. The 'name' field's formatter has weight -5, therefore
     // these follow.
-    $expected['component_1'] = array('weight' => -4, 'settings' => array(), 'third_party_settings' => array());
-    $expected['component_2'] = array('weight' => -3, 'settings' => array(), 'third_party_settings' => array());
+    $expected['component_1'] = array('weight' => -4, 'settings' => array(), 'third_party_settings' => array(), 'region' => 'content');
+    $expected['component_2'] = array('weight' => -3, 'settings' => array(), 'third_party_settings' => array(), 'region' => 'content');
     $display->setComponent('component_1');
     $display->setComponent('component_2');
     $this->assertEqual($display->getComponent('component_1'), $expected['component_1']);
     $this->assertEqual($display->getComponent('component_2'), $expected['component_2']);
 
     // Check that arbitrary options are correctly stored.
-    $expected['component_3'] = array('weight' => 10, 'third_party_settings' => array('field_test' => array('foo' => 'bar')), 'settings' => array());
+    $expected['component_3'] = array('weight' => 10, 'third_party_settings' => array('field_test' => array('foo' => 'bar')), 'settings' => array(), 'region' => 'content');
     $display->setComponent('component_3', $expected['component_3']);
     $this->assertEqual($display->getComponent('component_3'), $expected['component_3']);
 
@@ -86,6 +86,7 @@ public function testEntityDisplayCRUD() {
         'link_to_entity' => FALSE,
       ),
       'third_party_settings' => array(),
+      'region' => 'content',
     );
     $this->assertEqual($display->getComponents(), $expected);
 
@@ -148,7 +149,7 @@ public function testEntityGetDisplay() {
     $display = entity_get_display('entity_test', 'entity_test', 'default');
     $this->assertFalse($display->isNew());
     $this->assertEqual($display->id(), 'entity_test.entity_test.default');
-    $this->assertEqual($display->getComponent('component_1'), array( 'weight' => 10, 'settings' => array(), 'third_party_settings' => array()));
+    $this->assertEqual($display->getComponent('component_1'), array( 'weight' => 10, 'settings' => array(), 'third_party_settings' => array(), 'region' => 'content'));
   }
 
   /**
@@ -164,14 +165,14 @@ public function testExtraFieldComponent() {
 
     // Check that the default visibility taken into account for extra fields
     // unknown in the display.
-    $this->assertEqual($display->getComponent('display_extra_field'), array('weight' => 5));
+    $this->assertEqual($display->getComponent('display_extra_field'), array('weight' => 5, 'type' => 'visible', 'region' => 'content'));
     $this->assertNull($display->getComponent('display_extra_field_hidden'));
 
     // Check that setting explicit options overrides the defaults.
     $display->removeComponent('display_extra_field');
     $display->setComponent('display_extra_field_hidden', array('weight' => 10));
     $this->assertNull($display->getComponent('display_extra_field'));
-    $this->assertEqual($display->getComponent('display_extra_field_hidden'), array('weight' => 10, 'settings' => array(), 'third_party_settings' => array()));
+    $this->assertEqual($display->getComponent('display_extra_field_hidden'), array('weight' => 10, 'settings' => array(), 'third_party_settings' => array(), 'region' => 'content'));
   }
 
   /**
@@ -209,6 +210,7 @@ public function testFieldComponent() {
       'type' => $default_formatter,
       'settings' => $formatter_settings,
       'third_party_settings' => array(),
+      'region' => 'content',
     );
     $this->assertEqual($display->getComponent($field_name), $expected);
 
@@ -258,6 +260,7 @@ public function testBaseFieldComponent() {
         'settings' => $formatter_settings,
         'third_party_settings' => array(),
         'weight' => 10,
+        'region' => 'content',
       ),
       'test_display_non_configurable' => array(
         'label' => 'above',
@@ -265,6 +268,7 @@ public function testBaseFieldComponent() {
         'settings' => $formatter_settings,
         'third_party_settings' => array(),
         'weight' => 11,
+        'region' => 'content',
       ),
     );
     foreach ($expected as $field_name => $options) {
diff --git a/core/modules/field_ui/tests/src/Kernel/EntityFormDisplayTest.php b/core/modules/field_ui/tests/src/Kernel/EntityFormDisplayTest.php
index e38db7cbb735..fb343fdf0e33 100644
--- a/core/modules/field_ui/tests/src/Kernel/EntityFormDisplayTest.php
+++ b/core/modules/field_ui/tests/src/Kernel/EntityFormDisplayTest.php
@@ -43,7 +43,7 @@ public function testEntityGetFromDisplay() {
     $form_display = entity_get_form_display('entity_test', 'entity_test', 'default');
     $this->assertFalse($form_display->isNew());
     $this->assertEqual($form_display->id(), 'entity_test.entity_test.default');
-    $this->assertEqual($form_display->getComponent('component_1'), array('weight' => 10, 'settings' => array(), 'third_party_settings' => array()));
+    $this->assertEqual($form_display->getComponent('component_1'), array('weight' => 10, 'settings' => array(), 'third_party_settings' => array(), 'region' => 'content'));
   }
 
   /**
@@ -80,6 +80,7 @@ public function testFieldComponent() {
       'type' => $default_widget,
       'settings' => $widget_settings,
       'third_party_settings' => array(),
+      'region' => 'content',
     );
     $this->assertEqual($form_display->getComponent($field_name), $expected);
 
@@ -134,12 +135,14 @@ public function testBaseFieldComponent() {
         'settings' => $formatter_settings,
         'third_party_settings' => array(),
         'weight' => 10,
+        'region' => 'content',
       ),
       'test_display_non_configurable' => array(
         'type' => 'text_textfield',
         'settings' => $formatter_settings,
         'third_party_settings' => array(),
         'weight' => 11,
+        'region' => 'content',
       ),
     );
     foreach ($expected as $field_name => $options) {
diff --git a/core/modules/forum/config/optional/core.entity_form_display.comment.comment_forum.default.yml b/core/modules/forum/config/optional/core.entity_form_display.comment.comment_forum.default.yml
index a09c30b0069e..4738bbb43a48 100644
--- a/core/modules/forum/config/optional/core.entity_form_display.comment.comment_forum.default.yml
+++ b/core/modules/forum/config/optional/core.entity_form_display.comment.comment_forum.default.yml
@@ -13,9 +13,11 @@ mode: default
 content:
   author:
     weight: -2
+    region: content
   comment_body:
     type: text_textarea
     weight: 11
+    region: content
     settings:
       rows: 5
       placeholder: ''
@@ -23,6 +25,7 @@ content:
   subject:
     type: string_textfield
     weight: 10
+    region: content
     settings:
       size: 60
       placeholder: ''
diff --git a/core/modules/forum/config/optional/core.entity_form_display.node.forum.default.yml b/core/modules/forum/config/optional/core.entity_form_display.node.forum.default.yml
index c66ba236d174..6773d32d23aa 100644
--- a/core/modules/forum/config/optional/core.entity_form_display.node.forum.default.yml
+++ b/core/modules/forum/config/optional/core.entity_form_display.node.forum.default.yml
@@ -17,6 +17,7 @@ content:
   body:
     type: text_textarea_with_summary
     weight: 27
+    region: content
     settings:
       rows: 9
       summary_rows: 3
@@ -25,11 +26,13 @@ content:
   comment_forum:
     type: comment_default
     weight: 20
+    region: content
     settings: {  }
     third_party_settings: {  }
   created:
     type: datetime_timestamp
     weight: 10
+    region: content
     settings: {  }
     third_party_settings: {  }
   promote:
@@ -37,21 +40,25 @@ content:
     settings:
       display_label: true
     weight: 15
+    region: content
     third_party_settings: {  }
   sticky:
     type: boolean_checkbox
     settings:
       display_label: true
     weight: 16
+    region: content
     third_party_settings: {  }
   taxonomy_forums:
     type: options_select
     weight: 26
+    region: content
     settings: {  }
     third_party_settings: {  }
   title:
     type: string_textfield
     weight: -5
+    region: content
     settings:
       size: 60
       placeholder: ''
@@ -59,6 +66,7 @@ content:
   uid:
     type: entity_reference_autocomplete
     weight: 5
+    region: content
     settings:
       match_operator: CONTAINS
       size: 60
diff --git a/core/modules/forum/config/optional/core.entity_form_display.taxonomy_term.forums.default.yml b/core/modules/forum/config/optional/core.entity_form_display.taxonomy_term.forums.default.yml
index b18c869becd0..50df98ac1712 100644
--- a/core/modules/forum/config/optional/core.entity_form_display.taxonomy_term.forums.default.yml
+++ b/core/modules/forum/config/optional/core.entity_form_display.taxonomy_term.forums.default.yml
@@ -14,11 +14,13 @@ content:
   description:
     type: text_textfield
     weight: 0
+    region: content
     settings: {  }
     third_party_settings: {  }
   name:
     type: string_textfield
     weight: -5
+    region: content
     settings:
       size: 60
       placeholder: ''
diff --git a/core/modules/forum/config/optional/core.entity_view_display.comment.comment_forum.default.yml b/core/modules/forum/config/optional/core.entity_view_display.comment.comment_forum.default.yml
index f4f0112f588b..befeba89ae88 100644
--- a/core/modules/forum/config/optional/core.entity_view_display.comment.comment_forum.default.yml
+++ b/core/modules/forum/config/optional/core.entity_view_display.comment.comment_forum.default.yml
@@ -15,8 +15,10 @@ content:
     label: hidden
     type: text_default
     weight: 0
+    region: content
     settings: {  }
     third_party_settings: {  }
   links:
     weight: 100
+    region: content
 hidden: {  }
diff --git a/core/modules/forum/config/optional/core.entity_view_display.node.forum.default.yml b/core/modules/forum/config/optional/core.entity_view_display.node.forum.default.yml
index b157c839e108..f3e8c5c61322 100644
--- a/core/modules/forum/config/optional/core.entity_view_display.node.forum.default.yml
+++ b/core/modules/forum/config/optional/core.entity_view_display.node.forum.default.yml
@@ -20,21 +20,25 @@ content:
     label: hidden
     type: text_default
     weight: 0
+    region: content
     settings: {  }
     third_party_settings: {  }
   comment_forum:
     label: hidden
     type: comment_default
     weight: 20
+    region: content
     settings:
       view_mode: default
       pager_id: 0
     third_party_settings: {  }
   links:
     weight: 100
+    region: content
   taxonomy_forums:
     type: entity_reference_label
     weight: -1
+    region: content
     label: above
     settings:
       link: true
diff --git a/core/modules/forum/config/optional/core.entity_view_display.node.forum.teaser.yml b/core/modules/forum/config/optional/core.entity_view_display.node.forum.teaser.yml
index 4405e71f9ea4..7b174f442919 100644
--- a/core/modules/forum/config/optional/core.entity_view_display.node.forum.teaser.yml
+++ b/core/modules/forum/config/optional/core.entity_view_display.node.forum.teaser.yml
@@ -19,14 +19,17 @@ content:
     label: hidden
     type: text_summary_or_trimmed
     weight: 100
+    region: content
     settings:
       trim_length: 600
     third_party_settings: {  }
   links:
     weight: 101
+    region: content
   taxonomy_forums:
     type: entity_reference_label
     weight: 10
+    region: content
     label: above
     settings:
       link: true
diff --git a/core/modules/forum/config/optional/core.entity_view_display.taxonomy_term.forums.default.yml b/core/modules/forum/config/optional/core.entity_view_display.taxonomy_term.forums.default.yml
index d1242d99b6a0..b326039a46c3 100644
--- a/core/modules/forum/config/optional/core.entity_view_display.taxonomy_term.forums.default.yml
+++ b/core/modules/forum/config/optional/core.entity_view_display.taxonomy_term.forums.default.yml
@@ -14,6 +14,7 @@ content:
   description:
     type: text_default
     weight: 0
+    region: content
     settings: {  }
     third_party_settings: {  }
     label: above
diff --git a/core/modules/options/tests/options_config_install_test/config/install/core.entity_form_display.node.options_install_test.default.yml b/core/modules/options/tests/options_config_install_test/config/install/core.entity_form_display.node.options_install_test.default.yml
index 19a2ef7076a0..ff5f0ec275c8 100644
--- a/core/modules/options/tests/options_config_install_test/config/install/core.entity_form_display.node.options_install_test.default.yml
+++ b/core/modules/options/tests/options_config_install_test/config/install/core.entity_form_display.node.options_install_test.default.yml
@@ -14,6 +14,7 @@ content:
   title:
     type: string_textfield
     weight: -5
+    region: content
     settings:
       size: 60
       placeholder: ''
@@ -21,6 +22,7 @@ content:
   uid:
     type: entity_reference_autocomplete
     weight: 5
+    region: content
     settings:
       match_operator: CONTAINS
       size: 60
@@ -29,6 +31,7 @@ content:
   created:
     type: datetime_timestamp
     weight: 10
+    region: content
     settings: {  }
     third_party_settings: {  }
   promote:
@@ -36,16 +39,19 @@ content:
     settings:
       display_label: true
     weight: 15
+    region: content
     third_party_settings: {  }
   sticky:
     type: boolean_checkbox
     settings:
       display_label: true
     weight: 16
+    region: content
     third_party_settings: {  }
   body:
     type: text_textarea_with_summary
     weight: 26
+    region: content
     settings:
       rows: 9
       summary_rows: 3
diff --git a/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.default.yml b/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.default.yml
index c107b101753f..aaea1cb90e9c 100644
--- a/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.default.yml
+++ b/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.default.yml
@@ -14,10 +14,12 @@ mode: default
 content:
   links:
     weight: 100
+    region: content
   body:
     label: hidden
     type: text_default
     weight: 101
+    region: content
     settings: {  }
     third_party_settings: {  }
 hidden:
diff --git a/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.teaser.yml b/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.teaser.yml
index 3b472a7dcb2d..6e79af94ee5c 100644
--- a/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.teaser.yml
+++ b/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.teaser.yml
@@ -15,10 +15,12 @@ mode: teaser
 content:
   links:
     weight: 100
+    region: content
   body:
     label: hidden
     type: text_summary_or_trimmed
     weight: 101
+    region: content
     settings:
       trim_length: 600
     third_party_settings: {  }
diff --git a/core/modules/system/src/Tests/Update/UpdateEntityDisplayTest.php b/core/modules/system/src/Tests/Update/UpdateEntityDisplayTest.php
new file mode 100644
index 000000000000..0a2cbf45bbc0
--- /dev/null
+++ b/core/modules/system/src/Tests/Update/UpdateEntityDisplayTest.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Drupal\system\Tests\Update;
+
+use Drupal\Core\Entity\Entity\EntityFormDisplay;
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
+
+/**
+ * Tests system_post_update_add_region_to_entity_displays().
+ *
+ * @group Update
+ */
+class UpdateEntityDisplayTest extends UpdatePathTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setDatabaseDumpFiles() {
+    $this->databaseDumpFiles = [
+      __DIR__ . '/../../../tests/fixtures/update/drupal-8.bare.standard.php.gz',
+    ];
+  }
+
+  /**
+   * Tests that entity displays are updated with regions for their fields.
+   */
+  public function testUpdate() {
+    // No region key appears pre-update.
+    $entity_form_display = EntityFormDisplay::load('node.article.default');
+    $options = $entity_form_display->getComponent('body');
+    $this->assertFalse(array_key_exists('region', $options));
+
+    $entity_view_display = EntityViewDisplay::load('node.article.default');
+    $options = $entity_view_display->getComponent('body');
+    $this->assertFalse(array_key_exists('region', $options));
+
+    $this->runUpdates();
+
+    // The region key has been populated with 'content'.
+    $entity_form_display = EntityFormDisplay::load('node.article.default');
+    $options = $entity_form_display->getComponent('body');
+    $this->assertIdentical('content', $options['region']);
+
+    $entity_view_display = EntityViewDisplay::load('node.article.default');
+    $options = $entity_view_display->getComponent('body');
+    $this->assertIdentical('content', $options['region']);
+  }
+
+}
diff --git a/core/modules/system/system.post_update.php b/core/modules/system/system.post_update.php
index b75625c32d0b..1bd11a8fe6e5 100644
--- a/core/modules/system/system.post_update.php
+++ b/core/modules/system/system.post_update.php
@@ -5,6 +5,10 @@
  * Post update functions for System.
  */
 
+use Drupal\Core\Entity\Display\EntityDisplayInterface;
+use Drupal\Core\Entity\Entity\EntityFormDisplay;
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
+
 /**
  * @addtogroup updates-8.0.0-beta
  * @{
@@ -41,3 +45,18 @@ function system_post_update_recalculate_configuration_entity_dependencies(&$sand
 /**
  * @} End of "addtogroup updates-8.0.0-beta".
  */
+
+/**
+ * Update entity displays to contain the region for each field.
+ */
+function system_post_update_add_region_to_entity_displays() {
+  $entity_save = function (EntityDisplayInterface $entity) {
+    foreach ($entity->getComponents() as $name => $component) {
+      // setComponent() will fill in the correct region based on the 'type'.
+      $entity->setComponent($name, $component);
+    }
+    $entity->save();
+  };
+  array_map($entity_save, EntityViewDisplay::loadMultiple());
+  array_map($entity_save, EntityFormDisplay::loadMultiple());
+}
diff --git a/core/profiles/standard/config/install/core.entity_form_display.block_content.basic.default.yml b/core/profiles/standard/config/install/core.entity_form_display.block_content.basic.default.yml
index ee0c1384d950..7ccb5b0ad374 100644
--- a/core/profiles/standard/config/install/core.entity_form_display.block_content.basic.default.yml
+++ b/core/profiles/standard/config/install/core.entity_form_display.block_content.basic.default.yml
@@ -14,6 +14,7 @@ content:
   body:
     type: text_textarea_with_summary
     weight: -4
+    region: content
     settings:
       rows: 9
       summary_rows: 3
@@ -22,6 +23,7 @@ content:
   info:
     type: string_textfield
     weight: -5
+    region: content
     settings:
       size: 60
       placeholder: ''
diff --git a/core/profiles/standard/config/install/core.entity_form_display.comment.comment.default.yml b/core/profiles/standard/config/install/core.entity_form_display.comment.comment.default.yml
index fa5d834ce799..1010be292488 100644
--- a/core/profiles/standard/config/install/core.entity_form_display.comment.comment.default.yml
+++ b/core/profiles/standard/config/install/core.entity_form_display.comment.comment.default.yml
@@ -13,9 +13,11 @@ mode: default
 content:
   author:
     weight: -2
+    region: content
   comment_body:
     type: text_textarea
     weight: 11
+    region: content
     settings:
       rows: 5
       placeholder: ''
@@ -23,6 +25,7 @@ content:
   subject:
     type: string_textfield
     weight: 10
+    region: content
     settings:
       size: 60
       placeholder: ''
diff --git a/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml b/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml
index 79156b2e876f..c94e36e38831 100644
--- a/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml
+++ b/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml
@@ -21,6 +21,7 @@ content:
   body:
     type: text_textarea_with_summary
     weight: 1
+    region: content
     settings:
       rows: 9
       summary_rows: 3
@@ -29,16 +30,19 @@ content:
   comment:
     type: comment_default
     weight: 20
+    region: content
     settings: {  }
     third_party_settings: {  }
   created:
     type: datetime_timestamp
     weight: 10
+    region: content
     settings: {  }
     third_party_settings: {  }
   field_image:
     type: image_image
     weight: 4
+    region: content
     settings:
       progress_indicator: throbber
       preview_image_style: thumbnail
@@ -46,11 +50,13 @@ content:
   field_tags:
     type: entity_reference_autocomplete_tags
     weight: 3
+    region: content
     settings: {  }
     third_party_settings: {  }
   path:
     type: path
     weight: 30
+    region: content
     settings: {  }
     third_party_settings: {  }
   promote:
@@ -58,16 +64,19 @@ content:
     settings:
       display_label: true
     weight: 15
+    region: content
     third_party_settings: {  }
   sticky:
     type: boolean_checkbox
     settings:
       display_label: true
     weight: 16
+    region: content
     third_party_settings: {  }
   title:
     type: string_textfield
     weight: 0
+    region: content
     settings:
       size: 60
       placeholder: ''
@@ -75,6 +84,7 @@ content:
   uid:
     type: entity_reference_autocomplete
     weight: 5
+    region: content
     settings:
       match_operator: CONTAINS
       size: 60
diff --git a/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml b/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml
index 1fef06d1e8a9..0b7ffd133cf1 100644
--- a/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml
+++ b/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml
@@ -15,6 +15,7 @@ content:
   body:
     type: text_textarea_with_summary
     weight: 31
+    region: content
     settings:
       rows: 9
       summary_rows: 3
@@ -23,11 +24,13 @@ content:
   created:
     type: datetime_timestamp
     weight: 10
+    region: content
     settings: {  }
     third_party_settings: {  }
   path:
     type: path
     weight: 30
+    region: content
     settings: {  }
     third_party_settings: {  }
   promote:
@@ -35,16 +38,19 @@ content:
     settings:
       display_label: true
     weight: 15
+    region: content
     third_party_settings: {  }
   sticky:
     type: boolean_checkbox
     settings:
       display_label: true
     weight: 16
+    region: content
     third_party_settings: {  }
   title:
     type: string_textfield
     weight: -5
+    region: content
     settings:
       size: 60
       placeholder: ''
@@ -52,6 +58,7 @@ content:
   uid:
     type: entity_reference_autocomplete
     weight: 5
+    region: content
     settings:
       match_operator: CONTAINS
       size: 60
diff --git a/core/profiles/standard/config/install/core.entity_form_display.user.user.default.yml b/core/profiles/standard/config/install/core.entity_form_display.user.user.default.yml
index 466b6e0b38e3..683222926890 100644
--- a/core/profiles/standard/config/install/core.entity_form_display.user.user.default.yml
+++ b/core/profiles/standard/config/install/core.entity_form_display.user.user.default.yml
@@ -14,12 +14,16 @@ mode: default
 content:
   account:
     weight: -10
+    region: content
   contact:
     weight: 5
+    region: content
   language:
     weight: 0
+    region: content
   timezone:
     weight: 6
+    region: content
   user_picture:
     type: image_image
     settings:
@@ -27,4 +31,5 @@ content:
       preview_image_style: thumbnail
     third_party_settings: {  }
     weight: -1
+    region: content
 hidden: {  }
diff --git a/core/profiles/standard/config/install/core.entity_view_display.block_content.basic.default.yml b/core/profiles/standard/config/install/core.entity_view_display.block_content.basic.default.yml
index bd52f77507d0..e494882d40a8 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.block_content.basic.default.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.block_content.basic.default.yml
@@ -15,6 +15,7 @@ content:
     label: hidden
     type: text_default
     weight: 0
+    region: content
     settings: {  }
     third_party_settings: {  }
 hidden: {  }
diff --git a/core/profiles/standard/config/install/core.entity_view_display.comment.comment.default.yml b/core/profiles/standard/config/install/core.entity_view_display.comment.comment.default.yml
index 1ed49ce2f6c5..6ae213d3ee0d 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.comment.comment.default.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.comment.comment.default.yml
@@ -15,8 +15,10 @@ content:
     label: hidden
     type: text_default
     weight: 0
+    region: content
     settings: {  }
     third_party_settings: {  }
   links:
     weight: 100
+    region: content
 hidden: {  }
diff --git a/core/profiles/standard/config/install/core.entity_view_display.node.article.default.yml b/core/profiles/standard/config/install/core.entity_view_display.node.article.default.yml
index 98a2de8ab04b..5c432527bee8 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.node.article.default.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.node.article.default.yml
@@ -22,12 +22,14 @@ content:
   body:
     type: text_default
     weight: 0
+    region: content
     settings: {  }
     third_party_settings: {  }
     label: hidden
   comment:
     type: comment_default
     weight: 110
+    region: content
     label: above
     settings:
       view_mode: default
@@ -36,6 +38,7 @@ content:
   field_image:
     type: image
     weight: -1
+    region: content
     settings:
       image_style: large
       image_link: ''
@@ -44,12 +47,14 @@ content:
   field_tags:
     type: entity_reference_label
     weight: 10
+    region: content
     label: above
     settings:
       link: true
     third_party_settings: {  }
   links:
     weight: 100
+    region: content
 hidden:
   field_image: true
   field_tags: true
diff --git a/core/profiles/standard/config/install/core.entity_view_display.node.article.rss.yml b/core/profiles/standard/config/install/core.entity_view_display.node.article.rss.yml
index 75a14a3fb456..84660b6d2bd2 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.node.article.rss.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.node.article.rss.yml
@@ -17,6 +17,7 @@ mode: rss
 content:
   links:
     weight: 100
+    region: content
 hidden:
   body: true
   comment: true
diff --git a/core/profiles/standard/config/install/core.entity_view_display.node.article.teaser.yml b/core/profiles/standard/config/install/core.entity_view_display.node.article.teaser.yml
index 43ee079e1f86..7b96908bed1c 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.node.article.teaser.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.node.article.teaser.yml
@@ -21,6 +21,7 @@ content:
   body:
     type: text_summary_or_trimmed
     weight: 0
+    region: content
     settings:
       trim_length: 600
     third_party_settings: {  }
@@ -28,6 +29,7 @@ content:
   field_image:
     type: image
     weight: -1
+    region: content
     settings:
       image_style: medium
       image_link: content
@@ -36,12 +38,14 @@ content:
   field_tags:
     type: entity_reference_label
     weight: 10
+    region: content
     settings:
       link: true
     third_party_settings: {  }
     label: above
   links:
     weight: 100
+    region: content
 hidden:
   comment: true
   field_image: true
diff --git a/core/profiles/standard/config/install/core.entity_view_display.node.page.default.yml b/core/profiles/standard/config/install/core.entity_view_display.node.page.default.yml
index dcb2d3eceee9..8afd9423ec6b 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.node.page.default.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.node.page.default.yml
@@ -16,8 +16,10 @@ content:
     label: hidden
     type: text_default
     weight: 100
+    region: content
     settings: {  }
     third_party_settings: {  }
   links:
     weight: 101
+    region: content
 hidden: {  }
diff --git a/core/profiles/standard/config/install/core.entity_view_display.node.page.teaser.yml b/core/profiles/standard/config/install/core.entity_view_display.node.page.teaser.yml
index f235a10eded6..bc7a68c5b5fd 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.node.page.teaser.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.node.page.teaser.yml
@@ -17,9 +17,11 @@ content:
     label: hidden
     type: text_summary_or_trimmed
     weight: 100
+    region: content
     settings:
       trim_length: 600
     third_party_settings: {  }
   links:
     weight: 101
+    region: content
 hidden: {  }
diff --git a/core/profiles/standard/config/install/core.entity_view_display.user.user.compact.yml b/core/profiles/standard/config/install/core.entity_view_display.user.user.compact.yml
index 4c1379244907..2ff13ad10f02 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.user.user.compact.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.user.user.compact.yml
@@ -16,6 +16,7 @@ content:
   user_picture:
     type: image
     weight: 0
+    region: content
     settings:
       image_style: thumbnail
       image_link: content
diff --git a/core/profiles/standard/config/install/core.entity_view_display.user.user.default.yml b/core/profiles/standard/config/install/core.entity_view_display.user.user.default.yml
index 9e4621d5e497..ef1fdd79ce42 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.user.user.default.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.user.user.default.yml
@@ -14,9 +14,11 @@ mode: default
 content:
   member_for:
     weight: 5
+    region: content
   user_picture:
     type: image
     weight: 0
+    region: content
     settings:
       image_style: thumbnail
       image_link: content
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayFormBaseTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayFormBaseTest.php
new file mode 100644
index 000000000000..09124c0f0218
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayFormBaseTest.php
@@ -0,0 +1,296 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\Core\Entity\Display\EntityDisplayInterface;
+use Drupal\Core\Form\FormState;
+use Drupal\field_ui\Form\EntityViewDisplayEditForm;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * @coversDefaultClass \Drupal\field_ui\Form\EntityDisplayFormBase
+ *
+ * @group Entity
+ */
+class EntityDisplayFormBaseTest extends KernelTestBase {
+
+  /**
+   * @covers ::copyFormValuesToEntity
+   */
+  public function testCopyFormValuesToEntity() {
+    $field_values = [];
+    $entity = $this->prophesize(EntityDisplayInterface::class);
+    $entity->getPluginCollections()->willReturn([]);
+
+    // A field with no initial values, with mismatched submitted values, type is
+    // hidden.
+    $entity->getComponent('new_field_mismatch_type_hidden')->willReturn([]);
+    $field_values['new_field_mismatch_type_hidden'] = [
+      'weight' => 0,
+      'type' => 'hidden',
+      'region' => 'content',
+    ];
+    $entity
+      ->setComponent('new_field_mismatch_type_hidden', [
+        'weight' => 0,
+        'region' => 'content',
+      ])
+      ->will(function($args) {
+        // On subsequent calls, getComponent() will return the newly set values,
+        // plus the updated type value.
+        $args[1] += ['type' => 'textfield'];
+        $this->getComponent($args[0])->willReturn($args[1]);
+        $this->setComponent($args[0], $args[1])->shouldBeCalled();
+      })
+      ->shouldBeCalled();
+
+    // A field with no initial values, with mismatched submitted values, type is
+    // visible.
+    $entity->getComponent('new_field_mismatch_type_visible')->willReturn([]);
+    $field_values['new_field_mismatch_type_visible'] = [
+      'weight' => 0,
+      'type' => 'textfield',
+      'region' => 'hidden',
+    ];
+    $entity
+      ->setComponent('new_field_mismatch_type_visible', [
+        'weight' => 0,
+        'type' => 'textfield',
+      ])
+      ->will(function($args) {
+        // On subsequent calls, getComponent() will return the newly set values,
+        // plus the updated region value.
+        $args[1] += ['region' => 'content'];
+        $this->getComponent($args[0])->willReturn($args[1]);
+        $this->setComponent($args[0], $args[1])->shouldBeCalled();
+      })
+      ->shouldBeCalled();
+
+    // An initially hidden field, with identical submitted values.
+    $entity->getComponent('field_hidden_no_changes')
+      ->willReturn([
+        'weight' => 0,
+        'type' => 'hidden',
+        'region' => 'hidden',
+      ]);
+    $field_values['field_hidden_no_changes'] = [
+      'weight' => 0,
+      'type' => 'hidden',
+      'region' => 'hidden',
+    ];
+    $entity->removeComponent('field_hidden_no_changes')
+      ->will(function ($args) {
+        // On subsequent calls, getComponent() will return an empty array.
+        $this->getComponent($args[0])->willReturn([]);
+      })
+      ->shouldBeCalled();
+
+    // An initially visible field, with identical submitted values.
+    $entity->getComponent('field_visible_no_changes')
+      ->willReturn([
+        'weight' => 0,
+        'type' => 'textfield',
+        'region' => 'content',
+      ]);
+    $field_values['field_visible_no_changes'] = [
+      'weight' => 0,
+      'type' => 'textfield',
+      'region' => 'content',
+    ];
+    $entity
+      ->setComponent('field_visible_no_changes', [
+        'weight' => 0,
+        'type' => 'textfield',
+        'region' => 'content',
+      ])
+      ->shouldBeCalled();
+
+    // An initially hidden field, with a submitted type change.
+    $entity->getComponent('field_start_hidden_change_type')
+      ->willReturn([
+        'weight' => 0,
+        'type' => 'hidden',
+        'region' => 'hidden',
+      ]);
+    $field_values['field_start_hidden_change_type'] = [
+      'weight' => 0,
+      'type' => 'textfield',
+      'region' => 'hidden',
+    ];
+    $entity
+      ->setComponent('field_start_hidden_change_type', [
+        'weight' => 0,
+        'type' => 'textfield',
+      ])
+      ->will(function($args) {
+        // On subsequent calls, getComponent() will return the newly set values,
+        // plus the updated region value.
+        $args[1] += ['region' => 'content'];
+        $this->getComponent($args[0])->willReturn($args[1]);
+        $this->setComponent($args[0], $args[1])->shouldBeCalled();
+      })
+      ->shouldBeCalled();
+
+    // An initially hidden field, with a submitted region change.
+    $entity->getComponent('field_start_hidden_change_region')
+      ->willReturn([
+        'weight' => 0,
+        'type' => 'hidden',
+        'region' => 'hidden',
+      ]);
+    $field_values['field_start_hidden_change_region'] = [
+      'weight' => 0,
+      'type' => 'hidden',
+      'region' => 'content',
+    ];
+    $entity
+      ->setComponent('field_start_hidden_change_region', [
+        'weight' => 0,
+        'region' => 'content',
+      ])
+      ->will(function($args) {
+        // On subsequent calls, getComponent() will return the newly set values,
+        // plus the updated type value.
+        $args[1] += ['type' => 'textfield'];
+        $this->getComponent($args[0])->willReturn($args[1]);
+        $this->setComponent($args[0], $args[1])->shouldBeCalled();
+      })
+      ->shouldBeCalled();
+
+    // An initially hidden field, with a submitted region and type change.
+    $entity->getComponent('field_start_hidden_change_both')
+      ->willReturn([
+        'weight' => 0,
+        'type' => 'hidden',
+        'region' => 'hidden',
+      ]);
+    $field_values['field_start_hidden_change_both'] = [
+      'weight' => 0,
+      'type' => 'textfield',
+      'region' => 'content',
+    ];
+    $entity
+      ->setComponent('field_start_hidden_change_both', [
+        'weight' => 0,
+        'type' => 'textfield',
+        'region' => 'content',
+      ])
+      ->will(function($args) {
+        // On subsequent calls, getComponent() will return the newly set values.
+        $this->getComponent($args[0])->willReturn($args[1]);
+      })
+      ->shouldBeCalled();
+
+    // An initially visible field, with a submitted type change.
+    $entity->getComponent('field_start_visible_change_type')
+      ->willReturn([
+        'weight' => 0,
+        'type' => 'textfield',
+        'region' => 'content',
+      ]);
+    $field_values['field_start_visible_change_type'] = [
+      'weight' => 0,
+      'type' => 'hidden',
+      'region' => 'content',
+    ];
+    $entity->removeComponent('field_start_visible_change_type')
+      ->will(function ($args) {
+        // On subsequent calls, getComponent() will return an empty array.
+        $this->getComponent($args[0])->willReturn([]);
+      })
+      ->shouldBeCalled();
+
+    // An initially visible field, with a submitted region change.
+    $entity->getComponent('field_start_visible_change_region')
+      ->willReturn([
+        'weight' => 0,
+        'type' => 'textfield',
+        'region' => 'content',
+      ]);
+    $field_values['field_start_visible_change_region'] = [
+      'weight' => 0,
+      'type' => 'textfield',
+      'region' => 'hidden',
+    ];
+    $entity->removeComponent('field_start_visible_change_region')
+      ->will(function ($args) {
+        // On subsequent calls, getComponent() will return an empty array.
+        $this->getComponent($args[0])->willReturn([]);
+      })
+      ->shouldBeCalled();
+
+    // An initially visible field, with a submitted region and type change.
+    $entity->getComponent('field_start_visible_change_both')
+      ->willReturn([
+        'weight' => 0,
+        'type' => 'textfield',
+        'region' => 'content',
+      ]);
+    $field_values['field_start_visible_change_both'] = [
+      'weight' => 0,
+      'type' => 'hidden',
+      'region' => 'hidden',
+    ];
+    $entity->removeComponent('field_start_visible_change_both')
+      ->will(function ($args) {
+        // On subsequent calls, getComponent() will return an empty array.
+        $this->getComponent($args[0])->willReturn([]);
+      })
+      ->shouldBeCalled();
+
+    // A field that is flagged for plugin settings update on the second build.
+    $entity->getComponent('field_plugin_settings_update')
+      ->willReturn([
+        'weight' => 0,
+        'type' => 'textfield',
+        'region' => 'content',
+      ]);
+    $field_values['field_plugin_settings_update'] = [
+      'weight' => 0,
+      'type' => 'textfield',
+      'region' => 'content',
+      'settings_edit_form' => [
+        'third_party_settings' => [
+          'foo' => 'bar',
+        ],
+      ],
+    ];
+    $entity
+      ->setComponent('field_plugin_settings_update', [
+        'weight' => 0,
+        'type' => 'textfield',
+        'region' => 'content',
+      ])
+      ->will(function ($args) {
+        // On subsequent calls, getComponent() will return the newly set values.
+        $this->getComponent($args[0])->willReturn($args[1]);
+        $args[1] += [
+          'settings' => [],
+          'third_party_settings' => [
+            'foo' => 'bar',
+          ],
+        ];
+        $this->setComponent($args[0], $args[1])->shouldBeCalled();
+      })
+      ->shouldBeCalled();
+
+    $form_object = new EntityViewDisplayEditForm($this->container->get('plugin.manager.field.field_type'), $this->container->get('plugin.manager.field.formatter'));
+    $form_object->setEntity($entity->reveal());
+
+    $form = [
+      '#fields' => array_keys($field_values),
+      '#extra' => [],
+    ];
+    $form_state = new FormState();
+    $form_state->setValues(['fields' => $field_values]);
+
+    $form_object->buildEntity($form, $form_state);
+
+    // Flag one field for updating plugin settings.
+    $form_state->set('plugin_settings_update', 'field_plugin_settings_update');
+    // During form submission, buildEntity() will be called twice. Simulate that
+    // here to prove copyFormValuesToEntity() is idempotent.
+    $form_object->buildEntity($form, $form_state);
+  }
+
+}
-- 
GitLab