Webform Ajaxification

Specs

Version
Drupal 7
Tools
Ajax
Form API
Webform
Created
01 Nov 2015

Summary

This snippet has 2 parts. The first part alters all webforms in to Ajax forms with Ajax confirmation screens. The second part alters a specific webform to turn it in to a multi-step ajax form.

Code

1. Form Alter and Theme Implementation

MODULE_NAME.module
Alter all webforms and add the ajax callback to the submit button.
          
/*
 * Implements hook_form_FORM_ID_alter() to modify all webforms.
 * 
 * @param array $form
 *  The form structure
 * @param array $form_state
 *  The form data
 * 
 */
function MODULE_NAME_form_webform_client_form_alter(&$form, &$form_state) {
  
  // Don't modify the submission edit form. Only modify the submission add form.
  if (isset($form['#submission']->sid)) {
    return;
  }
  
  // Wrap the form in a standard wrapper based on the node ID of the webform. This is needed
  // to handle the replace command in the ajax callback.
  $form['#prefix'] = '<div id="webform-client-form-' . $form['#node']->nid . '-wrapper" class="form-wrapper">';
  $form['#suffix'] = '</div>';

  // Add ajax callback to submit button.
  $form['actions']['submit']['#ajax'] = array(
    'callback' => 'MODULE_NAME_webform_client_form_ajax_callback',
    'wrapper' => 'webform-client-form-' . $form['#node']->nid . '-wrapper',
    'method' => 'replace',
    'effect' => 'fade',
    'event' => 'click',
  );
  
  // Add auto-focus to form to focus on its first form element. Use a static variable to only add it once in case there are
  // multiple forms on the page.
  static $add_js = TRUE;
  if ($add_js) {
    $selector = '#' . $form['#id'] . ' :input:text:visible:enabled:first';
    $autofocus_js = 'jQuery(document).ready(function(){var i = jQuery("' . $selector . '");if (i.val() == "") {i[0].focus();}});';
    drupal_add_js($autofocus_js, 'inline');
  }
  $add_js = FALSE;
  
  // Add Honeypot.
  if (module_exists('honeypot')) {
    honeypot_add_form_protection($form, $form_state, array('honeypot'));
  }
	
}

/*
 * Ajax callback for Submit RFP web form to show the ajax confirmation screen.
 * 
 * @param array $form
 *   The form structure
 * @param array $form_state
 *   The form data
 * 
 */
function MODULE_NAME_webform_client_form_ajax_callback($form, $form_state) {
  
  // If this is left out, the confirmation screen will show even when the form fails validation.
  if (form_get_errors()) {
    return $form;
  }
  
  // Calling this function will flush out the status messages to prevent the default Webform message from displaying on the next
  // page load.
  $hide_webform_messages = drupal_get_messages();

  // Ajax Confirmation screen is kept in a template to separate logic and markup. The template is passed the $form and the webform
  // submitted values in order to print a confirmation screen.
  $output = theme('webform_submit_ajax_confirmation', array('form' => $form, 'submitted' => $form_state['values']['submitted']));
  
  $commands = array();
  $commands[] = ajax_command_replace('#webform-client-form-' . $form['#node']->nid . '-wrapper', $output);
  
  ajax_deliver(array('#type' => 'ajax', '#commands' => $commands));
  exit;
  
}

/*
 * Implements hook_theme() to define the template used for the ajax confirmation screen
 */
function MODULE_NAME_theme() {
 return array(
  'webform_submit_ajax_confirmation' => array(
    'template' => 'webform-submit-ajax-confirmation',
    'variables' => array('form' => NULL, 'submitted' => NULL),
    'path' => drupal_get_path('module', 'MODULE_NAME') . '/templates',
  ),
 );
}
          
          

2. Confirmation Template

webform-submit-ajax-confirmation.tpl.php
MODULE_PATH/templates
This template prints the confirmation screen for the webform. It checks the component type, and prints text and textarea fields. Additional logic would be required to print date or other field types.
          
<div id="webform-submit-ajax-confirmation">
  <div id="webform-submit-ajax-confirmation-top">
    <i class="fa fa-check-circle-o"></i> <?php print t('Submission Received'); ?>
  </div>
  <div id="webform-submit-ajax-confirmation-bottom">
    <?php print t('We\'ll be in touch with you shortly.'); ?>
  </div>
  <div id="webform-submit-ajax-confirmation-values">
    <div id="webform-submit-ajax-confirmation-form-name">
      <?php print t('Form: !form', array('!form' => $form['#node']->title)); ?>
    </div>
    <?php foreach ($submitted as $component_id => $value): ?>
    <?php if (($form['#node']->webform['components'][$component_id]['type'] == 'textarea') || ($form['#node']->webform['components'][$component_id]['type'] == 'textfield')): ?>
    <div class="webform-submit-ajax-confirmation-item">
      <div class="webform-submit-ajax-confirmation-label">
        <label><?php print t($form['#node']->webform['components'][$component_id]['name']); ?></label>
      </div>
      <div class="webform-submit-ajax-confirmation-value">
        <?php print !empty($submitted[$component_id]) ? t(check_plain($submitted[$component_id])) : t('Empty'); ?>
      </div>
    </div>
    <?php endif; ?>
    <?php endforeach; ?>
  </div>
