From 80cad41f1f29db1da88ce7cdfc4a99b7dbb571b8 Mon Sep 17 00:00:00 2001 From: Axel Rutz <axel.rutz@machbarmacher.net> Date: Wed, 26 Jun 2019 19:56:17 +0200 Subject: [PATCH] Issue #2476569: Suppress validation of required fields on ajax calls --- core/lib/Drupal/Core/Field/WidgetBase.php | 1 + .../Drupal/Core/Form/FormBuilderInterface.php | 5 +++ core/lib/Drupal/Core/Form/FormValidator.php | 8 +++- .../Core/Render/Element/RenderElement.php | 5 +++ .../tests/modules/form_test/form_test.module | 37 +++++++++++++++ .../tests/src/Functional/Form/RebuildTest.php | 45 ++++++++++++++++++- 6 files changed, 99 insertions(+), 2 deletions(-) diff --git a/core/lib/Drupal/Core/Field/WidgetBase.php b/core/lib/Drupal/Core/Field/WidgetBase.php index 63671576b2..93c318b99b 100644 --- a/core/lib/Drupal/Core/Field/WidgetBase.php +++ b/core/lib/Drupal/Core/Field/WidgetBase.php @@ -251,6 +251,7 @@ protected function formMultipleElements(FieldItemListInterface $items, array &$f 'callback' => [get_class($this), 'addMoreAjax'], 'wrapper' => $wrapper_id, 'effect' => 'fade', + 'suppress_required_fields_validation' => TRUE, ], ]; } diff --git a/core/lib/Drupal/Core/Form/FormBuilderInterface.php b/core/lib/Drupal/Core/Form/FormBuilderInterface.php index ca794379db..5d98504fb3 100644 --- a/core/lib/Drupal/Core/Form/FormBuilderInterface.php +++ b/core/lib/Drupal/Core/Form/FormBuilderInterface.php @@ -22,6 +22,11 @@ */ const AJAX_FORM_REQUEST = 'ajax_form'; + /** + * Request key for AJAX forms to suppress validation of required fields. + */ + const AJAX_SKIP_REQUIRED_VALIDATION = 'ajax_skip_required_validation'; + /** * Determines the ID of a form. * diff --git a/core/lib/Drupal/Core/Form/FormValidator.php b/core/lib/Drupal/Core/Form/FormValidator.php index 57d24cc21e..bbab3d6f4c 100644 --- a/core/lib/Drupal/Core/Form/FormValidator.php +++ b/core/lib/Drupal/Core/Form/FormValidator.php @@ -239,6 +239,12 @@ protected function doValidateForm(&$elements, FormStateInterface &$form_state, $ } } + // The validation of required fields might be suppressed for ajax calls. + $request = $this->requestStack->getCurrentRequest(); + $requires_validation = isset($elements['#required']) && $elements['#required']; + // Ensure there is an existing request with enforced validation suppression. + $suppress_required_fields_validation = $requires_validation && $request && $request->query->has(FormBuilderInterface::AJAX_FORM_REQUEST) && $request->query->has(FormBuilderInterface::AJAX_SKIP_REQUIRED_VALIDATION); + // Validate the current input. if (!isset($elements['#validated']) || !$elements['#validated']) { // The following errors are always shown. @@ -287,7 +293,7 @@ protected function doValidateForm(&$elements, FormStateInterface &$form_state, $ // #element_validate handlers changed any properties. If $is_empty_value // is defined, then above #required validation code ran, so the other // variables are also known to be defined and we can test them again. - if (isset($is_empty_value) && ($is_empty_multiple || $is_empty_string || $is_empty_value || $is_empty_null)) { + if (!$suppress_required_fields_validation && isset($is_empty_value) && ($is_empty_multiple || $is_empty_string || $is_empty_value)) { if (isset($elements['#required_error'])) { $form_state->setError($elements, $elements['#required_error']); } diff --git a/core/lib/Drupal/Core/Render/Element/RenderElement.php b/core/lib/Drupal/Core/Render/Element/RenderElement.php index 35814589b3..0d5f27ba45 100644 --- a/core/lib/Drupal/Core/Render/Element/RenderElement.php +++ b/core/lib/Drupal/Core/Render/Element/RenderElement.php @@ -348,6 +348,11 @@ public static function preRenderAjaxForm($element) { // to hide the password field dynamically. $settings['options']['query'] += \Drupal::request()->query->all(); $settings['options']['query'][FormBuilderInterface::AJAX_FORM_REQUEST] = TRUE; + + // Set query option to enforce validation suppression of required fields. + if (isset($settings['suppress_required_fields_validation']) && $settings['suppress_required_fields_validation'] === TRUE) { + $settings['options']['query'][FormBuilderInterface::AJAX_SKIP_REQUIRED_VALIDATION] = TRUE; + } } // @todo Legacy support. Remove in Drupal 8. diff --git a/core/modules/system/tests/modules/form_test/form_test.module b/core/modules/system/tests/modules/form_test/form_test.module index 14e011877a..e78310a5ad 100644 --- a/core/modules/system/tests/modules/form_test/form_test.module +++ b/core/modules/system/tests/modules/form_test/form_test.module @@ -5,6 +5,8 @@ * Helper module for the form API tests. */ +use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Field\WidgetBase; use Drupal\Core\Form\FormStateInterface; /** @@ -111,3 +113,38 @@ function form_test_form_form_test_vertical_tabs_access_form_alter(&$form, &$form function form_test_tableselect_ajax_callback($form, FormStateInterface $form_state) { return $form['tableselect']; } + +/** + * Implements hook_form_FORM_ID_alter(). + * + * Used to add a form validate handler to the add more widget. + * + * @param $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + * @param $form_id + * + * @see Drupal\system\Tests\Form\RebuildTest::testGeneratingMultipleRequiredFieldItemsPerAjax + */ +function form_test_form_node_form_edit_ajax_test_form_alter(&$form, FormStateInterface $form_state, $form_id) { + $form['field_ajax_test_required']['widget']['add_more']['#validate'][] = 'form_test_add_more_validate'; +} + +/** + * Validation handler for the "Add another item" button. + */ +function form_test_add_more_validate(array $form, FormStateInterface $form_state) { + $button = $form_state->getTriggeringElement(); + + if ($button['#name'] == 'field_ajax_test_required_add_more') { + // Go one level up in the form, to the widgets container. + $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1)); + $field_name = $element['#field_name']; + $parents = $element['#field_parents']; + + // Increment the items count. + $field_state = WidgetBase::getWidgetState($parents, $field_name, $form_state); + if ($field_state['items_count'] >= 1) { + $form_state->setError($element, 'Not more than three elements are allowed.'); + } + } +} diff --git a/core/modules/system/tests/src/Functional/Form/RebuildTest.php b/core/modules/system/tests/src/Functional/Form/RebuildTest.php index 3e951e2e80..877878d6b8 100644 --- a/core/modules/system/tests/src/Functional/Form/RebuildTest.php +++ b/core/modules/system/tests/src/Functional/Form/RebuildTest.php @@ -28,7 +28,7 @@ class RebuildTest extends BrowserTestBase { protected function setUp() { parent::setUp(); - $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']); + $this->drupalCreateContentType(['type' => 'form_edit_ajax_test', 'name' => 'Ajax test page']); $this->webUser = $this->drupalCreateUser(['access content']); $this->drupalLogin($this->webUser); @@ -58,4 +58,47 @@ public function testRebuildPreservesValues() { $assert_session->fieldValueEquals('edit-text-2', 'DEFAULT 2'); } + /** + * Tests generating instances of required fields per ajax. + * + * The user should be able to generate more than one item at once of a field + * which is marked as required, without filling each of the field items out. + */ + public function testGeneratingMultipleRequiredFieldItemsPerAjax() { + // Create a multi-valued field for 'page' nodes to use for Ajax testing. + $field_name = 'field_ajax_test_required'; + entity_create('field_storage_config', [ + 'field_name' => $field_name, + 'entity_type' => 'node', + 'type' => 'text', + 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + ])->save(); + entity_create('field_config', [ + 'field_name' => $field_name, + 'entity_type' => 'node', + 'bundle' => 'form_edit_ajax_test', + 'required' => TRUE, + ])->save(); + entity_get_form_display('node', 'form_edit_ajax_test', 'default') + ->setComponent($field_name, ['type' => 'text_textfield']) + ->save(); + + // Log in a user who can create 'form_edit_ajax_test' nodes. + $this->webUser = $this->drupalCreateUser(['create form_edit_ajax_test content']); + $this->drupalLogin($this->webUser); + + // Get the form for adding a 'page' node. Submit an "add another item" Ajax + // submission and verify it worked by ensuring the updated page has two text + // field items in the field for which we just added an item. + $this->drupalGet('node/add/form_edit_ajax_test'); + $this->drupalPostAjaxForm(NULL, [], ['field_ajax_test_required_add_more' => t('Add another item')], NULL, [], [], 'node-form-edit-ajax-test-form'); + $this->assert(count($this->xpath('//div[contains(@class, "field-name-field-ajax-test-required")]//input[@type="text"]')) == 2, 'AJAX submission of required field succeeded.'); + + // Adding a third field per ajax. Here the validate function + // form_test_add_more_validate should set an error, because it limits the + // number of allowed fields up to two. + $this->drupalPostAjaxForm(NULL, [], ['field_ajax_test_required_add_more' => t('Add another item')], NULL, [], [], 'node-form-edit-ajax-test-form'); + $this->assert(count($this->xpath('//div[contains(@class, "field-name-field-ajax-test-required")]//input[@type="text"]')) == 2, 'Correctly no third field item was added after an AJAX submission.'); + } + } -- 2.17.1