diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php index be0a22f456f8a9db363f5743bf3caf18572faded..d00b4cc4563446efb8f503429f9930cf3e76eab9 100644 --- a/core/lib/Drupal/Core/Form/FormBuilder.php +++ b/core/lib/Drupal/Core/Form/FormBuilder.php @@ -570,6 +570,18 @@ public function processForm($form_id, &$form, FormStateInterface &$form_state) { $unprocessed_form = $form; $form = $this->doBuildForm($form_id, $form, $form_state); + // Allow an Ajax callback while the form is operating in GET mode. For + // example, when using HOOK_form_views_exposed_form_alter. + if ($form_state->isMethodType('get')) { + $triggering_element_name = $this->requestStack->getCurrentRequest()->request->get('_triggering_element_name'); + $triggering_element = $form_state->getTriggeringElement(); + if (isset($triggering_element['#name']) + && $triggering_element['#name'] == $triggering_element_name + && isset($triggering_element['#ajax'])) { + throw new FormAjaxException($form, $form_state); + } + } + // Only process the input if we have a correct form submission. if ($form_state->isProcessingInput()) { // Form values for programmed form submissions typically do not include a diff --git a/core/modules/views/src/ViewExecutable.php b/core/modules/views/src/ViewExecutable.php index 87739a90c96c9381a6b6936abafdcb39336c9e95..70658d6eb0a5d8904e54b79eab844f2f0b0dac85 100644 --- a/core/modules/views/src/ViewExecutable.php +++ b/core/modules/views/src/ViewExecutable.php @@ -745,6 +745,43 @@ public function getExposedInput() { $this->initDisplay(); $this->exposed_input = $this->request->query->all(); + // Allow AJAX requests on exposed filters. + if ($this->request->isMethod('post') && $this->request->request->get('_triggering_element_name')) { + $post_form_data = $this->request->request->all(); + $exposed_field_names = []; + // Go through each handler and let it generate its exposed widget. + foreach ($this->display_handler->handlers as $type => $value) { + /** @var \Drupal\views\Plugin\views\ViewsHandlerInterface $handler */ + foreach ($this->$type as $handler) { + if ($handler->canExpose() && $handler->isExposed()) { + // Pick up POST data for all the exposed handlers. + if (!empty($handler->options['expose']['use_operator']) && !empty($handler->options['expose']['operator_id'])) { + $exposed_field_names[] = $handler->options['expose']['operator_id']; + } + if (!empty($handler->options['expose']['identifier'])) { + if ($handler->isAGroup()) { + $exposed_field_names[] = $handler->options['group_info']['identifier']; + } + else { + $exposed_field_names[] = $handler->options['expose']['identifier']; + } + } + } + } + } + foreach ($exposed_field_names as $exposed_field_name) { + foreach ($post_form_data as $post_form_key => $post_form_value) { + if ($post_form_key === $exposed_field_name || str_starts_with($post_form_key, "{$exposed_field_name}_")) { + // Pick up the exposed field and any extra variations starting + // with the same field name. + $this->exposed_input += [ + $post_form_key => $post_form_value, + ]; + } + } + } + } + // Unset items that are definitely not our input: foreach (['page', 'q'] as $key) { if (isset($this->exposed_input[$key])) { diff --git a/core/modules/views/tests/modules/views_test_exposed_filter/views_test_exposed_filter.info.yml b/core/modules/views/tests/modules/views_test_exposed_filter/views_test_exposed_filter.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..965b6a8d347bcc59b40a3754e86faf9a96aa79c0 --- /dev/null +++ b/core/modules/views/tests/modules/views_test_exposed_filter/views_test_exposed_filter.info.yml @@ -0,0 +1,7 @@ +name: 'Views Test Exposed Filter' +type: module +description: 'Alters Views exposed filter form for testing AJAX callbacks.' +package: Testing +version: VERSION +dependencies: + - drupal:views diff --git a/core/modules/views/tests/modules/views_test_exposed_filter/views_test_exposed_filter.module b/core/modules/views/tests/modules/views_test_exposed_filter/views_test_exposed_filter.module new file mode 100644 index 0000000000000000000000000000000000000000..39ef041d7a790acb94e58d594392e817ee3c8264 --- /dev/null +++ b/core/modules/views/tests/modules/views_test_exposed_filter/views_test_exposed_filter.module @@ -0,0 +1,36 @@ +Default prefix'; + } +} + +/** + * Returns render array via an AJAX callback for testing. + * + * @param array $form + * The form definition array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state object. + * + * @return array + * Render array to display when the AJAX callback is triggered. + */ +function views_test_exposed_filter_ajax_callback(array &$form, FormStateInterface $form_state): array { + return [ + '#markup' => 'Callback called.', + ]; +} diff --git a/core/modules/views/tests/src/FunctionalJavascript/ExposedFilterAJAXTest.php b/core/modules/views/tests/src/FunctionalJavascript/ExposedFilterAJAXTest.php index 6d61ecef80a0757ca90b2ddd80de2be5ba70678f..782c03bd694d4e5ff676275b4bdff2c61a48c396 100644 --- a/core/modules/views/tests/src/FunctionalJavascript/ExposedFilterAJAXTest.php +++ b/core/modules/views/tests/src/FunctionalJavascript/ExposedFilterAJAXTest.php @@ -27,6 +27,7 @@ class ExposedFilterAJAXTest extends WebDriverTestBase { 'views', 'views_test_modal', 'user_test_views', + 'views_test_config', ]; /** @@ -39,7 +40,7 @@ class ExposedFilterAJAXTest extends WebDriverTestBase { * * @var array */ - public static $testViews = ['test_user_name']; + public static $testViews = ['test_user_name', 'test_content_ajax']; /** * {@inheritdoc} @@ -258,4 +259,31 @@ public function testExposedFilterErrorMessages(): void { $this->assertSession()->pageTextContainsOnce(sprintf('There are no users matching "%s"', $name)); } + /** + * Tests if Ajax events can be attached to the exposed filter form. + */ + public function testExposedFilterAjaxCallback(): void { + ViewTestData::createTestViews(self::class, ['views_test_config']); + + // Attach an Ajax event to all 'title' fields in the exposed filter form. + \Drupal::service('module_installer')->install(['views_test_exposed_filter']); + $this->resetAll(); + $this->rebuildContainer(); + $this->container->get('module_handler')->reload(); + + $this->drupalGet('test-content-ajax'); + + $page = $this->getSession()->getPage(); + $this->assertSession()->pageTextContains('Default prefix'); + + $page->fillField('title', 'value'); + + // Simulate a click outside the title field so the title field ajax callback + // kicks in. It does not matter here what action is carried out here. + $page->selectFieldOption('status', 'Published'); + $this->assertSession()->assertWaitOnAjaxRequest(); + + $this->assertSession()->pageTextContains('Callback called.'); + } + }