This is part 3 of the One Page checkout discussion. Previous parts are: Magento One Page Checkout Part 1 and OnePage Checkout Part 2: Model, Views, Controller.
One Page Checkout JavaScript Layer
The distinct feature of One Page checkout is that the checkout process can be completed without the browser having to request and load every step in a new page. Instead, One Page checkout uses JavaScript to control the navigation between steps, the steps visibility and availability. This JavaScript layer is also responsible for submitting step data to the checkout controller and interpreting controller responses to update the content of the checkout steps. You can find the JavaScript code used by One Page checkout in the following files:
- /js/varien/accordion.js – contains a JavaScript class that combines checkout steps into an accordion-like object.
- /skin/frontend/base/default/js/opcheckout.js – contains JavaScript classes representing individual checkout steps and the checkout as a whole. Note that this file is located in the skin folder – it is possible to have different One Page JavaScript for different skins.
Accordion
The task of class Accordion is to provide a framework to control checkout step visibility and navigation. It uses a container element with a number of sub-elements that serve as sections. Accordion uses CSS class ”.section” to identify these sub-elements. Accordion makes sure that at any time no more than one section is displayed (and it is possible to have all section closed). For this task it implements functions such as openSection(), openNextSection(), and openPrevSection() that close other sections before opening the requested one.
An instance of the Accordion class is created in the main One Page checkout template (Listing 8) where it receives three parameters: “checkoutSteps”, ”.step-title”, and true. The first parameter is the ID of the step container element that will be controlled by the accordion. The second parameter is the class name that is used to identify clickable section headings. The last parameter is a flag that forces the accordion to check if the clicked section element is allowed to be displayed.
Checkout
Next in Listing 8, the system initializes a checkout object that is the centerpiece of the One Page checkout JavaScript layer. Its main task is to control the checkout process flow by displaying, hiding, and updating checkout steps in response to customer actions and in a correct sequence. It uses an accordion instance to interact with the checkout step DOM elements. For example, checkout registers a click event listener to every accordion section title element. This listener reads the name of the clicked section from the element ID and opens it by calling the checkout object’s gotoSection method:
gotoSection: function(section) { var sectionElement = $('opc-'+section); sectionElement.addClassName('allow'); this.accordion.openSection('opc-'+section); this.reloadProgressBlock(section); }
Listing 15. Opening a checkout step, /skin/frontend/base/default/js/opcheckout.js, line 111.
The method in Listing 15 not only opens a step, but requests an update for the progress block (this.reloadProgressBlock(section)) that is performed by an Ajax request.
When the checkout controller sends a JSON-encoded response, it becomes the task of the checkout object to interpret the decoded JSON object:
setStepResponse: function(response){ if (response.update_section) { $('checkout-'+response.update_section.name+'-load').update(response.update_section.html); } if (response.allow_sections) { response.allow_sections.each(function(e){ $('opc-'+e).addClassName('allow'); }); } if(response.duplicateBillingInfo) { shipping.setSameAsBilling(true); } if (response.goto_section) { this.gotoSection(response.goto_section); return true; } if (response.redirect) { location.href = response.redirect; return true; } return false; }
Listing 16. Processing the controller response, /skin/frontend/base/default/js/opcheckout.js, line 203.
In method setStepResponse, the system checks if the response object contains an update_section element and, if yes, updates the specified section with the new HTML content. Next, it sets allow classes to those section elements that the customer currently is allowed to switch to. After that, the system instructs the shipping object (representing the “Shipping Information” step) that billing address is used for shipping – if the response contains a duplicateBillingInfo element. Finally, the system either opens a new step (defined in the goto_section element) or redirects the customer to the URL defined in the redirect element.
Other notable methods implemented in the Checkout class are:
- setLoadWaiting – that is triggered when data from one of the checkout steps is submitted, i.e. when a step object (billing, shipping, shippingMethod, or payment) invokes its save method before posting an Ajax request. This method displays an element with a animated “loader” image whose ID is composed of the step name and ‘-please-wait’, e.g. ‘billing-please-wait’. The step buttons container changes its opacity and all its elements (i.e. the “Continue” and “Go Back” buttons) are disabled so that the customer couldn’t mistakenly submit the data again.
- resetPreviousSteps – that is invoked when the customer has switched to an earlier step in the checkout and it is necessary to reset the display of data submitted in sections that go after the current step. For example, if a customer was in the “review” step and switched to “billing” then the checkout data shown in the progress bar under steps “payment method”, “shipping method”, and “shipping” is invalidated and must be hidden.
- ajaxFailure – that is used as a callback for a failed outcome of an XHR call. It redirects the browser to the failure URL that has been specified during the checkout object’s initialization (by default: the URL of the shopping cart overview).
- setMethod – that is called from the “login” step in the checkout. The customer can choose whether to log into an existing account, or to checkout a guest, or to register a new account after the checkout. In the latter two cases the customer must click button “Continue” whose “onclick” event is processed by the setMethod function. This function checks what option the customer has selected (“guest” or “new account”) and sends an Ajax request to controller action Mage_Checkout_OnepageController::saveMethodAction that sets the checkout method to either guest or register. If customer didn’t select a checkout option and still clicked this button, the system displays an error message. Finally, the method fires a “login:setMethod” event that is captured by an event observer defined in file /js/mage/captcha.js. The checkout layout can contain two captcha elements: one to be used when a customer proceeds as guest and another when a customer wants to register an account. If these elements exist, they will be shown or hidden depending on the checkout method chosen by the customer.
Other classes defined in the opcheckout.js file provide functionality for individual checkout steps and their primary task is validating and submitting step form data to the controller.
Billing and Shipping
Classes Billing and Shipping do this for the steps “Billing Information” and “Shipping Information” respectively. Since they both deal with the same data type – address – their structure and methods are quite similar. The initialization of these classes’ instances happens in step templates: billing.phtml and shipping.phtml (note that the billing template is provided by module Mage_Persistent and not Mage_Checkout whose One Page billing,phtml file is not used).
When initialized, Billing and Checkout classes register submit event listeners to their forms. When a customer submits the address form, the system invokes the save method of the respective class that validates the form input (data type and requirement constraints) and sends an Ajax request to the checkout controller. This, for example, is the save method of class Billing:
save: function(){ if (checkout.loadWaiting!=false) return; var validator = new Validation(this.form); if (validator.validate()) { checkout.setLoadWaiting('billing'); var request = new Ajax.Request( this.saveUrl, { method: 'post', onComplete: this.onComplete, onSuccess: this.onSave, onFailure: checkout.ajaxFailure.bind(checkout), parameters: Form.serialize(this.form) } ); } }
Listing 17. Method to validate and submit data of the “Billing Information” step, /skin/frontend/base/default/js/opcheckout.js, line 302.
The method first checks if the loading animation is on. If yes – which means the previous request is not done yet – the method returns nothing. If the step data submission can proceed, the method uses a Validator class instance to validate the form inputs. If the validation succeeds, the script activates the loading animation and issues an Ajax request to the URL specified by the saveUrl property. This URL points to a checkout controller action responsible for the “Billing Information” step data processing. The Ajax code call defines handlers for three events: onComplete, onSuccess, and onFailure. The handler for the first event is the resetLoadWaiting method that disables the loading animation and fires an event indicating that billing step request is completed. The onSuccess event is processed by method nextStep that converts the JSON string response from the controller into an object, checks if the response contains an error (and displaying the message, if yes), and transfers the response data to the checkout object’s method setStepResponse for further processing (which has been discussed above).
nextStep: function(transport){ if (transport && transport.responseText){ try{ response = eval('(' + transport.responseText + ')'); } catch (e) { response = {}; } } if (response.error){ if ((typeof response.message) == 'string') { alert(response.message); } else { if (window.billingRegionUpdater) { billingRegionUpdater.update(); } alert(response.message.join("\n")); } return false; } checkout.setStepResponse(response); payment.initWhatIsCvvListeners(); }
Listing 18. Method nextStep of class Billing, /master/skin/frontend/base/default/js/opcheckout.js, line 335.
Finally, the onFailure event is handled by the checkout object’s method ajaxFailure that redirects the customer to the shopping cart.
The Shipping class differs from Billing in one major point. It is possible to set billing address to be used for shipping and this eventuality is handled by method syncWithBilling. In such a case, the controller responds to the “save billing information” request with an object containing element “duplicateBillingInfo” that invokes the Shipping class function setSameAsBilling() which in turn calls the syncWithBilling method:
syncWithBilling: function () { $('billing-address-select') && this.newAddress(!$('billing-address-select').value); $('shipping:same_as_billing').checked = true; if (!$('billing-address-select') || !$('billing-address-select').value) { arrElements = Form.getElements(this.form); for (var elemIndex in arrElements) { if (arrElements[elemIndex].id) { var sourceField = $(arrElements[elemIndex].id.replace(/^shipping:/, 'billing:')); if (sourceField){ arrElements[elemIndex].value = sourceField.value; } } } //$('shipping:country_id').value = $('billing:country_id').value; shippingRegionUpdater.update(); $('shipping:region_id').value = $('billing:region_id').value; $('shipping:region').value = $('billing:region').value; //shippingForm.elementChildLoad($('shipping:country_id'), this.setRegionValue.bind(this)); } else { $('shipping-address-select').value = $('billing-address-select').value; } },
Listing 19. Processing a “use billing for shipping” case, /skin/frontend/base/default/js/opcheckout.js, line 447.
The first line checks if the customer has selected an existing address or entered a new one into the billing address form. In the former case the shipping address form gets shown, in the latter – gets hidden. If the customer entered a new address, the script copies input field values of the billing form into the corresponding fields of the shipping address form. If the customer uses an existing billing address, the script sets its ID to the shipping address select element.
ShippingMethod
Class ShippingMethod, as the name hints, is responsible for the “Shipping Method” step. This one is quite simple: its method save listens to the form submit event and issues an Ajax call to the controller, its nextStep method receives the response and switches to the next step. The only specific it has, is its validate method that checks if there are any shipping methods found for the current shipping address. If none is available or if the customer has selected none, the method displays an error message and blocks the further checkout progress.
Payment
Class Payment has a more complex structure. It defines several hashes (entities similar to PHP associative arrays) to store functions that are called before and after the step initialization or the form validation:
Payment.prototype = { beforeInitFunc:$H({}), afterInitFunc:$H({}), beforeValidateFunc:$H({}), afterValidateFunc:$H({}), /** * code omitted for brevity */ }
Listing 20. Declaring hashes for auxiliary functions, /skin/frontend/base/default/js/opcheckout.js, line 625.
Some payment methods may require additional processing during the step initialization phase or have additional validation rules. This feature enables such payment methods to add functions that the instance of the Payment will call when needed:
beforeInit : function() { (this.beforeInitFunc).each(function(init){ (init.value)();; }); },
Listing 21. Calling auxiliary functions before the initialization of the “Payment” step , /skin/frontend/base/default/js/opcheckout.js, line 641.
In the listing above the beforeInit method iteratively calls functions set in the this.beforeInitFunc hash. Methods afterInit, beforevalidate and afterValidate work in a similar fashion. The payment methods defined in the core Magento do not use these hashes, but if necessary, a payment method can define a set of auxiliary functions by calling methods addBeforeValidateFunction, addAfterInitFunction, addBeforeValidateFunction, addAfterValidateFunction of the payment instance.
Whenever the “Payment” step is rendered (which happens at least twice: when opening the checkout page and when loading the step later in the process), the init method is invoked. This method calls the beforeInit method first, and then registers the save method as a handler to the payment form submit event. It also iterates through the payment step form elements and disables all whose names are not “payment[method]” – that is every element except radio buttons representing the payment options. If one of the methods is already checked, init calls the switchMethod function to display the method’s child form.
Some payment methods may require additional input fields, such as the “Credit card (saved)” that has its own child form:
The task of managing the visibility of these child forms is performed by the switchMethod and changeVisible function. These methods identify a child form by its container element ID “payment_form_%method name%” (e.g, “payment_form_ccsave”) and display it when a customer select the respective payment method; in the same time the previously shown form changes is display mode to none.
The save method of the payment class handles the step data submission event and ensures that the payment data are validated before posting them. For this task the checkout script uses the Validation class of the Prototype framework as well as the Payment class’ own method validate <https://github.com/varinen/solvingmagento_1.7.0/blob/master/skin/frontend/base/default/js/opcheckout.js#L728>. The latter checks if there are payment methods available to the current order, and if the customer has checked one. Also, the validate method invokes the “before-” and “after validate” functions. Note how the “before-” and “after validation” methods influence the result:
validate: function() { var result = this.beforeValidate(); if (result) { return true; } var methods = document.getElementsByName('payment[method]'); if (methods.length==0) { alert(Translator.translate('Your order cannot be completed at this time as there is no payment methods available for it.').stripTags()); return false; } for (var i=0; i<methods.length; i++) { if (methods[i].checked) { return true; } } result = this.afterValidate(); if (result) { return true; } alert(Translator.translate('Please specify payment method.').stripTags()); return false; }
Listing 22. Payment step validation method, /skin/frontend/base/default/js/opcheckout.js, line 728.
The functions set to be executed “before validation” can make the script to skip the rest of the validation if they return true. The “after validation” functions can make the script to ignore the fact that the customer has not select any of the available payment methods.
Review
“Order Review” is the last checkout step that has a JavaScript class assigned to it: Review. Its save method functions similarly to others previously described, with one small difference: it also posts data from the “checkout agreements” form. This is a separate form that injects itself as a child block into the last step and consists of a list of conditions the customer must agree to before submitting the order. These agreement can be defined in the shop back-end under Sales > Terms and Conditions.
Method nextStep also has a bit different routine to process the controller response. The response object can have a redirect or a success element. In the former case the method executes a redirect to the provided URL (e.g., to a payment provider such as PayPal), in the latter the script shows the checkout success page.
Conclusion
Despite the length of this chapter, it is still rather an overview than a complete descpription of, in my opinion, the most important Magento feature. But this deficiency is owed much to the complexity of the topic: trying to describe its every aspect in one chapter could easily leave the reader lost in endless details. Still, I think, two subjects can complement this discussion. One is checkout customization which I will address in the next section on building a One Step checkout. Another is an assessment of the checkout behaviour under heavy load: what are the limiting factors to number of order that can be created per minute or per second and what can be done to ensure a stable checkout performance under extreme conditions. In this area, my expertise is unfortunately limited and I have to postpone its discussion for now.
Another great article! Keep up the good work.
Dear Oleg,
I will wait impatiently next chapter of this series, “Checkout Customization”. Thank you so much for detailed explanation.
Thanks Oğuz,
I’m currently writing the next part – and it will be a huge one 🙂
hi, i need some help, im just trying to use radio buttons instead checkbox “billing[use_for_shipping]”, i’ve two options: Ship to this address, ship to diferente address.
<input type="radio"
name="billing[use_for_shipping]"
id="billing:use_for_shipping_yes"
value="1"isUseBillingAddressForShipping()) {?>
checked=”checked”
title=”__(‘Ship to this address’) ?>”
onclick=”$(‘shipping:same_as_billing’).checked = true;” class=”radio” />
__(‘Ship to this address’) ?>
<input type="radio"
name="billing[use_for_shipping]"
id="billing:use_for_shipping_no"
value="0"isUseBillingAddressForShipping()) {?>
checked=”checked”
title=”__(‘Ship to different address’) ?>”
onclick=”$(‘shipping:same_as_billing’).checked = false;” class=”radio” />
__(‘Ship to different address’) ?>
the problem is: the second option is always selected by default, if I do click in the first radio button it not works, the second option still selected.
Dear Oleg, The series of articles are extremely helpful in understanding Magento checkout process. I followed all the steps you have mentioned. So far with exception of this last one and it is because my limited understanding of AJAX. I will buy your book to keep as reference. Thank you for sharing your Magento expertise!
Thank you, Ray.
I’m happy you found my articles useful!