[Drupal/AHAH] Wielopoziomowe formularze ładowane przez AHAH

Data dodania wpisu: 21-04-2011

... czyli task dla hardkorowców:P Chyba, że formularz ma dwa pola, to o czym my gadamy;) A o co chodzi? Aby zrobić wielopoziomowy formularz (multistep form), którego kolejne kroki (również formularze) są ładowane poprzez AJAX (Drupal AHAH), ale jednocześnie zachowują one również możliwość działania w trybie AJAXowym.

 

No ale do rzeczy. Wyobraźmy sobie sytuację, że potrzebujemy wykonać grupę formularzy, które niczym Onepage Multistep Form, są ładowane krok po kroku za pomocą wywołań ajaksowych. Najprostszym przykładem może być realizacja tzw. Onepage Checkout - zamówienia w sklepach internetowych. A jak wiemy, każdy sklep ma albo jeden mega duży formularz, w którym trzeba wypełnić wszystkie pola za jednym zamachem, albo kolejne kroki są na kolejnych podstronach realizacji zamówienia. Jedno i drugie ma swoje plusy i minusy. Ale co, gdyby tak poszczególne kroki odseparować od siebie, ale jednocześnie pozwolić użytkownikowi przejść procedurę bez przeładowania strony ? :)

 

I tu w Drupalu z pomocą przychodzi z pomocą dość uporczywy AHAH. Omówię poniżej przykładowe dwa kroki formularzy, z czego jeden ładowany przez AHAH oraz istotę problemu, która występuje przy dynamicznie ładowanych elementach - bindowanie eventów na załadowanych AHAH'em formularzach.

 

Podam też, dlaczego moduł Wima Leersa (Drupalowcy wiedzą o kogo chodzi) zwany AHAH Helper nie jest tutaj potrzebny (a jakże!). Oczywiście dalej mowa o dynamicznych eventach na formularzach ładowanych przez AHAH :)

 

Na początku zadeklarujemy w hook_menu() router, dzięki któremu obsłużymy AHAH'owy callback oraz podstronę z formularzami:

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;
}

Ok, mamy router, jedziemy dalej - tworzymy dwa formularze i submity dla nich, w pierwszym wprowadzimy imię, w drugim nazwisko, aby po wysłaniu drugiego formularza wyświetlić zawartość obu pól.

 

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 (poniżej)
        '#value' => 'Submit',
        '#ahah' => array(
            'event' => 'click',
            'path' => 'ajax/onepage/first_step_submit/0',
           'wrapper' => 'ahah-output', // Ad. 2 (poniżej)
            '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 (poniżej)
        '#value' => 'Submit',
        '#ahah' => array(
            'event' => 'click',
            'path' => 'ajax/onepage/second_step_submit/0',
            'wrapper' => 'ahah-output', // Ad. 2 (poniżej)
            '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'];
}

Ok, o co chodzi z Ad. 1 i Ad. 2 :)

 

Ad. 1 TO JEST NAJWAŻNIEJSZA LINIJKA TEGO KODU, ŻEBY AHAH DZIAŁAŁ SPRAWNIE W KAŻDYM KROKU! Jeśli nie zadeklarujemy przyciskom zatwierdzenia formularzy (submitom) całkowicie unikalnego własnego ID (zamiast tego przydzielanego dynamicznie przez FormAPI) - pozbędziemy się zachowania zdolności formularza załadowanego poprzez AHAH, aby on sam mógł zostać wysłany przez AHAH. Jak usuniemy "unikalny" id - formularz załadowany poprzez AHAH zostanie najprościej pominięty przy bindowaniu eventa 'click', ponieważ domyślne ID tego elementu po wygenerowaniu go przez AHAH będzie pokrywać się z ID pierwszego formularza dostępnego ogólnie na stronie ('edit-submit-1') - czym może być cokolwiek. Szukajka, logowanie, rejestracja itp. Tym samym zbindujemy ten pierwszy niechciany formularz. A przecież nie o to chodzi :)

 

Ad. 2 W elemencie HTMLa o takim ID wyląduje output wygenerowany przez AHAH - np. komunikaty i javascript nadany poprzez AHAH z wysłanym w formacie JSON formularzem kolejnego kroku, gotowym do wstawienia w odpowiedni blok. 

 

Dalej: oczywiście tworzymy też funkcję będącą callbackiem dla strony z formularzami :) Oczywiście zabezpieczamy się przed wyłączonym javascriptem, wyświetlając formularze w odpowiedniej kolejności, jeśli poprzedni krok został zakończony.

 

function ahah_test_form_page() {
    $output = '<div id="onepage-wrapper">';
    $output .= '<div id="ahah-output"></div>'; // tutaj zostanie wstawiony wynik zapytania ajax
    $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>'; // a tutaj na końcu wstawimy wynik działania drugiego formularza
    $output .= '</div>';
    return $output;
}

Ok, mamy menu, mamy formularze, mamy submity, mamy stronę z widokiem formularzy - pora na mózg całej operacji - callback dla wywołań AHAHowych i zdalny parser formularzy:

 

