diff --git a/core/lib/Drupal/Core/Entity/ContentEntityForm.php b/core/lib/Drupal/Core/Entity/ContentEntityForm.php index 35834ab..6980701 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityForm.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityForm.php @@ -69,7 +69,7 @@ public function form(array $form, FormStateInterface $form_state) { * https://www.drupal.org/node/2015613. */ public function validate(array $form, FormStateInterface $form_state) { - $entity = $this->buildEntity($form, $form_state); + $entity = $this->entity; $this->getFormDisplay($form_state)->validateFormValues($entity, $form, $form_state); // @todo Remove this. diff --git a/core/lib/Drupal/Core/Entity/EntityForm.php b/core/lib/Drupal/Core/Entity/EntityForm.php index a0b18fa..a04ec88 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,30 @@ public function processForm($element, FormStateInterface $form_state, $form) { } /** + * Form element #after_build callback: Updates the entity with submitted data. + * + * This is the default entity object builder function. It is called before any + * other AJAX, validate or submit handlers to build the new entity object to + * be used by the following AJAX, validate or submit handlers. At this point + * of the form workflow, the entity is not yet validated so we have to use a + * cloned form state object for updating the entity, thus allowing the initial + * form state to be updated. This way, the subsequently invoked handlers can + * retrieve a regular entity object to act on. + */ + 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()) { + // Remove button and internal Form API values from submitted values. + $clean_form_state = clone $form_state; + $clean_form_state->cleanValues(); + $this->entity = $this->buildEntity($element, $clean_form_state); + } + + return $element; + } + + /** * Returns the action form element for the current entity form. */ protected function actionsElement(array $form, FormStateInterface $form_state) { @@ -239,25 +263,10 @@ public function validate(array $form, FormStateInterface $form_state) { /** * {@inheritdoc} - * - * This is the default entity object builder function. It is called before any - * other submit handler to build the new entity object to be used by the - * following submit handlers. At this point of the form workflow the entity is - * validated and the form state can be updated, this way the subsequently - * invoked handlers can retrieve a regular entity object to act on. Generally - * this method should not be overridden unless the entity requires the same - * preparation for two actions, see \Drupal\comment\CommentForm for an example - * with the save and preview actions. - * - * @param array $form - * An associative array containing the structure of the form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. */ public function submitForm(array &$form, FormStateInterface $form_state) { - // Remove button and internal Form API values from submitted values. - $form_state->cleanValues(); - $this->entity = $this->buildEntity($form, $form_state); + // Nothing to do here. An updated entity object is already prepared by + // static::afterBuild(). } /** diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php index 29f4cbf..d29bd8c 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..1de0264 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 = array( + '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 += array('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/tests/config_test/config/schema/config_test.schema.yml b/core/modules/config/tests/config_test/config/schema/config_test.schema.yml index 9240d4b..f68acde 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..975e27b 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) {