diff -u b/modules/webform_ajax/tests/src/FunctionalJavascript/WebformAjaxTest.php b/modules/webform_ajax/tests/src/FunctionalJavascript/WebformAjaxTest.php --- b/modules/webform_ajax/tests/src/FunctionalJavascript/WebformAjaxTest.php +++ b/modules/webform_ajax/tests/src/FunctionalJavascript/WebformAjaxTest.php @@ -10,6 +10,7 @@ * * @group webform_ajax * + * @todo Test the back link. * @todo This lumps a number of different tests together. AJAX is quite a cross- * cutting concern and I'd welcome input on how best to slice up the tests. */ @@ -18,50 +19,49 @@ /** * Modules to enable. * - * @var array + * @var string[] */ - public static $modules = ['filter', 'webform_ajax']; + public static $modules = ['webform_ajax']; /** * A basic test of an AJAX submission with a message confirmation. */ public function testAjaxSubmissionMessageConfirmation() { - $provider = new WebformAjaxProvider(); - $webform = $provider->webformWithTextfield(); + $webform = $this->provider->webformWithTextfield(); $webform->setSetting('confirmation_type', 'message'); $webform->save(); $path = $webform->toUrl()->getInternalPath(); $assert = $this->assertSession(); $this->drupalPostForm($path, ['textfield' => 'test value'], t('Submit')); - $element = $assert->waitForElement('css', '.webform-confirmation'); + $element = $assert->waitForElement('css', '.messages--status'); $this->assertNotNull($element, "An AJAX submission shows a confirmation message."); $assert->pageTextContains('New submission added to Test webform'); $assert->pageTextContains('test value'); - - // Bug: With the message confirmation type the form doesn't reset on - // submission. - // $assert->fieldValueNotEquals('textfield', 'test value'); + $assert->fieldValueNotEquals('textfield', 'test value'); } /** * A basic test of an AJAX submission with inline confirmation. - * - * @todo FIX THIS TEST. At the moment it passes even without AJAX enabled - * (which is strange as the same logic seems to work for the message test). */ public function testAjaxSubmissionInlineConfirmation() { // For now use the test_element_text webform as an easy check. - $provider = new WebformAjaxProvider(); - $webform = $provider->webformWithTextfield(); + $webform = $this->provider->webformWithTextfield(); $webform->setSetting('confirmation_type', 'inline'); $webform->save(); $path = $webform->toUrl()->getInternalPath(); + // @todo Use an API call to get the form ID. + $form_id = 'webform_submission_' . $webform->id() . '_form'; + // The inline confirmation message is wrapped in the same wrapper the form + // was. + // @todo Use an API to get the prefix. + $locator = "#webform-ajax-$form_id .webform-confirmation"; $assert = $this->assertSession(); + $this->drupalPostForm($path, ['textfield' => 'test value'], t('Submit')); - $element = $assert->waitForElement('css', '.webform-confirmation'); + $element = $assert->waitForElement('css', $locator); - $this->assertNotNull($element, "An AJAX submission shows a confirmation message."); + $this->assertNotNull($element, "An AJAX submission shows an inline confirmation message."); $assert->pageTextContains('New submission added to Test webform'); $assert->pageTextContains('test value'); } @@ -70,18 +70,18 @@ * A basic test of two AJAX submission with a message confirmation. */ public function testMultipleAjaxSubmissionMessageConfirmation() { - $provider = new WebformAjaxProvider(); - $webform = $provider->webformWithTextfield(); + $webform = $this->provider->webformWithTextfield(); $webform->setSetting('confirmation_type', 'message'); $webform->save(); $path = $webform->toUrl()->getInternalPath(); $assert = $this->assertSession(); + $this->drupalPostForm($path, ['textfield' => 'test value'], t('Submit')); - $assert->waitForElement('css', '.webform-confirmation'); + $assert->waitForElement('css', '.messages--status'); $this->drupalPostForm(NULL, ['textfield' => 'test value 2'], t('Submit')); // I'm not sure there's an element I can wait on. $assert->assertWaitOnAjaxRequest(); - $element = $assert->waitForElement('css', '.webform-confirmation'); + $element = $assert->waitForElement('css', '.messages--status'); $this->assertNotNull($element, "An AJAX submission shows a confirmation message."); $assert->pageTextContains('New submission added to Test webform'); @@ -94,14 +94,14 @@ * @see \Drupal\webform\Tests\WebformSubmissionStorageTest::testSubmissionStorage() */ public function testAjaxSubmissionStorage() { - $provider = new WebformAjaxProvider(); - $webform = $provider->webformWithTextfield(); + $webform = $this->provider->webformWithTextfield(); $webform->setSetting('confirmation_type', 'message'); $webform->save(); $path = $webform->toUrl()->getInternalPath(); $assert = $this->assertSession(); + $this->drupalPostForm($path, ['textfield' => 'test value'], t('Submit')); - $assert->waitForElement('css', '.webform-confirmation'); + $assert->waitForElement('css', '.messages--status'); /** @var \Drupal\webform\WebformSubmissionStorageInterface $storage */ $storage = \Drupal::entityTypeManager()->getStorage('webform_submission'); @@ -114,12 +114,12 @@ * @todo Look into why client-side validation doesn't seem to kick in. */ public function testRequiredTextField() { - $provider = new WebformAjaxProvider(); - $webform = $provider->webformWithTextfield(['#required' => TRUE]); + $webform = $this->provider->webformWithTextfield(['#required' => TRUE]); $webform->setSetting('confirmation_type', 'message'); $webform->save(); $path = $webform->toUrl()->getInternalPath(); $assert = $this->assertSession(); + $this->drupalPostForm($path, ['textfield' => ''], t('Submit')); $element = $assert->waitForElement('css', '.messages--error'); @@ -131,21 +131,81 @@ * Test that messages from submissions replace previous ones. */ public function testMessages() { - $provider = new WebformAjaxProvider(); - $webform = $provider->webformWithTextfield(); + $webform = $this->provider->webformWithTextfield(); $webform->setSetting('confirmation_type', 'message'); $webform->save(); $path = $webform->toUrl()->getInternalPath(); $assert = $this->assertSession(); + $this->drupalPostForm($path, ['textfield' => 'test value'], t('Submit')); - $assert->waitForElement('css', '.webform-confirmation'); + $assert->waitForElement('css', '.messages--warning'); $this->drupalPostForm(NULL, ['textfield' => 'test value 2'], t('Submit')); $assert->assertWaitOnAjaxRequest(); - $element = $assert->waitForElement('css', '.webform-confirmation'); + $element = $assert->waitForElement('css', '.messages--warning'); $this->assertNotNull($element, "An AJAX submission shows a confirmation message."); $assert->elementsCount('css', '.messages--warning', 1); } + + /** + * Test a simple two page form submits on both pages. + */ + public function testMultiPageForms() { + $webform = $this->provider->multipageWebformWithTextfields(); + $webform->setSetting('confirmation_type', 'message'); + $webform->save(); + $path = $webform->toUrl()->getInternalPath(); + $assert = $this->assertSession(); + + $this->drupalPostForm($path, ['textfield_1' => 'test value page 1'], t('Next Page >')); + $element = $assert->waitForButton('Submit'); + + $this->assertNotNull($element, "An AJAX submission on the first page of a multi-page form causes the next page to show"); + $assert->pageTextContains('test value page 1'); + + $this->drupalPostForm(NULL, ['textfield_2' => 'test value page 2'], t('Submit')); + $element = $assert->waitForElement('css', '.messages--status'); + + $this->assertNotNull($element, "An AJAX submission on the last page of a multi-page form shows the webform confirmation."); + $assert->pageTextContains('test value page 2'); + $assert->pageTextContains('Textfield Page 1'); + $assert->pageTextNotContains('Textfield Page 2'); + } + + /** + * Test the back link. + */ + public function testBackLink() { + // For now use the test_element_text webform as an easy check. + $webform = $this->provider->webformWithTextfield(); + $webform->setSetting('confirmation_type', 'inline'); + $webform->save(); + $path = $webform->toUrl()->getInternalPath(); + $assert = $this->assertSession(); + + $this->drupalPostForm($path, ['textfield' => 'test value'], t('Submit')); + $back_link = $assert->waitForLink('Back to form'); + + $this->assertNotNull($back_link, "An AJAX submission confirmation shows a 'Back to form' link."); + $back_link->click(); + $submit = $assert->waitForButton('Submit'); + $this->assertNotNull($submit, "The 'Back to form' link causes the form to display."); + $assert->pageTextNotContains('New submission added to Test webform'); + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + $this->provider = new WebformAjaxProvider(); + } + + /** + * @var \Drupal\Tests\webform_ajax\FunctionalJavascript\WebformProvider $provider + */ + protected $provider; + } /** @@ -172,15 +232,59 @@ return $this->createWebform([ 'title' => 'Test webform', 'elements' => [ - 'textfield' => $properties + [ - '#type' => 'textfield', - '#title' => 'Textfield', + 'textfield' => $this->textfield($properties), + ] + ]); + } + + /** + * Create a two page webform with a textfield on each page. + * + * It uses the following structure. + * page_1 (Page 1): + * - textfield_1 (Textfield 1) + * page_2 (Page 2): + * - textfield_2 (Textfield 2) + * + * @return \Drupal\webform\WebformInterface + */ + public function multipageWebformWithTextfields() { + return $this->createWebform([ + 'title' => 'Test webform', + 'elements' => [ + 'page_1' => [ + '#type' => 'webform_wizard_page', + '#title' => 'Page 1', + 'textfield_1' => $this->textfield(['#title' => 'Textfield Page 1']), + ], + 'page_2' => [ + '#type' => 'webform_wizard_page', + '#title' => 'Page 2', + 'textfield_2' => $this->textfield(['#title' => 'Textfield Page 2']), ] ] ]); } /** + * Create a textfield FAPI array, optionally with custom properties. + * + * By default the title is 'Textfield'. + * + * @param array $properties + * Custom properties that take precedence over the defaults. + * + * @return array + * The FAPI array + */ + public function textfield($properties = []) { + return $properties + [ + '#type' => 'textfield', + '#title' => 'Textfield', + ]; + } + + /** * Create a new webform entity. * * @param array $values diff -u b/modules/webform_ajax/webform_ajax.module b/modules/webform_ajax/webform_ajax.module --- b/modules/webform_ajax/webform_ajax.module +++ b/modules/webform_ajax/webform_ajax.module @@ -4,29 +4,36 @@ * @file * Provides AJAX submission for webforms. * - * @todo Double check the tests fail without AJAX! - * @todo Reset the form on successful submission if confirmation_type is message - * @todo Fix the back link - * @todo Overwrite rather than prepend AJAX content so messages don't stack up. + * Questions: + * - Is there any point in allowing AJAX submissions of forms with a redirect + * confirmation? + * * @todo Add more tests for server-side validation. * @todo Check the implementation against the reference on #2757491 and * https://api.drupal.org/api/drupal/core%21core.api.php/group/ajax/8.3.x * @todo Remove inappropriate confirmation options. - * @todo Expand test coverage! + * @todo Expand test coverage: + * - multiple step forms with preview and save draft enabled * @todo I think I saw that webform automatically loads from MODULE.webform.inc. * @todo Scroll up on submission. - * @todo Ensure there's no 'Back to form' link with 'message' confirmation. */ use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\BeforeCommand; +use Drupal\Core\Ajax\RemoveCommand; use Drupal\Core\Ajax\ReplaceCommand; +use Drupal\Core\Form\FormState; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element\StatusMessages; +use Drupal\Core\Url; +use Drupal\webform\WebformInterface; +use Drupal\webform\WebformMessageManagerInterface; +use Drupal\webform\WebformSubmissionInterface; /** * The prefix to use for the AJAX wrapper ID. */ -const WEBFORM_AJAX_PREFIX = 'webform_ajax_'; +const WEBFORM_AJAX_PREFIX = 'webform-ajax-'; /** * Implements hook_form_BASE_FORM_ID_alter() for webform_submission_form. @@ -48,18 +55,19 @@ return; } + $form = _webform_ajax_wrap_element($form, WEBFORM_AJAX_PREFIX . $form_id); $element_id = WEBFORM_AJAX_PREFIX . $form_id; - $old_prefix = !empty($form['#prefix']) ? $form['#prefix'] : ''; - $old_suffix = !empty($form['suffix']) ? $form['suffix'] : ''; - $form['#prefix'] = '
' . $old_prefix; - $form['#suffix'] = $old_suffix . '
'; - // Adjust the form to use ajax submit. - $form['actions']['submit']['#ajax'] = array( - 'callback' => 'webform_ajax_callback', - 'wrapper' => $element_id, - 'effect' => 'fade', - ); + // @todo: Add draft. + foreach (['previous', 'next', 'submit'] as $button) { + if (!empty($form['actions'][$button])) { + $form['actions'][$button]['#ajax'] = array( + 'callback' => 'webform_ajax_callback', + 'wrapper' => $element_id, + 'effect' => 'fade', + ); + } + } } /** @@ -68,50 +76,130 @@ function webform_ajax_callback($form, FormStateInterface &$form_state) { /** @var \Drupal\Core\Entity\ContentEntityFormInterface $form_object */ $form_object = $form_state->getFormObject(); - /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */ - $webform_submission = $form_object->getEntity(); - $webform = $webform_submission->getWebform(); + /** @var \Drupal\webform\WebformSubmissionInterface $submission */ + $submission = $form_object->getEntity(); + $webform = $submission->getWebform(); $element_id = WEBFORM_AJAX_PREFIX . $form_object->getFormId(); + $prefix = '#' . $element_id; - // Get the results of drupal_set_message() calls. - $messages = StatusMessages::renderMessages(NULL); $response = new AjaxResponse(); - - $output = array(); - $output[] = $messages; - if (!$form_state->getErrors()) { - /** @var \Drupal\webform\WebformMessageManagerInterface $message_manager */ - $source_entity = $webform_submission->getSourceEntity(); - - $output[] = [ - '#theme' => 'webform_confirmation__ajax', - '#webform' => $webform, - '#source_entity' => $source_entity, - '#webform_submission' => $webform_submission, - ]; - - /** @var \Drupal\Core\Render\RendererInterface $renderer */ - $renderer = \Drupal::service('renderer'); - $renderer->addCacheableDependency($output, $webform); - $renderer->addCacheableDependency($output, $source_entity); - - if ($webform->getSetting('confirmation_type') == 'message') { - // Redisplay the form. - // @todo Reset the form values. - $output[] = $form; + $output = []; + $confirmation_type = $webform->getSetting('confirmation_type'); + if ($submission->getState() == WebformSubmissionInterface::STATE_COMPLETED) { + switch ($confirmation_type) { + case 'message': + // Currently the message set by webform itself is being squashed by + // \Drupal\webform\WebformMessageManager::display() with an explicit + // AJAX check.. + // @todo: Neaten this up + _webform_display_confirmation_message($submission); + $output = _webform_ajax_get_fresh_submission_form($webform); + break; + + case 'inline': + $confirmation = webform_ajax_confirmation_message($submission, $element_id); + // Add the same wrapper as the form (messages go above it, and it gets + // replaced when clicking the back link). + $output = _webform_ajax_wrap_element($confirmation, $element_id); + break; } } else { - $output[] = $form; + // Display the incomplete form. + $output = $form; } - - $prefix = '#' . $element_id; $response->addCommand(new ReplaceCommand($prefix, $output)); + // As far as I know there's no JS command for displaying status messages. + // - clear our messages every submission; + // - add the messages before the form and/or confirmation message; + // - replace the form as usual. + $message_wrapper_id = $element_id . '-status-messages'; + // Clear out any old confirmation messages... + $response->addCommand(new RemoveCommand('#' . $message_wrapper_id)); + // ...and add in any new ones. + if (drupal_get_messages(NULL, FALSE)) { + $messages = StatusMessages::renderMessages(NULL); + $messages = _webform_ajax_wrap_element($messages, $message_wrapper_id); + $response->addCommand(new BeforeCommand('#' . $element_id, $messages)); + } + return $response; } /** + * Return a new webform submission form with no user input. + * + * It would be nice to use WebformInterface::getSubmissionForm() but that uses + * the input from the current request to populate the form. + * + * @param \Drupal\webform\WebformInterface $webform + * The webform to make a submission form from. + * + * @return array + * The built form array for the submission form. + */ +function _webform_ajax_get_fresh_submission_form(WebformInterface $webform) { + $submission = \Drupal::entityTypeManager() + ->getStorage('webform_submission') + ->create(['webform_id' => $webform->id()]); + $form_object = \Drupal::entityTypeManager() + ->getFormObject('webform_submission', 'default'); + $form_object->setEntity($submission); + $form_state = (new FormState())->setFormState([]); + // Explicitly set the user input to nothing, so the form is built on + // that, rather than the request's input. + $form_state->setUserInput([]); + /** @var \Drupal\Core\Form\FormBuilderInterface $form_builder */ + $form_builder = \Drupal::service('form_builder'); + return $form_builder->buildForm($form_object, $form_state); +} + +/** + * Returns a render array for a submission's confirmation message. + * + * @param \Drupal\webform\WebformSubmissionInterface $submission + * The webform submission + * + * @return array + * The confirmation message render array + */ +function webform_ajax_confirmation_message(WebformSubmissionInterface $submission, $element_id) { + $webform = $submission->getWebform(); + /** @var \Drupal\webform\WebformMessageManagerInterface $message_manager */ + $source_entity = $submission->getSourceEntity(); + $url = Url::fromRoute('webform_ajax.return_webform', [ + 'webform' => $webform->id(), + 'wrapper_id' => $element_id + ]); + $output = [ + '#theme' => 'webform_confirmation__ajax', + '#webform' => $webform, + '#source_entity' => $source_entity, + '#webform_submission' => $submission, + '#attached' => [ + 'library' => [ + 'webform_ajax/webform_ajax' + ], + 'drupalSettings' => [ + 'webform_ajax' => [ + 'url' => $url->toString(), + 'webform' => $webform->get('id'), + 'wrapper_id' => $element_id, + ] + ] + ] + ]; + + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + $renderer->addCacheableDependency($output, $webform); + $renderer->addCacheableDependency($output, $source_entity); + + return $output; +} + +/** * Implements hook_form_FORM_ID_alter() for webform_third_party_settings_form. * * Add webform_ajax settings to an individual webform. @@ -134,0 +223,49 @@ + +/** + * Safely wrap a render array with an ID using #prefix and #suffix. + * + * @param array $element + * The render array to wrap. + * @param $id + * The ID of the wrapping element. + * + * @return array + * The new render array. + */ +function _webform_ajax_wrap_element($element, $id) { + foreach (['#prefix', '#suffix'] as $property) { + if (!isset($element[$property])) { + $element[$property] = ''; + } + } + $element['#prefix'] = '
' . $element['#prefix']; + $element['#suffix'] .= '
'; + return $element; +} + +/** + * Display the submission confirmation message. + * + * This is ugly - I'm duplicating code from WebformMessageManager::display() + * to avoid the AJAX check. + * + * @param \Drupal\webform\WebformSubmissionInterface $submission + * The submission being confirmed. + * + * @see \Drupal\webform\WebformMessageManager::display() + */ +function _webform_display_confirmation_message(WebformSubmissionInterface $submission) { + /** @var \Drupal\webform\WebformMessageManagerInterface $message_manager */ + $message_manager = \Drupal::service('webform.message_manager'); + $message_manager->setWebform($submission->getWebform()); + $message_manager->setSourceEntity($submission->getSourceEntity()); + $message_manager->setWebformSubmission($submission); + if (!$build = $message_manager->build(WebformMessageManagerInterface::SUBMISSION_CONFIRMATION)) { + $build = $message_manager->build(WebformMessageManagerInterface::SUBMISSION_DEFAULT_CONFIRMATION); + } + if ($build) { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + drupal_set_message($renderer->renderPlain($build), 'status'); + } +} only in patch2: unchanged: --- /dev/null +++ b/modules/webform_ajax/js/webform_ajax.js @@ -0,0 +1,23 @@ +(function($) { + Drupal.behaviors.webform_ajax = { + attach : function(context, settings) { + var wrapper_id = settings.webform_ajax.wrapper_id; + var ajax_settings = { + url: settings.webform_ajax.url, + event: 'click', + progress: { + type: 'throbber' + } + }; + + // Bind Ajax behaviors to Webform confirmation screen's "Go back to form" + // link. + $(context) + .find('.webform-confirmation__back a') + .once('webform_ajax') + .each(function() { + Drupal.Ajax[wrapper_id] = new Drupal.Ajax(wrapper_id, this, ajax_settings); + }); + } + }; +}(jQuery)); only in patch2: unchanged: --- /dev/null +++ b/modules/webform_ajax/src/Controller/WebformAjaxController.php @@ -0,0 +1,43 @@ +addCommand(new RemoveCommand("#$wrapper_id-status-messages")); + // Display the webform only if it is enabled. Otherwise, show messages. + if ($webform->isOpen()) { + $form = Webform::load($webform->get('id')); + $output = \Drupal::entityManager()->getViewBuilder('webform')->view($form); + } else { + $output = array ( + '#type' => 'markup', + '#markup' => t('The webform cannot be displayed.') + ); + } + + $response->addCommand(new ReplaceCommand('#' . $wrapper_id, $output)); + + return $response; + } +} \ No newline at end of file only in patch2: unchanged: --- /dev/null +++ b/modules/webform_ajax/webform_ajax.libraries.yml @@ -0,0 +1,7 @@ +webform_ajax: + version: 1.x + js: + js/webform_ajax.js: {} + dependencies: + - core/jquery + - core/drupalSettings \ No newline at end of file