Magento – Magento 2 – How to add a custom field to checkout and then send it

checkoutfrontendmagento-2.1

All tutorials are covering only adding a fields, but saving value of this fileds is skipped #mindblown. I don't know why, it's most important part of adding any field or form.

I tried to follow Magento docs, but… it sucks.

For testing purposes I try to add another fields to shipping address, just to ignore custom scopes, custom data sets, custom data providers and other undocumented stuff, which looks too weird for me.

I have no idea what mean that form is "static" or "dynamic". For me all checkout forms are builded dynamically on top of KnockoutJS templates, but… when I try "static" way, I'm able to add inputs here (so it's a static form or not?).

First I try to debug why Knockout observables just ignore my fields during parsing and sending data. I found that my fields have empty name parameter, but I can't manage a way to fix this issue. IMO it should be passed to UI component renderer via inputName parameter, same as any other options like disabled, placeholder etc. (other parameters works fine, I checked configuration generated from my XML to checkout module initialization and looks good for me)

Second I tried to use "dynamic" way with creating plugin with LayoutProcessor and passing exactly same data… and now I have fields with names, but sending still doesn't work at all.

After digging into JS I found that preparing this request is maintained in module-checkout/view/frontend/web/js/model/shipping-save-processor/default.js file, which depends on module-checkout/view/frontend/web/js/model/quote.js where Knockout observables are defined/created.

Somehow module-checkout/view/frontend/web/js/model/address-converter.js update this observables and depends on module-checkout/view/frontend/web/js/model/new-customer-address.js, where I finally found some interesting configuration options – list of all address fields.

When I add my fields here, scripts start parsing and sending them, OFC I get 500, b/c backend doesn't recognize them… (don't ask, I'm not a backend dev)

So here comes my questions:

  • It's a right way to handle this type of customization? (b/c looks weird for me)
  • How to send values of fields not related to new addresses? I didn't see similar configuration anywhere. In my case I'd like to send order comment (textarea) and invoice request (checkbox). Both shouldn't be saved as a address, b/c some users might want to save this address for future use.
  • Is there any documentation about "static" and "dynamic" forms or some
    examples / comparison? It's worth thinking about it this way?

Additional existential question:

  • Why this is so inconsistent? Why I have to define tons of parameters in XML/PHP files, while Magento at all can only render a input and then I have to handle everything on my own?

Best Answer

I will try to answer your question(s).

  1. No. This is not a correct way to add custom attributes to the shipping address form. You do not need to edit new-customer-address.js. Indeed, this JS file lists all predefined address attributes and matches corresponding backend interface \Magento\Quote\Api\Data\AddressInterface but Magento provides ability to pass any custom attributes to backend without modification of the backend/frontend components.

    Mentioned JS component has customAttributes property. Your custom attributes will be automatically handled if their $dataScopePrefix is 'shippindAddress.custom_attributes'.

  2. If I understood your question correctly, you have data that is not a part of customer address but you need to send it to backend as well. The answer to this question is:

    It depends. For instance, you can choose the following approach: add custom form to checkout page that includes all your extra fields (like comment, invoice request etc), add JS logic that will handle this form based on some events, and provide a custom service that will receive the data from frontend and store it somewhere for future use.

  3. All official documentation related to checkout is located at http://devdocs.magento.com/guides/v2.1/howdoi/checkout/checkout_overview.html. The term static refers to the forms where all the fields are already known/predefined (for instance: form will always have 2 text fields with predefined labels) and cannot change based on some settings in the backend.

    Such forms can be declared using layout XML configuration. On the other hand, term dynamic refers to forms whose field set can change (for instance: checkout form can have more/less fields based on configuration settings).

    In this case the only way to declare such form is to use LayoutProcessor plugin.

  4. :) Magento tries to cover as much possible use cases that can be significant to the merchants during Magento use/customization as possible. Sometimes this leads to situation when some simple use cases become more complex.

Hope, this helps.

=========================================================================

OK... Lets write some code ;)

  1. PHP code that inserts additional field in LayoutProcessor

========

/**
 * @author aakimov
 */
$customAttributeCode = 'custom_field';
$customField = [
    'component' => 'Magento_Ui/js/form/element/abstract',
    'config' => [
        // customScope is used to group elements within a single form (e.g. they can be validated separately)
        'customScope' => 'shippingAddress.custom_attributes',
        'customEntry' => null,
        'template' => 'ui/form/field',
        'elementTmpl' => 'ui/form/element/input',
        'tooltip' => [
            'description' => 'Yes, this works. I tested it. Sacrificed my lunch break but verified this approach.',
        ],
    ],
    'dataScope' => 'shippingAddress.custom_attributes' . '.' . $customAttributeCode,
    'label' => 'Custom Attribute',
    'provider' => 'checkoutProvider',
    'sortOrder' => 0,
    'validation' => [
       'required-entry' => true
    ],
    'options' => [],
    'filterBy' => null,
    'customEntry' => null,
    'visible' => true,
];

$jsLayout['components']['checkout']['children']['steps']['children']['shipping-step']['children']['shippingAddress']['children']['shipping-address-fieldset']['children'][$customAttributeCode] = $customField;

return $jsLayout;

As I already mentioned, this will add your field to customAttributes property of the JS address object. This property was designed to contain custom EAV address attributes and is related to \Magento\Quote\Model\Quote\Address\CustomAttributeListInterface::getAttributes method.

The code above will automatically handle local storage persistence on frontend. You can get your field value from local storage using checkoutData.getShippingAddressFromData() (where checkoutData is Magento_Checkout/js/checkout-data).

  1. Add mixin to change the behavior of 'Magento_Checkout/js/action/set-shipping-information' (this component is responsible for data submission between shipping and billing checkout steps)

========

2.1. Create your_module_name/view/frontend/requirejs-config.js


/**
 * @author aakimov
 */
var config = {
    config: {
        mixins: {
            'Magento_Checkout/js/action/set-shipping-information': {
                '<your_module_name>/js/action/set-shipping-information-mixin': true
            }
        }
    }
};

2.2. Create your_module_name/view/frontend/web/js/action/set-shipping-information-mixin.js


/**
 * @author aakimov
 */
/*jshint browser:true jquery:true*/
/*global alert*/
define([
    'jquery',
    'mage/utils/wrapper',
    'Magento_Checkout/js/model/quote'
], function ($, wrapper, quote) {
    'use strict';

    return function (setShippingInformationAction) {

        return wrapper.wrap(setShippingInformationAction, function (originalAction) {
            var shippingAddress = quote.shippingAddress();
            if (shippingAddress['extension_attributes'] === undefined) {
                shippingAddress['extension_attributes'] = {};
            }

            // you can extract value of extension attribute from any place (in this example I use customAttributes approach)
            shippingAddress['extension_attributes']['custom_field'] = shippingAddress.customAttributes['custom_field'];
            // pass execution to original action ('Magento_Checkout/js/action/set-shipping-information')
            return originalAction();
        });
    };
});
  1. Create your_module_name/etc/extension_attributes.xml

========

<?xml version="1.0"?>
<!--
/**
 * @author aakimov
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd">
    <extension_attributes for="Magento\Quote\Api\Data\AddressInterface">
        <attribute code="custom_field" type="string" />
    </extension_attributes>
</config>

This will add an extension attribute to address model on backend side. Extension attributes are one of the extension points that Magento provides. To access your data on backend you can use:

// Magento will generate interface that includes your custom attribute
$value = $address->getExtensionAttributes()->getCustomField();

Hope, this helps and will be added to official documentation.