diff -u b/core/core.api.php b/core/core.api.php --- b/core/core.api.php +++ b/core/core.api.php @@ -2079,6 +2079,13 @@ * - message: Translated message to display. * - url: For a bar progress indicator, URL path for determining progress. * - interval: For a bar progress indicator, how often to update it. + * - url: A \Drupal\Core\URL to which to submit the Ajax request. If omitted, + * defaults to either the same URL as the form or link destination is for + * someone with JavaScript disabled, or a slightly modified version (e.g., + * with a query parameter added, removed, or changed) of that URL if + * necessary to support Drupal's content negotiation. It is recommended to + * omit this key and use Drupal's content negotiation rather than using + * substantially different URLs between Ajax and non-Ajax. * * @subsection sub_callback Setting up a callback to process Ajax * Once you have set up your form to trigger an Ajax response (see @ref sub_form reverted: --- b/core/includes/pager.inc +++ a/core/includes/pager.inc @@ -148,12 +148,7 @@ function pager_get_query_parameters() { $query = &drupal_static(__FUNCTION__); if (!isset($query)) { + $query = UrlHelper::filterQueryParameters(\Drupal::request()->query->all(), array('page')); - // There is a chance that the pager is rendered in the context of an AJAX - // form request. Therefore exclude AJAX_FORM_REQUEST from from the query - // arguments, as otherwise clicking the link would be interpreted - // potentially as an AJAX form request, especially when done as POST request - // using JS. - $query = UrlHelper::filterQueryParameters(\Drupal::request()->query->all(), array('page', \Drupal\Core\Form\FormBuilderInterface::AJAX_FORM_REQUEST)); } return $query; } diff -u b/core/lib/Drupal/Core/Render/Element/RenderElement.php b/core/lib/Drupal/Core/Render/Element/RenderElement.php --- b/core/lib/Drupal/Core/Render/Element/RenderElement.php +++ b/core/lib/Drupal/Core/Render/Element/RenderElement.php @@ -7,7 +7,6 @@ namespace Drupal\Core\Render\Element; -use Drupal\Component\Utility\NestedArray; use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\PluginBase; @@ -143,6 +142,7 @@ * Properties used: * - #ajax['event'] * - #ajax['prevent'] + * - #ajax['url'] * - #ajax['callback'] * - #ajax['options'] * - #ajax['wrapper'] @@ -152,9 +152,6 @@ * * @return array * The processed element with the necessary JavaScript attached to it. - * - * @throws \InvalidArgumentException - * Thrown when an element provides a custom #ajax URL. */ public static function preRenderAjaxForm($element) { // Skip already processed elements. @@ -169,12 +166,6 @@ return $element; } - // If #ajax is TRUE, convert it to an empty array. Depending on the #type, - // it will still be given an event. - if ($element['#ajax'] === TRUE) { - $element['#ajax'] = []; - } - // Add a reasonable default event handler if none was specified. if (isset($element['#ajax']) && !isset($element['#ajax']['event'])) { switch ($element['#type']) { @@ -233,23 +224,26 @@ $settings = $element['#ajax']; - // Assign default settings. + // Assign default settings. When 'url' is set to NULL, ajax.js submits the + // Ajax request to the same URL as the form or link destination is for + // someone with JavaScript disabled. This is generally preferred as a way to + // ensure consistent server processing for js and no-js users, and Drupal's + // content negotiation takes care of formatting the response appropriately. + // However, 'url' and 'options' may be set when wanting server processing + // to be substantially different for a JavaScript triggered submission. $settings += [ + 'url' => NULL, + 'options' => ['query' => []], 'dialogType' => 'ajax', ]; - if (array_key_exists('callback', $settings)) { + if (array_key_exists('callback', $settings) && !isset($settings['url'])) { + $settings['url'] = Url::fromRoute(''); // Add all the current query parameters in order to ensure that we build // the same form on the AJAX POST requests. For example, // \Drupal\user\AccountForm takes query parameters into account in order // to hide the password field dynamically. - $query = \Drupal::request()->query->all(); - $query[FormBuilderInterface::AJAX_FORM_REQUEST] = TRUE; - - // Add options['query'] => [] as default value. - $settings = NestedArray::mergeDeep($settings, ['options' => ['query' => []]]); - $options['query'] = $settings['options']['query'] + $query; - - $settings['url'] = Url::fromRoute('', [], $options)->toString(); + $settings['options']['query'] += \Drupal::request()->query->all(); + $settings['options']['query'][FormBuilderInterface::AJAX_FORM_REQUEST] = TRUE; } // @todo Legacy support. Remove in Drupal 8. @@ -257,6 +251,13 @@ $settings['method'] = 'replaceWith'; } + // Convert \Drupal\Core\Url object to string. + if (isset($settings['url']) && $settings['url'] instanceof Url) { + $settings['url'] = $settings['url']->setOptions($settings['options'])->toString(); + } + else { + $settings['url'] = NULL; + } unset($settings['options']); // Add special data to $settings['submit'] so that when this element reverted: --- b/core/modules/views/src/Plugin/views/field/Field.php +++ a/core/modules/views/src/Plugin/views/field/Field.php @@ -447,7 +447,9 @@ '#title' => $this->t('Formatter'), '#options' => $formatters, '#default_value' => $this->options['type'], + '#ajax' => array( + 'url' => views_ui_build_form_url($form_state), + ), - '#ajax' => TRUE, '#submit' => array(array($this, 'submitTemporaryForm')), '#executes_submit_callback' => TRUE, ); reverted: --- b/core/modules/views_ui/src/Form/Ajax/ConfigHandler.php +++ a/core/modules/views_ui/src/Form/Ajax/ConfigHandler.php @@ -181,7 +181,9 @@ '#value' => $this->t('Remove'), '#submit' => array(array($this, 'remove')), '#limit_validation_errors' => array(array('override')), + '#ajax' => array( + 'url' => Url::fromRoute(''), + ), - '#ajax' => TRUE, '#button_type' => 'danger', ); } reverted: --- b/core/modules/views_ui/src/Form/Ajax/RearrangeFilter.php +++ a/core/modules/views_ui/src/Form/Ajax/RearrangeFilter.php @@ -134,7 +134,7 @@ 'class' => array('views-remove-group'), ), '#group' => $id, + '#ajax' => ['url' => NULL], - '#ajax' => TRUE, ); } $group_options[$id] = $id == 1 ? $this->t('Default group') : $this->t('Group @group', array('@group' => $id)); @@ -218,7 +218,7 @@ '#attributes' => array( 'class' => array('views-add-group'), ), + '#ajax' => ['url' => NULL], - '#ajax' => TRUE, ); return $form; reverted: --- b/core/modules/views_ui/src/Tests/PreviewTest.php +++ a/core/modules/views_ui/src/Tests/PreviewTest.php @@ -278,7 +278,7 @@ * @param int $row_count * The expected number of rows in the preview. */ + protected function assertPreviewAJAX($result, $row_count) { - protected function assertPreviewAJAX(array $result, $row_count) { // Has AJAX callback replied with an insert command? If so, we can // assume that the page content was updated with AJAX returned data. $result_commands = array(); reverted: --- b/core/modules/views_ui/src/ViewPreviewForm.php +++ a/core/modules/views_ui/src/ViewPreviewForm.php @@ -76,6 +76,7 @@ * {@inheritdoc} */ protected function actions(array $form, FormStateInterface $form_state) { + $view = $this->entity; return array( '#attributes' => array( 'id' => 'preview-submit-wrapper', @@ -88,7 +89,7 @@ '#submit' => array('::submitPreview'), '#id' => 'preview-submit', '#ajax' => array( + 'url' => Url::fromRoute('entity.view.preview_form', ['view' => $view->id(), 'display_id' => $this->displayID]), - 'callback' => [get_called_class(), 'previewSubmit'], 'wrapper' => 'views-preview-wrapper', 'event' => 'click', 'progress' => array('type' => 'fullscreen'), @@ -99,19 +100,6 @@ } /** - * AJAX callback: Return the form as-is to let the views UI be rebuilt. - * - * @param array $form - * An associative array containing the structure of the form. - * - * @return array - * The form, with no changes. - */ - public static function previewSubmit(array $form) { - return $form; - } - - /** * Form submission handler for the Preview button. */ public function submitPreview($form, FormStateInterface $form_state) { reverted: --- b/core/modules/views_ui/src/ViewUI.php +++ a/core/modules/views_ui/src/ViewUI.php @@ -12,9 +12,7 @@ use Drupal\Component\Utility\Xss; use Drupal\Core\Config\Entity\ThirdPartySettingsInterface; use Drupal\Core\EventSubscriber\AjaxResponseSubscriber; -use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Url; use Drupal\views\Views; use Drupal\Core\Entity\EntityStorageInterface; @@ -313,6 +311,11 @@ $names = array(t('Apply'), t('Apply and continue')); } + // Views provides its own custom handling of AJAX form submissions. Usually + // this happens at the same path, but custom paths may be specified in + // $form_state. + $form_url = $form_state->get('url') ?: Url::fromRouteMatch(\Drupal::routeMatch()); + // Forms that are purely informational set an ok_button flag, so we know not // to create an "Apply" button for them. if (!$form_state->get('ok_button')) { @@ -327,7 +330,9 @@ // take care of running the regular submit handler as appropriate. '#submit' => array(array($this, 'standardSubmit')), '#button_type' => 'primary', + '#ajax' => array( + 'url' => $form_url, + ), - '#ajax' => TRUE, ); // Form API button click detection requires the button's #value to be the // same between the form build of the initial page request, and the @@ -353,7 +358,9 @@ '#value' => !$form_state->get('ok_button') ? t('Cancel') : t('Ok'), '#submit' => array($cancel_submit), '#validate' => array(), + '#ajax' => array( + 'path' => $form_url, + ), - '#ajax' => TRUE, '#limit_validation_errors' => array(), ); @@ -543,10 +550,6 @@ $request_stack = \Drupal::requestStack(); $current_request = $request_stack->getCurrentRequest(); $executable = $this->getExecutable(); - /** @var \Drupal\Core\Render\RendererInterface $renderer */ - $renderer = \Drupal::service('renderer'); - /** @var \Drupal\Core\Render\RenderCacheInterface $render_cache */ - $render_cache = \Drupal::service('render_cache'); // Determine where the query and performance statistics should be output. $config = \Drupal::config('views.settings'); @@ -575,7 +578,7 @@ // have some input in the query parameters, so we merge request() and // query() to ensure we get it all. $exposed_input = array_merge(\Drupal::request()->request->all(), \Drupal::request()->query->all()); + foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', AjaxResponseSubscriber::AJAX_REQUEST_PARAMETER, 'ajax_page_state', 'form_id', 'form_build_id', 'form_token') as $key) { - foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', FormBuilderInterface::AJAX_FORM_REQUEST, AjaxResponseSubscriber::AJAX_REQUEST_PARAMETER, 'ajax_page_state', 'form_id', 'form_build_id', 'form_token') as $key) { if (isset($exposed_input[$key])) { unset($exposed_input[$key]); } @@ -609,13 +612,10 @@ $raw_parameters->set('display_id', $display_id); $request->attributes->set('_raw_variables', $raw_parameters); - $request->query->remove(FormBuilderInterface::AJAX_FORM_REQUEST); - foreach ($args as $key => $arg) { $request->attributes->set('arg_' . $key, $arg); } $request_stack->push($request); - $executable->setRequest($request_stack->getCurrentRequest()); // Suppress contextual links of entities within the result set during a // Preview. @@ -855,14 +855,6 @@ ]; } - if (!empty($output['preview'])) { - // We render the preview now, in order to render it in the context of the - // previous setup request, because for example pager links depend on the - // "current" URL. - $renderer->renderPlain($output['preview']); - $output['preview'] = $render_cache->getCacheableRenderArray($output['preview']); - } - // Ensure that we just remove an additional request we pushed earlier. // This could happen if $errors was not empty. if ($request_stack->getCurrentRequest() != $current_request) { diff -u b/core/tests/Drupal/Tests/Core/Render/Element/RenderElementTest.php b/core/tests/Drupal/Tests/Core/Render/Element/RenderElementTest.php --- b/core/tests/Drupal/Tests/Core/Render/Element/RenderElementTest.php +++ b/core/tests/Drupal/Tests/Core/Render/Element/RenderElementTest.php @@ -115,22 +115,2 @@ - /** - * @covers ::preRenderAjaxForm - */ - public function testPreRenderAjaxFormAjaxTrue() { - $element = [ - '#type' => 'select', - '#id' => 'test', - '#ajax' => TRUE, - ]; - - $element = RenderElement::preRenderAjaxForm($element); - - $this->assertTrue($element['#ajax_processed']); - $ajax_settings = [ - 'event' => 'change', - 'dialogType' => 'ajax', - ]; - $this->assertSame($ajax_settings, $element['#attached']['drupalSettings']['ajax']['test']); - } - }