[Drupal/AHAH] Multistep AHAH forms with proper AHAH behavior

Data dodania wpisu: 21-04-2011

... that's a pity hardcore job ;) Trust me or not: due to developing this pieces of code, before I found a solution for the inproper AHAH behavior on AHAH-loaded forms in a multistep AHAH forms I pulled a half of hair from my head and perform a facepalm in the end;) I've read dozens of articles about AHAH forms to find a solution. No positive results. So this is it :) The shortest possible multistep AHAH form example :)

 

Imagine: we have to develop a multiform page with AJAX behavior like a common Onepage Multistep Form. The easiest to meet is Onepage Checkout page in the online stores. As we know, these have either one such a big form with all fields required to fill-in at once or a single steps to fill-in data in a separate pages. Both have pros and cons. But what if we could deliver a possibility to have separate form steps and let user fill them in without page refresh?

 

Here comes AHAH. Let me show you example with three form steps - first form AHAH-ready, second - AHAH-loaded, third - page result based on first and second form data. In addition, we do not need to use AHAH Helper by Wim Leers here.

 

Firstly, we declare hook_menu() for module page and AHAH callbacks:

function ahah_test_menu() {
    $items['ahah_test/onepage'] = array(
        'page callback' => 'ahah_test_form_page',
        'access arguments' => array('access content'),
        'type' => MENU_CALLBACK,
    );
    $items['ajax/onepage/%/%'] = array(
        'page callback' => 'onepage_checkout_forms_js',
        'page arguments' => array(2,3),
        'access arguments' => array('access content'),
        'type' => MENU_CALLBACK,
    );
    return $items;
}

Next: two forms and submit callbacks:

 

function ahah_test_step_first_form($form, &$form_state) {
    $form = array();
    $form['firstname'] = array(
        '#type' => 'textfield',
        '#title' => 'Firstname',
        '#default_value' => $_SESSION['ahah_test_onepage']['firstname'],
        '#required' => TRUE,
    );
    $form['submit'] = array(
        '#type' => 'submit',
        '#id' => 'first_step_submit', // Ad. 1 (explanation below)
        '#value' => 'Submit',
        '#ahah' => array(
            'event' => 'click',
            'path' => 'ajax/onepage/first_step_submit/0',
           'wrapper' => 'ahah-output', // Ad. 2 (explanation below)
            'method' => 'replace',
            'effect' => 'fade',
        ),
    );

    return $form;
}

function ahah_test_step_second_form($form, &$form_state) {
    $form = array();
    $form['lastname'] = array(
        '#type' => 'textfield',
        '#title' => 'Lastname',
        '#default_value' => $_SESSION['ahah_test_onepage']['lastname'],
        '#required' => TRUE,
    );
    $form['submit'] = array(
        '#type' => 'submit',
        '#id' => 'second_step_submit', // Ad. 1 (explanation below)
        '#value' => 'Submit',
        '#ahah' => array(
            'event' => 'click',
            'path' => 'ajax/onepage/second_step_submit/0',
            'wrapper' => 'ahah-output', // Ad. 2 (explanation below)
            'method' => 'replace',
            'effect' => 'fade',
        ),
    );

    return $form;
}

function ahah_test_step_first_form_submit($form, &$form_state) {
    $_SESSION['ahah_test_onepage']['firstname'] = $form_state['values']['firstname'];
}

function ahah_test_step_second_form_submit($form, &$form_state) {
    $_SESSION['ahah_test_onepage']['lastname'] = $form_state['values']['lastname'];
}

 

Ad. 1 THIS IS THE MOST IMPORTANT ATTRIBUTE TO MAKE AHAH-LOADED FORMS AHAH-READY! If we do not set up this attribute to the submit form field as an absolute unique value (instead of ID rendered dynamically by Drupal Form API) - our AHAH-loaded form will fail with AHAH behavior, because AHAH binding after loading each form step will be processed on a first enabled form in a page ever (ie. login form, register form, search form).

 

Ad. 2 Output (messages and other JSON data) generated in an AHAH callbacks will be inserted to the HTML element with such an ID value.

 