function onepage_checkout_forms_js($op = 'list', $arg = 0) {
    switch ($op) {
        case 'first_step_submit':
            $form = ahah_test_form_handler($arg); // tutaj zrealizuje się zdalny submit formularzy
            $output = theme('status_messages'); // przechwytujemy komunikaty błędów, jeśli istnieją
            $second_step_form = drupal_get_form('ahah_test_step_second_form');
            if (empty($output)) { // jeśli nie ma błędów
                $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); // tutaj zrealizuje się zdalny submit formularzy
            $output = theme('status_messages'); // przechwytujemy komunikaty błędów, jeśli istnieją
            $forms_result = ahah_test_forms_result(); // wysyłamy wynik obu formularzy
            if (empty($output)) { // jeśli nie ma błędów
                $output .= '<script type="text/javascript">
                    $(document).ready(function() {
                        $("#ahah-forms-result").empty().html(' . drupal_to_js($forms_result) . ');
                    });
                </script>';
            }
            break;
    }
    // i tutaj zaczyna się druga ważna część - bindowanie elementów nowych formularzy
    // pierwszym krokiem jest wysłanie do przeglądarki nowych danych Drupal.settings, które
    // po wysłaniu formularza przez AHAH, zawierają zaktualizowane ustawienia formularzy
    // które AHAH mają bindowany na ich elementach (kurde, trudne normalnym językiem do wyjaśnienia:D)
    // ważne że działa;)
    // Tak tak, to jest ten kawałek kodu, który nie wiadomo co robi, ale musi być!;)
    // A tak naprawdę, to ten kawałek kodu "binduje" wszystkie eventy typu AHAH (AJAX) na nowo-dodanych elementach formularzy (coś jak event .live() w jQuery 1.4+)
    $javascript = drupal_add_js(NULL, NULL, 'header');
    $settings = call_user_func_array('array_merge_recursive', $javascript['setting']);
    // a tutaj robimy to, co Wim Leers oferuje w swoim module, jednak w nieco inny sposób
    // ponownie bindujemy pola formularzy, które dostaliśmy przez AHAH
    $output .= '<script type="text/javascript">
        $(document).ready(function() {
            // jakby niektórzy dobrze poszukali, to zorientują się, że poniższy kawałek kodu
            // pochodzi z misc/ahah.js :)
            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>';

    // no i w końcu wysyłamy do przeglądarki ajaksowe dane w formacie JSON oraz nowe settingsy dla
    // javascriptowego obiektu Drupal :)
    drupal_json(array(
        'status' => TRUE,
        'data' => $output,
        'settings' => array('ahah' => $settings['ahah']),
    ));
}

Mamy AHAHowe funkcyjki, pora na krótki snippet, który wyświetli wynik zapisany w obu formularzach:

 

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

Przydałoby się jeszcze zdefiniować funkcję pełniącą ajaksowy handler dla wysyłanych formularzy (tak, to jest drugi kawałek kodu, który nie wiadomo co robi, ale musi być;). A tak naprawdę, to dzięki niemu formularz jest wysyłany poprzez AHAH, a następnie przebudowie ulega jego cache i elementy:

 

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;
}

 

No to co Panie i Panowie - ogień ! :)

 

PS. Nie obiecuję, że będzie łatwo;) A jak w artykule są jakieś bugi, lub komuś totalnie to nie bangla, pisać :)



DEMO, o wiele bardziej złożone i ciekawsze: dostępne w mojej developerskiej wersji sklepu opartego o Drupala, pod adresem www.dis-cart.designend.net, wystarczy dodać cuś do koszyka, przejść do strony realizacji zamówienia, wybrać opcję zamówienia jako gość (a po co macie się rejestrować :) i wypełniać kolejne kroki zamówienia ładowane przez AHAH :) Jak przy złożeniu zamówienia (po kliknięciu przycisku potwierdzającego zamówienie) sklep się wyłoży - trudno;P System realizacji zamówień w skrypcie przechodzi aktualnie poważne zmiany genetyczne :P Żeby nie było, że nie ostrzegałem ;)

 

A dla leniwych, GOTOWY MODUŁ TESTOWY, do pobrania, zainstalowania i przerobienia: DOWNLOAD :) 

Komentarze

dit :Je crois que je suis e0 peu pre8s dans la meame phase que toi tirer un trait sur l ado que j e9tais et que j ai l impression eonrce d eatre parfois n est pas e9vident mais je m accroche.Re9gime, reprise du sport, on verra bien si avec tout e7a j y arrive.Bon courage e0 toi
Zrobić multistep jest prosto;)
Zrobić multistep z alterami z modułów rozserzających jest trudniej.
Spiąć to wszystko przez AHAH, to rzeźnia.
Ale sprawić, żeby to działało w trybie AHAH-ready po załadowaniu kroków przez AHAH - to jest właśnie powyższy hardcore:D
Jestem pod wrażeniem. To się nazywa oddać "trochę siebie" społeczności. Ciekawe podejście. Ja swego czasu wzorowałem się na przykładach z http://d6.drupalexamples.info/examples/form_example/tutorial/10 . W ogóle ciekawy projekt, który nieco wprowadza w meandry drupalowego świata. Pozdrawiam.
Comments closed...