diff --git a/core/lib/Drupal/Core/Form/EventSubscriber/FormAjaxSubscriber.php b/core/lib/Drupal/Core/Form/EventSubscriber/FormAjaxSubscriber.php index ffbd8f3..ef8b420 100644 --- a/core/lib/Drupal/Core/Form/EventSubscriber/FormAjaxSubscriber.php +++ b/core/lib/Drupal/Core/Form/EventSubscriber/FormAjaxSubscriber.php @@ -7,9 +7,12 @@ namespace Drupal\Core\Form\EventSubscriber; +use Drupal\Core\EventSubscriber\MainContentViewSubscriber; use Drupal\Core\Form\FormAjaxException; use Drupal\Core\Form\FormAjaxResponseBuilderInterface; +use Drupal\Core\Form\FormBuilderInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; use Symfony\Component\HttpKernel\KernelEvents; @@ -36,6 +39,21 @@ public function __construct(FormAjaxResponseBuilderInterface $form_ajax_response } /** + * Alters the wrapper format if this is an AJAX form request. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent $event + * The event to process. + */ + public function onView(GetResponseForControllerResultEvent $event) { + // Ensure this request is rendered as HTML so that we can manage the + // creation of an AJAX response directly in self::onException(). + $request = $event->getRequest(); + if ($request->query->has(FormBuilderInterface::AJAX_FORM_REQUEST)) { + $request->query->set(MainContentViewSubscriber::WRAPPER_FORMAT, 'html'); + } + } + + /** * Catches a form AJAX exception and build a response from it. * * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event @@ -95,6 +113,8 @@ protected function getFormAjaxException(\Exception $e) { public static function getSubscribedEvents() { // Run before exception.logger. $events[KernelEvents::EXCEPTION] = ['onException', 51]; + // Run before main_content_view_subscriber. + $events[KernelEvents::VIEW][] = ['onView', 1]; return $events; } diff --git a/core/lib/Drupal/Core/Form/FormAjaxResponseBuilder.php b/core/lib/Drupal/Core/Form/FormAjaxResponseBuilder.php index 4b826ff..293475c 100644 --- a/core/lib/Drupal/Core/Form/FormAjaxResponseBuilder.php +++ b/core/lib/Drupal/Core/Form/FormAjaxResponseBuilder.php @@ -60,8 +60,7 @@ public function buildResponse(Request $request, array $form, FormStateInterface // We need to return the part of the form (or some other content) that needs // to be re-rendered so the browser can update the page with changed - // content. Since this is the generic menu callback used by many Ajax - // elements, it is up to the #ajax['callback'] function of the element (may + // content. It is up to the #ajax['callback'] function of the element (may // or may not be a button) that triggered the Ajax request to determine what // needs to be rendered. $callback = NULL; diff --git a/core/lib/Drupal/Core/Form/FormBuilderInterface.php b/core/lib/Drupal/Core/Form/FormBuilderInterface.php index 125199d..60ad704 100644 --- a/core/lib/Drupal/Core/Form/FormBuilderInterface.php +++ b/core/lib/Drupal/Core/Form/FormBuilderInterface.php @@ -13,7 +13,17 @@ interface FormBuilderInterface { /** - * Request key for a form that was triggered via AJAX. + * Request key for AJAX forms that submit to the form's original route. + * + * This constant is distinct from a "drupal_ajax" value for + * \Drupal\Core\EventSubscriber\MainContentViewSubscriber::WRAPPER_FORMAT, + * because that one is set for all AJAX submissions, including ones with + * dedicated routes for which self::buildForm() should not exit early via a + * \Drupal\Core\Form\FormAjaxException. + * + * @todo Re-evaluate the need for this constant after + * https://www.drupal.org/node/2502785 and + * https://www.drupal.org/node/2503429. */ const AJAX_FORM_REQUEST = 'ajax_form'; diff --git a/core/lib/Drupal/Core/Render/Element/RenderElement.php b/core/lib/Drupal/Core/Render/Element/RenderElement.php index 73b4c36..c6f5944 100644 --- a/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\Core\EventSubscriber\MainContentViewSubscriber; use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\PluginBase; @@ -129,10 +128,14 @@ public static function preRenderGroup($element) { * @see self::preRenderAjaxForm() */ public static function processAjaxForm(&$element, FormStateInterface $form_state, &$complete_form) { - if (!empty($element['#ajax']['url'])) { + $element = static::preRenderAjaxForm($element); + + // If the element was processed as an #ajax element, and a custom URL was + // provided, set the form to be cached. + if (!empty($element['#ajax_processed']) && !empty($element['#ajax']['url'])) { $form_state->setCached(); } - return static::preRenderAjaxForm($element); + return $element; } /** @@ -237,12 +240,11 @@ public static function preRenderAjaxForm($element) { // to be substantially different for a JavaScript triggered submission. // One such substantial difference is form elements that use // #ajax['callback'] for determining which part of the form needs - // re-rendering. For that, we have a special 'system/ajax' route. - if (isset($settings['callback']) && !isset($settings['url'])) { + // re-rendering. For that, we have a special 'system.ajax' route which + // must be manually set. + if (array_key_exists('callback', $settings) && !isset($settings['url'])) { $settings['url'] = Url::fromRoute(''); $settings['options']['query'][FormBuilderInterface::AJAX_FORM_REQUEST] = TRUE; - // Specify that we expect HTML back, despite the AJAX request. - $settings['options']['query'][MainContentViewSubscriber::WRAPPER_FORMAT] = 'html'; } $settings += array( 'url' => NULL, diff --git a/core/modules/file/src/Controller/FileWidgetAjaxController.php b/core/modules/file/src/Controller/FileWidgetAjaxController.php index 6cc1391..90176f8 100644 --- a/core/modules/file/src/Controller/FileWidgetAjaxController.php +++ b/core/modules/file/src/Controller/FileWidgetAjaxController.php @@ -10,12 +10,7 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\ReplaceCommand; -use Drupal\Core\Form\FormAjaxResponseBuilderInterface; -use Drupal\Core\Form\FormBuilderInterface; -use Drupal\Core\Render\RendererInterface; use Drupal\system\Controller\FormAjaxController; -use Psr\Log\LoggerInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; @@ -26,42 +21,6 @@ class FileWidgetAjaxController extends FormAjaxController { /** - * The renderer. - * - * @var \Drupal\Core\Render\RendererInterface - */ - protected $renderer; - - /** - * Constructs a FileWidgetAjaxController object. - * - * @param \Psr\Log\LoggerInterface $logger - * A logger instance. - * @param \Drupal\Core\Form\FormBuilderInterface $form_builder - * The form builder. - * @param \Drupal\Core\Form\FormAjaxResponseBuilderInterface $form_ajax_response_builder - * The form AJAX response builder. - * @param \Drupal\Core\Render\RendererInterface $renderer - * The renderer. - */ - public function __construct(LoggerInterface $logger, FormBuilderInterface $form_builder, FormAjaxResponseBuilderInterface $form_ajax_response_builder, RendererInterface $renderer) { - parent::__construct($logger, $form_builder, $form_ajax_response_builder); - $this->renderer = $renderer; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('logger.factory')->get('ajax'), - $container->get('form_builder'), - $container->get('form_ajax_response_builder'), - $container->get('renderer') - ); - } - - /** * Processes AJAX file uploads and deletions. * * @param \Symfony\Component\HttpFoundation\Request $request diff --git a/core/modules/system/src/Controller/FormAjaxController.php b/core/modules/system/src/Controller/FormAjaxController.php index 65078a8..4908197 100644 --- a/core/modules/system/src/Controller/FormAjaxController.php +++ b/core/modules/system/src/Controller/FormAjaxController.php @@ -11,6 +11,9 @@ use Drupal\Core\Form\FormAjaxResponseBuilderInterface; use Drupal\Core\Form\FormState; use Drupal\Core\Form\FormBuilderInterface; +use Drupal\Core\Render\MainContent\MainContentRendererInterface; +use Drupal\Core\Render\RendererInterface; +use Drupal\Core\Routing\RouteMatchInterface; use Drupal\system\FileAjaxForm; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -37,6 +40,27 @@ class FormAjaxController implements ContainerInjectionInterface { protected $formBuilder; /** + * The renderer. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** + * The main content to AJAX Response renderer. + * + * @var \Drupal\Core\Render\MainContent\MainContentRendererInterface + */ + protected $ajaxRenderer; + + /** + * The current route match. + * + * @var \Drupal\Core\Routing\RouteMatchInterface + */ + protected $routeMatch; + + /** * The form AJAX response builder. * * @var \Drupal\Core\Form\FormAjaxResponseBuilderInterface @@ -50,12 +74,19 @@ class FormAjaxController implements ContainerInjectionInterface { * A logger instance. * @param \Drupal\Core\Form\FormBuilderInterface $form_builder * The form builder. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer. + * @param \Drupal\Core\Render\MainContent\MainContentRendererInterface $ajax_renderer + * The main content to AJAX Response renderer. + * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * The current route match. * @param \Drupal\Core\Form\FormAjaxResponseBuilderInterface $form_ajax_response_builder * The form AJAX response builder. */ - public function __construct(LoggerInterface $logger, FormBuilderInterface $form_builder, FormAjaxResponseBuilderInterface $form_ajax_response_builder) { + public function __construct(LoggerInterface $logger, FormBuilderInterface $form_builder, RendererInterface $renderer, MainContentRendererInterface $ajax_renderer, RouteMatchInterface $route_match, FormAjaxResponseBuilderInterface $form_ajax_response_builder) { $this->logger = $logger; $this->formBuilder = $form_builder; + $this->renderer = $renderer; $this->formAjaxResponseBuilder = $form_ajax_response_builder; } @@ -66,6 +97,9 @@ public static function create(ContainerInterface $container) { return new static( $container->get('logger.factory')->get('ajax'), $container->get('form_builder'), + $container->get('renderer'), + $container->get('main_content_renderer.ajax'), + $container->get('current_route_match'), $container->get('form_ajax_response_builder') ); } diff --git a/core/modules/system/src/Tests/Ajax/AjaxFormCacheTest.php b/core/modules/system/src/Tests/Ajax/AjaxFormCacheTest.php index b70040f..f28adf1 100644 --- a/core/modules/system/src/Tests/Ajax/AjaxFormCacheTest.php +++ b/core/modules/system/src/Tests/Ajax/AjaxFormCacheTest.php @@ -17,7 +17,7 @@ class AjaxFormCacheTest extends AjaxTestBase { /** - * {@inheritdoc} + * Tests the usage of form cache for AJAX forms. */ public function testFormCacheUsage() { /** @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $key_value_expirable */ diff --git a/core/modules/system/src/Tests/Ajax/DialogTest.php b/core/modules/system/src/Tests/Ajax/DialogTest.php index e9e90b7..3422c4e 100644 --- a/core/modules/system/src/Tests/Ajax/DialogTest.php +++ b/core/modules/system/src/Tests/Ajax/DialogTest.php @@ -168,7 +168,6 @@ public function testDialog() { 'event' => 'click', 'url' => Url::fromRoute('ajax_test.dialog_form', [], ['query' => [ FormBuilderInterface::AJAX_FORM_REQUEST => TRUE, - MainContentViewSubscriber::WRAPPER_FORMAT => 'html', ]])->toString(), 'dialogType' => 'ajax', 'submit' => [ diff --git a/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestSimpleForm.php b/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestSimpleForm.php index 1f76850..225664d 100644 --- a/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestSimpleForm.php +++ b/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestSimpleForm.php @@ -10,7 +10,6 @@ use Drupal\Core\Form\FormBase; use Drupal\ajax_forms_test\Callbacks; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Url; /** * Form builder: Builds a form that triggers a simple AJAX callback. @@ -69,11 +68,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#type' => 'select', '#title' => $this->t('Test %key callbacks', array('%key' => $key)), '#options' => array('red' => 'red'), - '#ajax' => array( - 'callback' => $value, - // If the callback is NULL, the URL needs to be specified. - 'url' => $value === NULL ? Url::fromRoute('system.ajax') : NULL, - ), + '#ajax' => array('callback' => $value), ); }