In this post we will check how to move billing address right under the shipping address and assign billing address from shipping step in Magento2.
You can find complete module on Github at Magelearn_ImprovedCheckout
Create folder inside app/code/Magelearn/ImprovedCheckout
Add registration.php file in it:
1 2 3 4 5 6 7 | <?php \Magento\Framework\Component\ComponentRegistrar::register( \Magento\Framework\Component\ComponentRegistrar::MODULE, 'Magelearn_ImprovedCheckout' , __DIR__ ); |
Add composer.json file in it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | { "name" : "magelearn/improved-checkout" , "description" : "Move billing address just below shipping address and assign billing address from shipping step in Magento2." , "type" : "magento2-module" , "license" : "proprietary" , "authors" : [ { "name" : "vijay rami" , "email" : "vijaymrami@gmail.com" } ], "minimum-stability" : "dev" , "require" : {}, "autoload" : { "files" : [ "registration.php" ], "psr-4" : { "Magelearn\\ImprovedCheckout\\" : "" } } } |
Add etc/module.xml file in it:
1 2 3 4 5 6 7 8 9 | <? xml version = "1.0" ?> xsi:noNamespaceSchemaLocation = "urn:magento:framework:Module/etc/module.xsd" > < module name = "Magelearn_ImprovedCheckout" setup_version = "1.0.0" > < sequence > < module name = "Magento_Checkout" /> </ sequence > </ module > </ config > |
1 2 3 4 5 6 7 8 9 | <? xml version = "1.0" ?> xsi:noNamespaceSchemaLocation = "urn:magento:framework:ObjectManager/etc/config.xsd" > < type name = "Magento\Checkout\Block\Checkout\LayoutProcessor" > < plugin name = "magelearn_improved_checkout_layout_processor" type = "Magelearn\ImprovedCheckout\Plugin\Block\LayoutProcessor" sortOrder = "1" /> </ type > </ config > |
Now add Plugin/Block/LayoutProcessor.php file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 | <?php namespace Magelearn\ImprovedCheckout\Plugin\Block; use Magento\Customer\Model\AttributeMetadataDataProvider; use Magento\Ui\Component\Form\AttributeMapper; use Magento\Checkout\Block\Checkout\AttributeMerger; use Magento\Checkout\Model\Session as CheckoutSession; use Magento\Customer\Model\Options; class LayoutProcessor { /** * * @var AttributeMetadataDataProvider */ public $attributeMetadataDataProvider ; /** * * @var AttributeMapper */ public $attributeMapper ; /** * * @var AttributeMerger */ public $merger ; /** * * @var CheckoutSession */ public $checkoutSession ; /** * * @var null */ public $quote = null; /** * @var Options */ public $options ; /** * LayoutProcessor constructor. * * @param AttributeMetadataDataProvider $attributeMetadataDataProvider * @param AttributeMapper $attributeMapper * @param AttributeMerger $merger * @param CheckoutSession $checkoutSession * @param Options $options */ public function __construct( AttributeMetadataDataProvider $attributeMetadataDataProvider , AttributeMapper $attributeMapper , AttributeMerger $merger , CheckoutSession $checkoutSession , Options $options = null ) { $this ->attributeMetadataDataProvider = $attributeMetadataDataProvider ; $this ->attributeMapper = $attributeMapper ; $this ->merger = $merger ; $this ->checkoutSession = $checkoutSession ; $this ->options = $options ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Customer\Model\Options:: class ); } /** * Get Quote * * @return \Magento\Quote\Model\Quote|null */ public function getQuote() { if (null === $this ->quote) { $this ->quote = $this ->checkoutSession->getQuote(); } return $this ->quote; } /** * * @param \Magento\Checkout\Block\Checkout\LayoutProcessor $subject * @param array $jsLayout * @return array */ public function aroundProcess( \Magento\Checkout\Block\Checkout\LayoutProcessor $subject , \Closure $proceed , array $jsLayout ) { $jsLayoutResult = $proceed ( $jsLayout ); if ( $this ->getQuote()->isVirtual()) { return $jsLayoutResult ; } $attributesToConvert = [ 'prefix' => [ $this ->options, 'getNamePrefixOptions' ], 'suffix' => [ $this ->options, 'getNameSuffixOptions' ], ]; if (isset( $jsLayoutResult [ 'components' ][ 'checkout' ][ 'children' ][ 'steps' ][ 'children' ][ 'shipping-step' ][ 'children' ] [ 'shippingAddress' ][ 'children' ][ 'shipping-address-fieldset' ])) { $jsLayoutResult [ 'components' ][ 'checkout' ][ 'children' ][ 'steps' ][ 'children' ][ 'shipping-step' ][ 'children' ] [ 'shippingAddress' ][ 'children' ][ 'shipping-address-fieldset' ][ 'children' ] [ 'street' ][ 'children' ][0][ 'placeholder' ] = __( 'Street Address' ); $jsLayoutResult [ 'components' ][ 'checkout' ][ 'children' ][ 'steps' ][ 'children' ][ 'shipping-step' ][ 'children' ] [ 'shippingAddress' ][ 'children' ][ 'shipping-address-fieldset' ][ 'children' ] [ 'street' ][ 'children' ][1][ 'placeholder' ] = __( 'Street line 2' ); $jsLayoutResult [ 'components' ][ 'checkout' ][ 'children' ][ 'steps' ][ 'children' ][ 'shipping-step' ][ 'children' ] [ 'shippingAddress' ][ 'children' ][ 'billing-address' ][ 'children' ][ 'form-fields' ][ 'children' ] [ 'street' ][ 'children' ][0][ 'placeholder' ] = __( 'Street Address' ); $jsLayoutResult [ 'components' ][ 'checkout' ][ 'children' ][ 'steps' ][ 'children' ][ 'shipping-step' ][ 'children' ] [ 'shippingAddress' ][ 'children' ][ 'billing-address' ][ 'children' ][ 'form-fields' ][ 'children' ] [ 'street' ][ 'children' ][1][ 'placeholder' ] = __( 'Street line 2' ); } $elements = $this ->getAddressAttributes(); $elements = $this ->convertElementsToSelect( $elements , $attributesToConvert ); $jsLayoutResult [ 'components' ][ 'checkout' ][ 'children' ][ 'steps' ][ 'children' ][ 'shipping-step' ][ 'children' ][ 'shippingAddress' ] [ 'children' ][ 'billingAddress' ][ 'children' ][ 'address-fieldset' ] = $this ->getCustomBillingAddressComponent( $elements ); if (isset( $jsLayoutResult [ 'components' ][ 'checkout' ][ 'children' ][ 'steps' ][ 'children' ][ 'billing-step' ][ 'children' ] [ 'payment' ][ 'children' ][ 'afterMethods' ][ 'children' ][ 'billing-address-form' ])) { unset( $jsLayoutResult [ 'components' ][ 'checkout' ][ 'children' ][ 'steps' ][ 'children' ][ 'billing-step' ][ 'children' ] [ 'payment' ][ 'children' ][ 'afterMethods' ][ 'children' ][ 'billing-address-form' ]); } if ( $billingAddressForms = $jsLayoutResult [ 'components' ][ 'checkout' ][ 'children' ][ 'steps' ][ 'children' ] [ 'billing-step' ][ 'children' ][ 'payment' ][ 'children' ][ 'payments-list' ][ 'children' ]) { foreach ( $billingAddressForms as $billingAddressFormsKey => $billingAddressForm ) { if ( $billingAddressFormsKey != 'before-place-order' ) { unset( $jsLayoutResult [ 'components' ][ 'checkout' ][ 'children' ][ 'steps' ][ 'children' ][ 'billing-step' ][ 'children' ] [ 'payment' ][ 'children' ][ 'payments-list' ][ 'children' ][ $billingAddressFormsKey ]); } } } return $jsLayoutResult ; } /** * Get all visible address attribute * * @return array */ private function getAddressAttributes() { /** @var \Magento\Eav\Api\Data\AttributeInterface[] $attributes */ $attributes = $this ->attributeMetadataDataProvider->loadAttributesCollection( 'customer_address' , 'customer_register_address' ); $elements = []; foreach ( $attributes as $attribute ) { $code = $attribute ->getAttributeCode(); if ( $attribute ->getIsUserDefined()) { continue ; } $elements [ $code ] = $this ->attributeMapper->map( $attribute ); if (isset( $elements [ $code ][ 'label' ])) { $label = $elements [ $code ][ 'label' ]; $elements [ $code ][ 'label' ] = __( $label ); } } return $elements ; } /** * Prepare billing address field for shipping step for physical product * * @param * $elements * @return array */ public function getCustomBillingAddressComponent( $elements ) { $providerName = 'checkoutProvider' ; $components = [ 'component' => 'uiComponent' , 'displayArea' => 'additional-fieldsets' , 'children' => $this ->merger->merge( $elements , $providerName , 'billingAddress' , [ 'country_id' => [ 'sortOrder' => 115 ], 'region' => [ 'visible' => false ], 'region_id' => [ 'component' => 'Magento_Ui/js/form/element/region' , 'config' => [ 'template' => 'ui/form/field' , 'elementTmpl' => 'ui/form/element/select' , 'customEntry' => 'billingAddress.region' ], 'validation' => [ 'required-entry' => true ], 'filterBy' => [ 'target' => '${ $.provider }:${ $.parentScope }.country_id' , 'field' => 'country_id' ] ], 'postcode' => [ 'component' => 'Magento_Ui/js/form/element/post-code' , 'validation' => [ 'required-entry' => true ] ], 'company' => [ 'validation' => [ 'min_text_length' => 0 ] ], 'fax' => [ 'validation' => [ 'min_text_length' => 0 ] ], 'telephone' => [ 'config' => [ 'tooltip' => [ 'description' => __( 'For delivery questions.' ) ] ] ] ]) ]; return $components ; } private function convertElementsToSelect( $elements , $attributesToConvert ) { $codes = array_keys ( $attributesToConvert ); foreach ( array_keys ( $elements ) as $code ) { if (!in_array( $code , $codes )) { continue ; } // phpcs:ignore Magento2.Functions.DiscouragedFunction $options = call_user_func( $attributesToConvert [ $code ]); if (! is_array ( $options )) { continue ; } $elements [ $code ][ 'dataType' ] = 'checkbox-set' ; $elements [ $code ][ 'formElement' ] = 'checkbox-set' ; $elements [ $code ][ 'value' ] = '0' ; foreach ( $options as $key => $value ) { $elements [ $code ][ 'options' ][] = [ 'value' => $key , 'label' => $value ]; } } return $elements ; } } |
Now we will modify checkout_index_index.xml file and modify it by adding some child node just below shipping address to display billing address below it.
Add file at view/frontend/layout/checkout_index_index.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | <? xml version = "1.0" ?> layout = "1column" xsi:noNamespaceSchemaLocation = "urn:magento:framework:View/Layout/etc/page_configuration.xsd" > < update handle = "styles" /> < head > < css src = "Magelearn_ImprovedCheckout::css/improvedcheckout.css" /> </ head > < body > < referenceBlock name = "checkout.root" > < arguments > < argument name = "jsLayout" xsi:type = "array" > < item name = "components" xsi:type = "array" > < item name = "checkout" xsi:type = "array" > < item name = "children" xsi:type = "array" > < item name = "steps" xsi:type = "array" > < item name = "children" xsi:type = "array" > < item name = "shipping-step" xsi:type = "array" > < item name = "children" xsi:type = "array" > < item name = "shippingAddress" xsi:type = "array" > < item name = "children" xsi:type = "array" > < item name = "billingAddress" xsi:type = "array" > < item name = "config" xsi:type = "array" > < item name = "deps" xsi:type = "array" > < item name = "0" xsi:type = "string" >checkout.steps.shipping-step.step-config</ item > < item name = "1" xsi:type = "string" >checkoutProvider</ item > </ item > < item name = "popUpForm" xsi:type = "array" > < item name = "element" xsi:type = "string" >#opc-new-billing-address</ item > < item name = "options" xsi:type = "array" > < item name = "type" xsi:type = "string" >popup</ item > < item name = "responsive" xsi:type = "boolean" >true</ item > < item name = "innerScroll" xsi:type = "boolean" >true</ item > < item name = "title" xsi:type = "string" translate = "true" >Billing Address</ item > < item name = "trigger" xsi:type = "string" >opc-new-billing-address</ item > < item name = "buttons" xsi:type = "array" > < item name = "save" xsi:type = "array" > < item name = "text" xsi:type = "string" translate = "true" >Save Address</ item > < item name = "class" xsi:type = "string" >action primary action-save-address</ item > </ item > < item name = "cancel" xsi:type = "array" > < item name = "text" xsi:type = "string" translate = "true" >Cancel</ item > < item name = "class" xsi:type = "string" >action secondary action-hide-popup</ item > </ item > </ item > </ item > </ item > </ item > < item name = "component" xsi:type = "string" >Magelearn_ImprovedCheckout/js/view/billing</ item > < item name = "displayArea" xsi:type = "string" >billing-address</ item > < item name = "provider" xsi:type = "string" >checkoutProvider</ item > < item name = "sortOrder" xsi:type = "string" >1</ item > < item name = "children" xsi:type = "array" > < item name = "before-form" xsi:type = "array" > < item name = "component" xsi:type = "string" >uiComponent</ item > < item name = "displayArea" xsi:type = "string" >billing-before-form</ item > < item name = "children" xsi:type = "array" > <!-- before form fields --> </ item > </ item > < item name = "before-fields" xsi:type = "array" > < item name = "component" xsi:type = "string" >uiComponent</ item > < item name = "displayArea" xsi:type = "string" >billing-before-fields</ item > < item name = "children" xsi:type = "array" > <!-- before fields --> </ item > </ item > < item name = "address-list" xsi:type = "array" > < item name = "component" xsi:type = "string" >Magelearn_ImprovedCheckout/js/view/billing-address/list</ item > < item name = "config" xsi:type = "array" > < item name = "template" xsi:type = "string" >Magelearn_ImprovedCheckout/billing-address/custom-list</ item > </ item > < item name = "displayArea" xsi:type = "string" >billing-address-list</ item > </ item > </ item > </ item > </ item > </ item > </ item > </ item > </ item > </ item > </ item > </ item > </ item > </ argument > </ arguments > </ referenceBlock > </ body > </ page > |
Now as per highlighted code above first we will create our JS component file at view/frontend/web/js/view/billing.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 | define([ 'jquery' , 'underscore' , 'Magento_Ui/js/form/form' , 'ko' , 'Magento_Customer/js/model/customer' , 'Magento_Customer/js/model/address-list' , 'Magento_Checkout/js/model/address-converter' , 'Magento_Checkout/js/model/quote' , 'Magelearn_ImprovedCheckout/js/model/billing-address/form-popup-state' , 'Magelearn_ImprovedCheckout/js/action/edit-billing-address' , 'Magento_Checkout/js/action/create-billing-address' , 'Magento_Checkout/js/action/select-billing-address' , 'Magento_Checkout/js/checkout-data' , 'Magento_Checkout/js/model/checkout-data-resolver' , 'Magento_Customer/js/customer-data' , 'Magento_Checkout/js/action/set-billing-address' , 'Magento_Ui/js/modal/modal' , 'Magento_Ui/js/model/messageList' , 'mage/translate' , 'mage/url' ], function ( $, _, Component, ko, customer, addressList, addressConverter, quote, formPopUpState, editBillingAddress, createBillingAddress, selectBillingAddress, checkoutData, checkoutDataResolver, customerData, setBillingAddressAction, modal, globalMessageList, $t, url ) { 'use strict' ; var popUp = null , addressOptions = addressList().filter( function (address) { return address.getType() === 'customer-address' ; }); return Component.extend({ defaults: { template: 'Magelearn_ImprovedCheckout/billing' , billingFormTemplate: 'Magelearn_ImprovedCheckout/billing-address/form' }, visible: ko.observable(!quote.isVirtual()), errorValidationMessage: ko.observable( false ), isCustomerLoggedIn: customer.isLoggedIn, isFormPopUpVisible: formPopUpState.isVisible, isFormInline: addressList().length === 0, isNewAddressAdded: ko.observable( false ), saveInAddressBook: 1, quoteIsVirtual: quote.isVirtual(), customerHasAddresses: addressOptions.length > 0, /** * @return {exports} */ /** * Init component */ initialize: function () { this ._super(); quote.paymentMethod.subscribe( function () { checkoutDataResolver.resolveBillingAddress(); }, this ); }, /** * @return {exports.initObservable} */ initObservable: function () { var self = this , hasNewAddress; this ._super() .observe({ selectedAddress: null , isAddressFormVisible: true , isAddressSameAsShipping: false , isAddressFormListVisible: false , saveInAddressBook: 1 }); quote.billingAddress.subscribe( function (newAddress) { if (quote.isVirtual()) { this .isAddressSameAsShipping( false ); } else { this .isAddressSameAsShipping( newAddress != null && quote.shippingAddress() && newAddress.getCacheKey() === quote.shippingAddress().getCacheKey() //eslint-disable-line eqeqeq ); } if (newAddress != null && newAddress.saveInAddressBook !== undefined) { this .saveInAddressBook(newAddress.saveInAddressBook); } else { this .saveInAddressBook(1); } }, this ); checkoutDataResolver.resolveBillingAddress(); hasNewAddress = addressList.some( function (address) { return address.getType() === 'new-customer-address' || address.getType() === 'new-billing-address' ; //eslint-disable-line eqeqeq }); this .isNewAddressAdded(hasNewAddress); this .isFormPopUpVisible.subscribe( function (value) { if (value) { self.getPopUp().openModal(); } }); return this ; }, /** * Navigator change hash handler. * * @param {Object} step - navigation step */ navigate: function (step) { step && step.isVisible( true ); }, /** * @return {*} */ getPopUp: function () { var self = this , buttons; if (!popUp) { buttons = this .popUpForm.options.buttons; this .popUpForm.options.buttons = [ { text: buttons.save.text ? buttons.save.text : $t( 'Save Address' ), class: buttons.save.class ? buttons.save.class : 'action primary action-save-address' , click: self.saveNewAddress.bind(self) }, { text: buttons.cancel.text ? buttons.cancel.text : $t( 'Cancel' ), class: buttons.cancel.class ? buttons.cancel.class : 'action secondary action-hide-popup' , /** @inheritdoc */ click: this .onClosePopUp.bind( this ) } ]; /** @inheritdoc */ this .popUpForm.options.closed = function () { self.isFormPopUpVisible( false ); }; this .popUpForm.options.modalCloseBtnHandler = this .onClosePopUp.bind( this ); this .popUpForm.options.keyEventHandlers = { escapeKey: this .onClosePopUp.bind( this ) }; /** @inheritdoc */ this .popUpForm.options.opened = function () { // Store temporary address for revert action in case when user click cancel action self.temporaryAddress = $.extend( true , {}, checkoutData.getBillingAddressFromData()); }; popUp = modal( this .popUpForm.options, $( this .popUpForm.element)); } return popUp; }, /** * Revert address and close modal. */ onClosePopUp: function () { checkoutData.getBillingAddressFromData($.extend( true , {}, this .temporaryAddress)); this .getPopUp().closeModal(); }, /** * Show address form popup */ showFormPopUp: function () { this .isFormPopUpVisible( true ); }, /** * Save new billing address */ saveNewAddress: function () { var addressData, newBillingAddress; this .source.set( 'params.invalid' , false ); this .triggerBillingDataValidateEvent(); if (! this .source.get( 'params.invalid' )) { addressData = this .source.get( 'billingAddress' ); // if user clicked the checkbox, its value is true or false. Need to convert. addressData[ 'save_in_address_book' ] = this .saveInAddressBook ? 1 : 0; // New address must be selected as a billing address newBillingAddress = editBillingAddress(addressData); selectBillingAddress(newBillingAddress); checkoutData.setSelectedBillingAddress(newBillingAddress.getKey()); checkoutData.setNewCustomerBillingAddress($.extend( true , {}, addressData)); this .getPopUp().closeModal(); this .isNewAddressAdded( true ); } }, /** * Trigger Billing data Validate Event. */ triggerBillingDataValidateEvent: function () { this .source.trigger( 'billingAddress.data.validate' ); if ( this .source.get( 'billingAddress.custom_attributes' )) { this .source.trigger( 'billingAddress.custom_attributes.data.validate' ); } }, }); }); |
Now as per highlighted code above we will add our necessary JS files.
Add file at view/frontend/web/js/model/billing-address/form-popup-state.js
1 2 3 4 5 6 7 8 9 | define([ 'ko' ], function (ko) { 'use strict' ; return { isVisible: ko.observable( false ) }; }); |
Add file at view/frontend/web/js/action/edit-billing-address.js
This file is responsible to Append new address to the address list after adding a New Billing Address.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | define([ 'Magento_Customer/js/model/address-list' , 'Magento_Checkout/js/model/address-converter' ], function (addressList,addressConverter) { 'use strict' ; return function (addressData) { var address = addressConverter.formAddressDataToQuoteAddress(addressData), isAddressUpdated = addressList().some( function (currentAddress, index, addresses) { if (currentAddress.getKey() == address.getKey()) { //eslint-disable-line eqeqeq addresses[index] = address; return true ; } return false ; }); if (!isAddressUpdated) { addressList.push(address); } else { addressList.valueHasMutated(); } return address; }; }); |
Now as per highlighted code in billing.js file we will add our template file as well as billing form template file.
Add file at view/frontend/web/template/billing.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | < div class = "checkout-billing-address" > <!-- ko foreach: getRegion('billing-address-list') --> <!-- ko template: getTemplate() --> <!-- /ko --> <!--/ko--> <!-- Inline address form --> < render if = "isFormInline" args = "billingFormTemplate" ></ render > </ div > <!-- Address form pop up --> < if args = "!isFormInline" > < div class = "new-address-popup" > < button type = "button" class = "action action-show-popup" click = "showFormPopUp" visible = "!isNewAddressAdded()" > < span translate = "'New Address'" ></ span > </ button > </ div > < div id = "opc-new-billing-address" visible = "isFormPopUpVisible()" render = "billingFormTemplate" ></ div > </ if > |
Add file at view/frontend/web/template/billing-address/form.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | < div class = "billing-address-form" data-bind = "fadeVisible: isAddressFormVisible" > <!-- ko foreach: getRegion('before-fields') --> <!-- ko template: getTemplate() --> <!-- /ko --> <!--/ko--> < fieldset class = "fieldset address" data-form = "billing-new-address" > <!-- ko foreach: getRegion('additional-fieldsets') --> <!-- ko template: getTemplate() --> <!-- /ko --> <!--/ko--> <!-- ko if: (isCustomerLoggedIn && customerHasAddresses) --> < div class = "choice field" > < input type = "checkbox" class = "checkbox" data-bind = "checked: saveInAddressBook, attr: {id: 'billing-save-in-address-book'}" /> < label class = "label" data-bind = "attr: {for: 'billing-save-in-address-book'}" > < span data-bind = "i18n: 'Save in address book'" ></ span > </ label > </ div > <!-- /ko --> </ fieldset > </ div > |
Now we will add our billing address list component JS file and Billing address list items template file.
First add our billing address list JS component file at view/frontend/web/js/view/billing-address/list.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | define([ 'underscore' , 'ko' , 'mageUtils' , 'uiComponent' , 'uiLayout' , 'Magento_Customer/js/model/address-list' ], function (_, ko, utils, Component, layout, addressList) { 'use strict' ; var defaultRendererTemplate = { parent: '${ $.$data.parentName }' , name: '${ $.$data.name }' , component: 'Magelearn_ImprovedCheckout/js/view/billing-address/address-renderer/default' , provider: 'checkoutProvider' }; return Component.extend({ defaults: { template: 'Magelearn_ImprovedCheckout/billing-address/list' , visible: addressList().length > 0, rendererTemplates: [] }, /** @inheritdoc */ initialize: function () { this ._super() .initChildren(); addressList.subscribe( function (changes) { var self = this ; changes.forEach( function (change) { if (change.status === 'added' ) { self.createRendererComponent(change.value, change.index); } }); }, this , 'arrayChange' ); return this ; }, /** @inheritdoc */ initConfig: function () { this ._super(); this .rendererComponents = []; return this ; }, /** @inheritdoc */ initChildren: function () { _.each(addressList(), this .createRendererComponent, this ); return this ; }, /** * Create new component that will render given address in the address list * * @param {Object} address * @param {*} index */ createRendererComponent: function (address, index) { var rendererTemplate, templateData, rendererComponent; if (index in this .rendererComponents) { this .rendererComponents[index].address(address); } else { // rendererTemplates are provided via layout rendererTemplate = address.getType() != undefined && this .rendererTemplates[address.getType()] != undefined ? //eslint-disable-line utils.extend({}, defaultRendererTemplate, this .rendererTemplates[address.getType()]) : defaultRendererTemplate; templateData = { parentName: this .name, name: index }; rendererComponent = utils.template(rendererTemplate, templateData); utils.extend(rendererComponent, { address: ko.observable(address) }); layout([rendererComponent]); this .rendererComponents[index] = rendererComponent; } } }); }); |
And billing address Item's template file at view/frontend/web/template/billing-address/custom-list.html
1 2 3 4 5 6 7 8 9 10 11 | <!-- ko if: (visible)--> < div class = "field addresses" > < div class = "control" > < div class = "billing-address-items" > <!-- ko foreach: { data: elems, as: 'element' } --> <!-- ko template: element.getTemplate() --> <!-- /ko --> <!-- /ko --> </ div > </ div > </ div > <!-- /ko --> |
Now as per highlighted in list.js file we will add JS component and template file.
Add file at view/frontend/web/js/view/billing-address/address-renderer/default.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | define([ 'jquery' , 'ko' , 'uiComponent' , 'Magento_Checkout/js/action/select-billing-address' , 'Magento_Checkout/js/model/quote' , 'Magelearn_ImprovedCheckout/js/model/billing-address/form-popup-state' , 'Magento_Checkout/js/checkout-data' , 'Magento_Customer/js/customer-data' ], function ($, ko, Component, selectBillingAddressAction, quote, formPopUpState, checkoutData, customerData) { 'use strict' ; var countryData = customerData.get( 'directory-data' ); return Component.extend({ defaults: { template: 'Magelearn_ImprovedCheckout/billing-address/address-renderer/default' }, /** @inheritdoc */ initObservable: function () { this ._super(); this .isSelected = ko.computed( function () { var isSelected = false , billingAddress = quote.billingAddress(); if (billingAddress) { isSelected = billingAddress.getKey() == this .address().getKey(); //eslint-disable-line eqeqeq } return isSelected; }, this ); return this ; }, /** * @param {String} countryId * @return {String} */ getCountryName: function (countryId) { return countryData()[countryId] != undefined ? countryData()[countryId].name : '' ; //eslint-disable-line }, getCustomAttributeLabel: function (attribute) { var label; if ( typeof attribute === 'string' ) { return attribute; } if (attribute.label) { return attribute.label; } if (_.isArray(attribute.value)) { label = _.map(attribute.value, function (value) { return this .getCustomAttributeOptionLabel(attribute[ 'attribute_code' ], value) || value; }, this ).join( ', ' ); } else if ( typeof attribute.value === 'object' ) { label = _.map(Object.values(attribute.value)).join( ', ' ); } else { label = this .getCustomAttributeOptionLabel(attribute[ 'attribute_code' ], attribute.value); } return label || attribute.value; }, getCustomAttributeOptionLabel: function (attributeCode, value) { var option, label, options = this .source.get( 'customAttributes' ) || {}; if (options[attributeCode]) { option = _.findWhere(options[attributeCode], { value: value }); if (option) { label = option.label; } } else if (value.file !== null ) { label = value.file; } return label; }, /** Set selected customer shipping address */ selectAddress: function () { selectBillingAddressAction( this .address()); checkoutData.setSelectedBillingAddress( this .address().getKey()); }, /** * Edit address. */ editAddress: function () { formPopUpState.isVisible( true ); this .showPopup(); }, /** * Show popup. */ showPopup: function () { $( '[data-open-modal="opc-new-billing-address"]' ).trigger( 'click' ); } }); }); |
Now as per highlighted code in above file, we will add our default address render file.
Add file at view/frontend/web/template/billing-address/address-renderer/default.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | < div class = "billing-address-item" css = "'selected-item' : isSelected() , 'not-selected-item':!isSelected()" > < text args = "address().prefix" ></ text > < text args = "address().firstname" ></ text > < text args = "address().middlename" ></ text > < text args = "address().lastname" ></ text > < text args = "address().suffix" ></ text > < text args = "_.values(address().street).join(', ')" ></ text > < text args = "address().city " ></ text >, < span text = "address().region" ></ span > < text args = "address().postcode" ></ text > < text args = "getCountryName(address().countryId)" ></ text > < a if = "address().telephone" attr = "'href': 'tel:' + address().telephone" text = "address().telephone" ></ a > < each args = "data: address().customAttributes, as: 'element'" > < text args = "$parent.getCustomAttributeLabel(element)" ></ text > </ each > < button visible = "address().isEditable()" type = "button" class = "action edit-address-link" click = "editAddress" > < span translate = "'Edit'" ></ span > </ button > <!-- ko if: (!isSelected()) --> < button type = "button" click = "selectAddress" class = "action action-select-billing-item" > < span translate = "'Bill Here'" ></ span > </ button > <!-- /ko --> </ div > |
And will also add billing address list template file at view/frontend/web/template/billing-address/list.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <!-- ko foreach: getRegion('before-fields') --> <!-- ko template: getTemplate() --> <!-- /ko --> <!--/ko--> < form data-bind = "attr: {'data-hasrequired': $t('* Required Fields')}" > < fieldset id = "billing-new-address-form" class = "fieldset address" > <!-- ko foreach: getRegion('additional-fieldsets') --> <!-- ko template: getTemplate() --> <!-- /ko --> <!--/ko--> <!-- ko if: (isCustomerLoggedIn) --> < div class = "field save-address" > < input type = "checkbox" class = "checkbox" data-bind = "checked: saveInAddressBook attr: {id: 'billing-save-in-address-book'}" /> < label class = "label" data-bind = "attr: {for: 'billing-save-in-address-book'}" > < span data-bind = "i18n: 'Save in address book'" ></ span > </ label > </ div > <!-- /ko --> </ fieldset > </ form > |
At last we will add our shipping mixin file.
For that first add file at view/frontend/requirejs-config.js
1 2 3 4 5 6 7 8 9 | var config = { config: { mixins: { 'Magento_Checkout/js/view/shipping' : { 'Magelearn_ImprovedCheckout/js/view/shipping-mixin' : true }, } } }; |
Now add file at view/frontend/web/js/view/shipping-mixin.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 | define([ 'jquery' , 'underscore' , 'Magento_Ui/js/form/form' , 'ko' , 'Magento_Customer/js/model/customer' , 'Magento_Customer/js/model/address-list' , 'Magento_Checkout/js/model/address-converter' , 'Magento_Checkout/js/model/quote' , 'Magento_Checkout/js/action/create-shipping-address' , 'Magento_Checkout/js/action/select-shipping-address' , 'Magento_Checkout/js/action/create-billing-address' , 'Magento_Checkout/js/action/select-billing-address' , 'Magento_Checkout/js/model/shipping-rates-validator' , 'Magento_Checkout/js/model/shipping-address/form-popup-state' , 'Magento_Checkout/js/model/shipping-service' , 'Magento_Checkout/js/action/select-shipping-method' , 'Magento_Checkout/js/model/shipping-rate-registry' , 'Magento_Checkout/js/action/set-shipping-information' , 'Magento_Checkout/js/model/step-navigator' , 'Magento_Ui/js/modal/modal' , 'Magento_Checkout/js/model/checkout-data-resolver' , 'Magento_Checkout/js/checkout-data' , 'uiRegistry' , 'mage/translate' , 'Magento_Checkout/js/model/shipping-rate-service' ], function ( $, _, Component, ko, customer, addressList, addressConverter, quote, createShippingAddress, selectShippingAddress, createBillingAddress, selectBillingAddress, shippingRatesValidator, formPopUpState, shippingService, selectShippingMethodAction, rateRegistry, setShippingInformationAction, stepNavigator, modal, checkoutDataResolver, checkoutData, registry, $t ) { 'use strict' ; var popUp = null ; return function (Component) { return Component.extend({ defaults: { template: 'Magelearn_ImprovedCheckout/shipping' , }, isAddressSameAsShipping: ko.observable( true ), isShowBillingForm: ko.observable( false ), initChildren: function () { this .messageContainer = new Messages(); this .createMessagesComponent(); return this ; }, createMessagesComponent: function () { var messagesComponent = { parent: this .name, name: this .name + '.messages' , displayArea: 'messages' , component: 'Magento_Ui/js/view/messages' , config: { messageContainer: this .messageContainer } }; layout([messagesComponent]); return this ; }, setShippingInformation: function () { if ( this .validateShippingInformation() && this .validateBillingInformation()) { setShippingInformationAction().done( function () { stepNavigator.next(); }); } }, /** * @return {Boolean} */ validateShippingInformation: function () { var shippingAddress, addressData, loginFormSelector = 'form[data-role=email-with-possible-login]' , emailValidationResult = customer.isLoggedIn(), field, option = _.isObject( this .countryOptions) && this .countryOptions[quote.shippingAddress().countryId], messageContainer = registry.get( 'checkout.errors' ).messageContainer; if (!quote.shippingMethod()) { this .errorValidationMessage($t( 'Please specify a shipping method.' )); return false ; } if (!customer.isLoggedIn()) { $(loginFormSelector).validation(); emailValidationResult = Boolean($(loginFormSelector + ' input[name=username]' ).valid()); } if ( this .isFormInline) { this .source.set( 'params.invalid' , false ); this .triggerShippingDataValidateEvent(); if (emailValidationResult && this .source.get( 'params.invalid' ) || !quote.shippingMethod()[ 'method_code' ] || !quote.shippingMethod()[ 'carrier_code' ] ) { this .focusInvalid(); return false ; } shippingAddress = quote.shippingAddress(); addressData = addressConverter.formAddressDataToQuoteAddress( this .source.get( 'shippingAddress' ) ); //Copy form data to quote shipping address object for (field in addressData) { if (addressData.hasOwnProperty(field) && //eslint-disable-line max-depth shippingAddress.hasOwnProperty(field) && typeof addressData[field] != 'function' && _.isEqual(shippingAddress[field], addressData[field]) ) { shippingAddress[field] = addressData[field]; } else if ( typeof addressData[field] != 'function' && !_.isEqual(shippingAddress[field], addressData[field])) { shippingAddress = addressData; break ; } } if (customer.isLoggedIn()) { shippingAddress[ 'save_in_address_book' ] = 1; } selectShippingAddress(shippingAddress); } else if (customer.isLoggedIn() && option && option[ 'is_region_required' ] && !quote.shippingAddress().region ) { messageContainer.addErrorMessage({ message: $t( 'Please specify a regionId in shipping address.' ) }); return false ; } if (!emailValidationResult) { $(loginFormSelector + ' input[name=username]' ).focus(); return false ; } return true ; }, validateBillingInformation: function () { var addressData, newBillingAddress; if ($( '[name="billing-address-same-as-shipping"]' ).is( ":checked" )) { if ( this .isFormInline) { var shippingAddress = quote.shippingAddress(); addressData = addressConverter.formAddressDataToQuoteAddress( this .source.get( 'shippingAddress' ) ); //Copy form data to quote shipping address object for ( var field in addressData) { if (addressData.hasOwnProperty(field) && shippingAddress.hasOwnProperty(field) && typeof addressData[field] !== 'function' && _.isEqual(shippingAddress[field], addressData[field]) ) { shippingAddress[field] = addressData[field]; } else if ( typeof addressData[field] !== 'function' && !_.isEqual(shippingAddress[field], addressData[field])) { shippingAddress = addressData; break ; } } if (customer.isLoggedIn()) { shippingAddress.save_in_address_book = 1; } newBillingAddress = createBillingAddress(shippingAddress); selectBillingAddress(newBillingAddress); } else { var billingAddress = quote.shippingAddress(); selectBillingAddress(billingAddress); } return true ; } var selectedAddress = quote.billingAddress(); if (selectedAddress) { if (selectedAddress.customerAddressId) { return addressList.some( function (address) { if (selectedAddress.customerAddressId === address.customerAddressId) { selectBillingAddress(address); return true ; } return false ; }); } else if (selectedAddress.getType() === 'new-customer-address' || selectedAddress.getType() === 'new-billing-address' ) { return true ; } } this .source.set( 'params.invalid' , false ); this .source.trigger( 'billingAddress.data.validate' ); if ( this .source.get( 'billingAddress.custom_attributes' )) { this .source.trigger( 'billingAddress.custom_attributes.data.validate' ); } if ( this .source.get( 'params.invalid' )) { return false ; } addressData = this .source.get( 'billingAddress' ); if ($( '#billing-save-in-address-book' ).is( ":checked" )) { addressData.save_in_address_book = 1; } newBillingAddress = createBillingAddress(addressData); selectBillingAddress(newBillingAddress); return true ; }, /** * @return {Boolean} */ useShippingAddress: function () { if ( this .isAddressSameAsShipping()) { this .isShowBillingForm( false ); } else { this .isShowBillingForm( true ); } return true ; }, }); } }); |
Now as per highlighted code above, we will add our template file at view/frontend/web/template/shipping.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | < li id = "shipping" class = "checkout-shipping-address" data-bind = "fadeVisible: visible()" > < div class = "step-title" translate = "'Shipping Address'" data-role = "title" ></ div > < div id = "checkout-step-shipping" class = "step-content" data-role = "content" > < each if = "!quoteIsVirtual" args = "getRegion('customer-email')" render = "" ></ each > < each args = "getRegion('address-list')" render = "" ></ each > < each args = "getRegion('address-list-additional-addresses')" render = "" ></ each > <!-- Address form pop up --> < if args = "!isFormInline" > < div class = "new-address-popup" > < button type = "button" class = "action action-show-popup" click = "showFormPopUp" visible = "!isNewAddressAdded()" > < span translate = "'New Address'" ></ span > </ button > </ div > < div id = "opc-new-shipping-address" visible = "isFormPopUpVisible()" render = "shippingFormTemplate" ></ div > </ if > < each args = "getRegion('before-form')" render = "" ></ each > <!-- Inline address form --> < render if = "isFormInline" args = "shippingFormTemplate" ></ render > < div class = "step-title" translate = "'Billing Address'" data-role = "title" ></ div > < div id = "checkout-step-billing" class = "step-content" data-role = "content" > < div id = "billing-address-container" > < div class = "billing-address-same-as-shipping-block field choice" > < input type = "checkbox" name = "billing-address-same-as-shipping" data-bind = "checked: isAddressSameAsShipping, click: useShippingAddress, attr: {id: 'billing-address-same-as-shipping-shared'}" /> < label data-bind = "attr: {for: 'billing-address-same-as-shipping-shared'}" >< span data-bind = "i18n: 'Billing address is same as Shipping address (uncheck if you want to use different address).'" ></ span ></ label > </ div > < div class = "form-billing-address" data-bind = "visible: isShowBillingForm" > < each args = "getRegion('billing-address')" render = "" ></ each > </ div > </ div > <!-- Address form pop up --> </ div > </ div > </ li > <!--Shipping method template--> < li id = "opc-shipping_method" class = "checkout-shipping-method" data-bind = "fadeVisible: visible(), blockLoader: isLoading" role = "presentation" > < div class = "checkout-shipping-method" > < div class = "step-title" translate = "'Shipping Methods'" data-role = "title" ></ div > < each args = "getRegion('before-shipping-method-form')" render = "" ></ each > < div id = "checkout-step-shipping_method" class = "step-content" data-role = "content" role = "tabpanel" aria-hidden = "false" > < form id = "co-shipping-method-form" class = "form methods-shipping" if = "rates().length" submit = "setShippingInformation" novalidate = "novalidate" > < render args = "shippingMethodListTemplate" ></ render > < div id = "onepage-checkout-shipping-method-additional-load" > < each args = "getRegion('shippingAdditional')" render = "" ></ each > </ div > < div role = "alert" if = "errorValidationMessage().length" class = "message notice" > < span text = "errorValidationMessage()" ></ span > </ div > < div class = "actions-toolbar" id = "shipping-method-buttons-container" > < div class = "primary" > < button data-role = "opc-continue" type = "submit" class = "button action continue primary" > < span translate = "'Next'" ></ span > </ button > </ div > </ div > </ form > < div class = "no-quotes-block" ifnot="rates().length > 0" translate="'Sorry, no quotes are available for this order at this time'"></ div > </ div > </ div > </ li > |
At last as per highlighted in checkout_index_index.xml we will add our css file at view/frontend/web/css/improvedcheckout.css to display billing address data properly.
0 Comments On "Move billing address just below shipping address and assign billing address from shipping step in Magento2."