function ahah_test_form_page() {
    $output = '<div id="onepage-wrapper">';
    $output .= '<div id="ahah-output"></div>'; // wrapper for AHAH callback output
    $output .= '<div id="ahah_test-step-first-form">' . drupal_get_form('ahah_test_step_first_form') . '</div>';
    $output .= '<div id="ahah_test-step-second-form">' . ($_SESSION['ahah_test_onepage']['step_first_completed'] ? drupal_get_form('ahah_test_step_second_form') : '') . '</div>';
    $output .= '<div id="ahah-forms-result"></div>'; // final result output after second form
    $output .= '</div>';
    return $output;
}

We declare AHAH callback to process each available form step:

 

function onepage_checkout_forms_js($op = 'list', $arg = 0) {
    switch ($op) {
        case 'first_step_submit':
            $form = ahah_test_form_handler($arg); // we process current form by custom developed form handler
            $output = theme('status_messages');
            $second_step_form = drupal_get_form('ahah_test_step_second_form');
            if (empty($output)) { // if there is no messages like errors ($output is empty)
                $output .= '<script type="text/javascript">
                    $(document).ready(function() {
                        $("#ahah_test-step-second-form").empty().html(' . drupal_to_js($second_step_form) . ');
                    });
                </script>';
            }
            break;
        case 'second_step_submit':
            $form = ahah_test_form_handler($arg);
            $output = theme('status_messages');
            $forms_result = ahah_test_forms_result(); // final page results
            if (empty($output)) {
                $output .= '<script type="text/javascript">
                    $(document).ready(function() {
                        $("#ahah-forms-result").empty().html(' . drupal_to_js($forms_result) . ');
                    });
                </script>';
            }
            break;
    }
    // In order to make AHAH-loaded forms AHAH-ready, we need to send new Drupal.settings
    // with updated AHAH-form bindings ready to use by javascript callout
    $javascript = drupal_add_js(NULL, NULL, 'header');
    $settings = call_user_func_array('array_merge_recursive', $javascript['setting']);
    // we bind form elements using a new Drupal.settings for AHAH fields
    $output .= '<script type="text/javascript">
        $(document).ready(function() {
            // Yeap, this is from misc/ahah.js :) We need such a thing to make AHAH-loaded forms AHAH-ready
            var buttons = ' . drupal_to_js($settings['ahah']) . ';
            for (var base in buttons) {
                if(!$("#" + base + ".ahah-processed").length > 0) {
                    var element_settings = buttons[base];
                    $(element_settings.selector).each(function() {
                       element_settings.element = this;
                       var ahah = new Drupal.ahah(base, element_settings);
                    });
                    $("#" + base).addClass("ahah-processed");
                }
            }
        });
    </script>';

    drupal_json(array(
        'status' => TRUE,
        'data' => $output,
        'settings' => array('ahah' => $settings['ahah']),
    ));
}

Final results, based on the content inserted in both previous steps:

 

function ahah_test_forms_result() {
    return 'Username: ' . $_SESSION['ahah_test_onepage']['firstname'] . ' ' . $_SESSION['ahah_test_onepage']['lastname'];
}

The last thing is to create form handler to process and rebuild form steps:

 

function ahah_test_form_handler($delta = 0) {
    include_once 'modules/node/node.pages.inc';
    $form_state = array('storage' => NULL, 'submitted' => FALSE);
    $form_build_id = $_POST['form_build_id'];

    $form = form_get_cache($form_build_id, $form_state);
    $args = $form['#parameters'];
    $form_id = array_shift($args);

    $form_state['post'] = $form['#post'] = $_POST;
    $form['#programmed'] = $form['#redirect'] = FALSE;

    $form_state['remove_delta'] = $delta;
    drupal_process_form($form_id, $form, $form_state);

    if (form_get_errors ()) {
        form_execute_handlers('submit', $form, $form_state);
    }

    $form = drupal_rebuild_form($form_id, $form_state, $args, $form_build_id);

    return $form;
}


DEMONSTRATION, a little bit more complex: availabe on my test online store based on Drupal: www.dis-cart.designend.net (in Polish, you can use GoogleTranslate if needed:)), just add something to the cart, goto order, select quick order as a guest and fill-in all onepage checkout steps by AJAX. If the order process will fail in the end, don't remind me. This does have a big changes in modules right now;) Orders won't be processed;)

 

And there is something more for these ones, that wan't a ready module to check out: DOWNLOAD :) 

Comments

Although the solution works in a way, the problem with the above solution is that form_get_cache fails if the form is being submitted again after having an error. The solution is still not a good one.
Comments closed...

DesignEnd on Facebook

Inspiration