</div>
          
          

3. Multistep Ajax Webform

MODULE_NAME.module
This is an unrelated but useful example of how to turn a Webform in to an Ajax multi-step form.
          
/*
 * Implements hook_form_FORM_ID_alter() to modify the Submit RFP Webform.
 * 
 * @param array $form
 *   The form structure
 * @param array $form_state
 *   The form data
 * 
 */
function MODULE_NAME_form_webform_client_form_41_alter(&$form, &$form_state) {
  
  // Don't modify the submission edit form. Only modify the submission add form.
  if (isset($form['#submission']->sid)) {
    return;
  }

  // The $form_state['storage'] array is kept persisent through Ajax requests. I've had problems using the rest of the
  // $form_state array on Ajax forms. On initial page load, store the step number as 1 to be incremented in the ajax callback.
  if (empty($form_state['storage']['step'])) {
    $form_state['storage']['step'] = 1;
  }
  
  $step_descriptions = array(
    1 => t('Select a submission method'),
    2 => t('Complete the form below'),
  );
  
  $form['step_description'] = array(
    '#markup' => '<div class="submit-rfp-form-step-description">' . t('Step !number of 2: !description', array('!number' => $form_state['storage']['step'], '!description' => $step_descriptions[$form_state['storage']['step']])) . '</div>',
    '#weight' => -999,
  );

  
  if ($form_state['storage']['step'] == 1) {
    // Step 1 just displays 2 submit buttons that toggle which fields to display in step 2.
    
    // Hide all of the webform components and the form actions. 
    $form['submitted']['#access'] = FALSE;
    $form['actions']['#access'] = FALSE;
    
    // Also unset the submit handlers so the webform module does not try to process the form.
    $form['#submit'] = array();
    
    $form['webform'] = array(
      '#type' => 'submit',
      '#value' => t('Web Based Form'),
      '#suffix' => '<div class="submit-rfp-workflow-description">' . t('Fill out our helpful online form to submit your RFP.') . '</div>',
      '#submit' => array('MODULE_NAME_next_submit'),
      '#ajax' => array(
        'event' => 'click',
        'callback' => 'MODULE_NAME_return_form_ajax_callback',
        'wrapper' => 'webform-client-form-41-wrapper',
        'method' => 'replace',
        'effect' => 'fade',
        'progress' => array(
          'type' => 'throbber',
          'message' => NULL,
        ),
      ),
    );
    
    $form['file_upload'] = array(
      '#type' => 'submit',
      '#value' => t('File Upload'),
      '#suffix' => '<div class="submit-rfp-workflow-description">' . t('Upload an attachment and submit it as your RFP.') . '</div>',
      '#submit' => array('MODULE_NAME_next_submit'),
      '#ajax' => array(
        'event' => 'click',
        'callback' => 'MODULE_NAME_return_form_ajax_callback',
        'wrapper' => 'webform-client-form-41-wrapper',
        'method' => 'replace',
        'effect' => 'fade',
        'progress' => array(
          'type' => 'throbber',
          'message' => NULL,
        ),
      ),
    );
    
  }
  elseif ($form_state['storage']['step'] == 2) {
    // Step 2 displays the actual webform components based on what was selected in step 1.
    
    $form['start_over'] = array(
      '#markup' => '<div class="submit-rfp-form-start-over">' . l(t('Start Over'), 'node/41') . '</div>',
      '#weight' => -999,
    );
    
    // Either the RFP Upload managed file form element or the rest of the webform need to display
    // based on what was selected in step 1. The fields are left unrequired on the webform node and set as
    // required here to prevent validation errors between the steps.
    if ($form_state['triggering_element']['#value'] == 'Web Based Form') {
      $form['submitted']['rfp_document']['#access'] = FALSE;
      $form['submitted']['contact_info']['first_name']['#required'] = TRUE;
      $form['submitted']['contact_info']['last_name']['#required'] = TRUE;
      $form['submitted']['contact_info']['email']['#required'] = TRUE;
    }
    elseif ($form_state['triggering_element']['#value'] == 'File Upload') {
      $form['submitted']['contact_info']['#access'] = FALSE;
      $form['submitted']['meeting_info']['#access'] = FALSE;
      $form['submitted']['submission_notes']['#access'] = FALSE;
      $form['submitted']['rfp_document']['attach_rfp_document']['#required'] = TRUE;
    }
    
    $form['actions']['submit']['#value'] = t('Submit RFP');
    
  }
  
}

/*
 * Simple function to return the form. This has a generic name because it's needed often when adding Ajax
 * to any form.
 * 
 * @param array $form
 *   The form structure
 * @param array $form_state
 *   The form data
 * 
 */
function MODULE_NAME_return_form_ajax_callback($form, $form_state) {
  return $form;
}

/*
 * Processes the multi-step ajax functionality. This is given a generic name so this can be re-usable for other Ajax forms.
 * 
 * @param array $form
 *   The form structure
 * @param array $form_state
 *   The form data
 * 
 */
function MODULE_NAME_next_submit(&$form, &$form_state) {
  $form_state['storage']['step']++;
  $form_state['rebuild'] = TRUE;
}
          
          

Comments

Placeholder: I'll extend node comments with ajax and other functionality here.