Magento2 checkout page edit address

1.点击Editing 编辑地址执行流程

    pub/static/frontend/Ayd/inf/hu_HU/Magento_Checkout/js/view/shipping-address/address-renderer/default.js

    editAddress: function () {
    formPopUpState.isVisible(true);
    this.showPopup();

    },

    /**
    * Show popup.
    */
    showPopup: function () {
    $('[data-open-modal="opc-new-shipping-address"]').trigger('click');
    }

    editAddress 函数

    功能:这是入口函数。它首先设置一个可观察的状态变量 formPopUpState.isVisible 为 true,标志着弹窗应该被打开

    逻辑:随后它调用 showPopup 方法来实际触发弹窗的显示。将状态管理和实际触发分离,是一种比较清晰的代码组织方式。

    showPopup 函数

    功能:此函数通过 jQuery 选择器 $('[data-open-modal="opc-new-shipping-address"]') 查找到负责打开新送货地址模态框(弹窗)的按钮或元素,并模拟一次点击事件(.trigger('click')

    pub/static/frontend/Ayd/inf/hu_HU/Magento_Checkout/template/shipping.html

    <!--
    /**
    * Copyright © Magento, Inc. All rights reserved.
    * See COPYING.txt for license details.
    */
    -->
    <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>
    </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>

    2. 点击Shipping 保存

    保存的js页面 pub/static/frontend/Ayd/inf/hu_HU/Magento_Checkout/js/view/shipping.js

    /**
    * Copyright © Magento, Inc. All rights reserved.
    * See COPYING.txt for license details.
    */

    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/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,
    shippingRatesValidator,
    formPopUpState,
    shippingService,
    selectShippingMethodAction,
    rateRegistry,
    setShippingInformationAction,
    stepNavigator,
    modal,
    checkoutDataResolver,
    checkoutData,
    registry,
    $t
    ) {
    'use strict';

    var popUp = null;

    return Component.extend({
    defaults: {
    template: 'Magento_Checkout/shipping',
    shippingFormTemplate: 'Magento_Checkout/shipping-address/form',
    shippingMethodListTemplate: 'Magento_Checkout/shipping-address/shipping-method-list',
    shippingMethodItemTemplate: 'Magento_Checkout/shipping-address/shipping-method-item',
    imports: {
    countryOptions: '${ $.parentName }.shippingAddress.shipping-address-fieldset.country_id:indexedOptions'
    }
    },
    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(),

    /**
    * @return {exports}
    */
    initialize: function () {
    var self = this,
    hasNewAddress,
    fieldsetName = 'checkout.steps.shipping-step.shippingAddress.shipping-address-fieldset';

    this._super();

    if (!quote.isVirtual()) {
    stepNavigator.registerStep(
    'shipping',
    '',
    $t('Shipping'),
    $t('Shipping'),
    this.visible, _.bind(this.navigate, this),
    this.sortOrder
    );
    }
    checkoutDataResolver.resolveShippingAddress();

    hasNewAddress = addressList.some(function (address) {
    return address.getType() == 'new-customer-address'; //eslint-disable-line eqeqeq
    });

    this.isNewAddressAdded(hasNewAddress);

    this.isFormPopUpVisible.subscribe(function (value) {
    if (value) {
    self.getPopUp().openModal();
    }
    });

    quote.shippingMethod.subscribe(function () {
    self.errorValidationMessage(false);
    });

    registry.async('checkoutProvider')(function (checkoutProvider) {
    var shippingAddressData = checkoutData.getShippingAddressFromData();

    if (shippingAddressData) {
    checkoutProvider.set(
    'shippingAddress',
    $.extend(true, {}, checkoutProvider.get('shippingAddress'), shippingAddressData)
    );
    }
    checkoutProvider.on('shippingAddress', function (shippingAddrsData, changes) {
    var isStreetAddressDeleted, isStreetAddressNotEmpty;

    /**
    * In last modifying operation street address was deleted.
    * @return {Boolean}
    */
    isStreetAddressDeleted = function () {
    var change;

    if (!changes || changes.length === 0) {
    return false;
    }

    change = changes.pop();

    if (_.isUndefined(change.value) || _.isUndefined(change.oldValue)) {
    return false;
    }

    if (!change.path.startsWith('shippingAddress.street')) {
    return false;
    }

    return change.value.length === 0 && change.oldValue.length > 0;
    };

    isStreetAddressNotEmpty = shippingAddrsData.street && !_.isEmpty(shippingAddrsData.street[0]);

    if (isStreetAddressNotEmpty || isStreetAddressDeleted()) {
    checkoutData.setShippingAddressFromData(shippingAddrsData);
    }
    });
    shippingRatesValidator.initFields(fieldsetName);
    });

    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.getShippingAddressFromData());
    };
    popUp = modal(this.popUpForm.options, $(this.popUpForm.element));
    }

    return popUp;
    },

    /**
    * Revert address and close modal.
    */
    onClosePopUp: function () {
    checkoutData.setShippingAddressFromData($.extend(true, {}, this.temporaryAddress));
    this.getPopUp().closeModal();
    },

    /**
    * Show address form popup
    */
    showFormPopUp: function () {
    this.isFormPopUpVisible(true);
    },

    /**
    * Save new shipping address
    */
    saveNewAddress: function () {
    var addressData,
    newShippingAddress;

    this.source.set('params.invalid', false);
    this.triggerShippingDataValidateEvent();

    if (!this.source.get('params.invalid')) {
    addressData = this.source.get('shippingAddress');
    // 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 shipping address
    newShippingAddress = createShippingAddress(addressData);
    selectShippingAddress(newShippingAddress);
    checkoutData.setSelectedShippingAddress(newShippingAddress.getKey());
    checkoutData.setNewCustomerShippingAddress($.extend(true, {}, addressData));
    this.getPopUp().closeModal();
    this.isNewAddressAdded(true);
    }
    },

    /**
    * Shipping Method View
    */
    rates: shippingService.getShippingRates(),
    isLoading: shippingService.isLoading,
    isSelected: ko.computed(function () {
    return checkoutData.getSelectedShippingRate() ? checkoutData.getSelectedShippingRate() :
    quote.shippingMethod() ?
    quote.shippingMethod()['carrier_code'] + '_' + quote.shippingMethod()['method_code'] :
    null;
    }),

    /**
    * @param {Object} shippingMethod
    * @return {Boolean}
    */
    selectShippingMethod: function (shippingMethod) {
    selectShippingMethodAction(shippingMethod);
    checkoutData.setSelectedShippingRate(shippingMethod['carrier_code'] + '_' + shippingMethod['method_code']);

    return true;
    },

    /**
    * Set shipping information handler
    */
    setShippingInformation: function () {
    if (this.validateShippingInformation()) {
    quote.billingAddress(null);
    checkoutDataResolver.resolveBillingAddress();
    registry.async('checkoutProvider')(function (checkoutProvider) {
    var shippingAddressData = checkoutData.getShippingAddressFromData();

    if (shippingAddressData) {
    checkoutProvider.set(
    'shippingAddress',
    $.extend(true, {}, checkoutProvider.get('shippingAddress'), shippingAddressData)
    );
    }
    });
    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('The shipping method is missing. Select the shipping method and try again.')
    );

    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 (!quote.shippingMethod()['method_code']) {
    this.errorValidationMessage(
    $t('The shipping method is missing. Select the shipping method and try again.')
    );
    }

    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]').trigger('focus');

    return false;
    }

    return true;
    },

    /**
    * Trigger Shipping data Validate Event.
    */
    triggerShippingDataValidateEvent: function () {
    this.source.trigger('shippingAddress.data.validate');

    if (this.source.get('shippingAddress.custom_attributes')) {
    this.source.trigger('shippingAddress.custom_attributes.data.validate');
    }
    }
    });
    });

    分析上面js

    弹窗管理

    /**
     * 获取或创建地址编辑弹窗
     */
    getPopUp: function () {
        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',
                    click: this.onClosePopUp.bind(this)    // 关闭弹窗
                }
            ];
            
            popUp = modal(this.popUpForm.options, $(this.popUpForm.element));
        }
        return popUp;
    }

    保存新地址

     /**
             * Save new shipping address
             */
            saveNewAddress: function () {
                var addressData,
                    newShippingAddress;
    
                this.source.set('params.invalid', false);
                this.triggerShippingDataValidateEvent();
    
                if (!this.source.get('params.invalid')) {
                    addressData = this.source.get('shippingAddress');
                    // 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 shipping address
                    newShippingAddress = createShippingAddress(addressData);
                    selectShippingAddress(newShippingAddress);
                    checkoutData.setSelectedShippingAddress(newShippingAddress.getKey());
                    checkoutData.setNewCustomerShippingAddress($.extend(true, {}, addressData));
                    this.getPopUp().closeModal();
                    this.isNewAddressAdded(true);
                }
            },

    其中createShippingAddress 如下:

        newShippingAddress = createShippingAddress(addressData);
                    selectShippingAddress(newShippingAddress);
                    checkoutData.setSelectedShippingAddress(newShippingAddress.getKey());
                    checkoutData.setNewCustomerShippingAddress($.extend(true, {}, addressData));
                    this.getPopUp().closeModal();
                    this.isNewAddressAdded(true);

    该方法在pub/static/frontend/Ayd/inf/hu_HU/Magento_Checkout/js/action/create-shipping-address.js

    /**
     * Copyright © Magento, Inc. All rights reserved.
     * See COPYING.txt for license details.
     */
    
    /**
     * @api
     */
    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;
        };
    });
    

    这个createShippingAddress函数负责将表单地址数据转换为报价(Quote)地址对象,并管理地址列表的更新。让我详细分析其工作原理。

    addressConverter : Magento_Checkout/js/model/address-converter.js'
    var address = addressConverter.formAddressDataToQuoteAddress(addressData);

    作用:将UI组件的表单数据转换为Quote地址的标准格式

    /**
     * Copyright © Magento, Inc. All rights reserved.
     * See COPYING.txt for license details.
     */
    /**
     * @api
     */
    define([
        'jquery',
        'Magento_Checkout/js/model/new-customer-address',
        'Magento_Customer/js/customer-data',
        'mage/utils/objects',
        'underscore'
    ], function ($, address, customerData, mageUtils, _) {
        'use strict';
    
        var countryData = customerData.get('directory-data');
    
        return {
            /**
             * Convert address form data to Address object
             *
             * @param {Object} formData
             * @returns {Object}
             */
            formAddressDataToQuoteAddress: function (formData) {
                // clone address form data to new object
                var addressData = $.extend(true, {}, formData),
                    region,
                    regionName = addressData.region,
                    customAttributes;
    
                if (mageUtils.isObject(addressData.street)) {
                    addressData.street = this.objectToArray(addressData.street);
                }
    
                addressData.region = {
                    'region_id': addressData['region_id'],
                    'region_code': addressData['region_code'],
                    region: regionName
                };
    
                if (addressData['region_id'] &&
                    countryData()[addressData['country_id']] &&
                    countryData()[addressData['country_id']].regions
                ) {
                    region = countryData()[addressData['country_id']].regions[addressData['region_id']];
    
                    if (region) {
                        addressData.region['region_id'] = addressData['region_id'];
                        addressData.region['region_code'] = region.code;
                        addressData.region.region = region.name;
                    }
                } else if (
                    !addressData['region_id'] &&
                    countryData()[addressData['country_id']] &&
                    countryData()[addressData['country_id']].regions
                ) {
                    addressData.region['region_code'] = '';
                    addressData.region.region = '';
                }
                delete addressData['region_id'];
    
                if (addressData['custom_attributes']) {
                    addressData['custom_attributes'] = _.map(
                        addressData['custom_attributes'],
                        function (value, key) {
                            customAttributes = {
                                'attribute_code': key,
                                'value': value
                            };
    
                            if (typeof value === 'boolean') {
                                customAttributes = {
                                    'attribute_code': key,
                                    'value': value,
                                    'label': value === true ? 'Yes' : 'No'
                                };
                            }
    
                            return customAttributes;
                        }
                    );
                }
    
                return address(addressData);
            },
    
            /**
             * Convert Address object to address form data.
             *
             * @param {Object} addrs
             * @returns {Object}
             */
            quoteAddressToFormAddressData: function (addrs) {
                var self = this,
                    output = {},
                    streetObject,
                    customAttributesObject;
    
                $.each(addrs, function (key) {
                    if (addrs.hasOwnProperty(key) && typeof addrs[key] !== 'function') {
                        output[self.toUnderscore(key)] = addrs[key];
                    }
                });
    
                if (Array.isArray(addrs.street)) {
                    streetObject = {};
                    addrs.street.forEach(function (value, index) {
                        streetObject[index] = value;
                    });
                    output.street = streetObject;
                }
    
                //jscs:disable requireCamelCaseOrUpperCaseIdentifiers
                if (Array.isArray(addrs.customAttributes)) {
                    customAttributesObject = {};
                    addrs.customAttributes.forEach(function (value) {
                        customAttributesObject[value.attribute_code] = value.value;
                    });
                    output.custom_attributes = customAttributesObject;
                }
                //jscs:enable requireCamelCaseOrUpperCaseIdentifiers
    
                return output;
            },
    
            /**
             * @param {String} string
             */
            toUnderscore: function (string) {
                return string.replace(/([A-Z])/g, function ($1) {
                    return '_' + $1.toLowerCase();
                });
            },
    
            /**
             * @param {Object} formProviderData
             * @param {String} formIndex
             * @return {Object}
             */
            formDataProviderToFlatData: function (formProviderData, formIndex) {
                var addressData = {};
    
                $.each(formProviderData, function (path, value) {
                    var pathComponents = path.split('.'),
                        dataObject = {};
    
                    pathComponents.splice(pathComponents.indexOf(formIndex), 1);
                    pathComponents.reverse();
                    $.each(pathComponents, function (index, pathPart) {
                        var parent = {};
    
                        if (index == 0) { //eslint-disable-line eqeqeq
                            dataObject[pathPart] = value;
                        } else {
                            parent[pathPart] = dataObject;
                            dataObject = parent;
                        }
                    });
                    $.extend(true, addressData, dataObject);
                });
    
                return addressData;
            },
    
            /**
             * Convert object to array
             * @param {Object} object
             * @returns {Array}
             */
            objectToArray: function (object) {
                var convertedArray = [];
    
                $.each(object, function (key) {
                    return typeof object[key] === 'string' ? convertedArray.push(object[key]) : false;
                });
    
                return convertedArray.slice(0);
            },
    
            /**
             * @param {Object} addrs
             * @return {*|Object}
             */
            addressToEstimationAddress: function (addrs) {
                var self = this,
                    estimatedAddressData = {};
    
                $.each(addrs, function (key) {
                    estimatedAddressData[self.toUnderscore(key)] = addrs[key];
                });
    
                return this.formAddressDataToQuoteAddress(estimatedAddressData);
            }
        };
    });
    

    addressList :  'Magento_Customer/js/model/address-list',

    发表评论