Checkout Form - How to wrap multiple elements in a class - Magento 2

Very interesting question. Let me answer the last assumption about the Checkout implementation. It might be over-engineered a bit since you have to add more than just 1 change in 1 file.

The approach does not require performing modifications in the Magento 2 core modules.

In order to achieve your goal and wrap checkout shipping address fields into a custom element the following elements should be added:

  1. Custom checkout_index_index.xml file with the new UI Component definition
  2. New HTML template with custom element
  3. Layout Processor plugin
  4. The di.xml declaration for the new plugin

The Custom_Checkout\view\frontend\layout\checkout_index_index.xml file:

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="checkout" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <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="shipping-address-fieldset" xsi:type="array">
                                                        <item name="children" xsi:type="array">
                                                            <item name="custom-field-group" xsi:type="array">
                                                                <item name="component" xsi:type="string">uiComponent</item>
                                                                <item name="sortOrder" xsi:type="string">0</item>
                                                                <item name="template" xsi:type="string">Custom_Checkout/checkout/field-group</item>
                                                                <item name="children" xsi:type="array">
                                                                    <item name="field-group" xsi:type="array">
                                                                        <item name="component" xsi:type="string">uiComponent</item>
                                                                        <item name="displayArea" xsi:type="string">field-group</item>
                                                                    </item>
                                                                </item>
                                                            </item>
                                                        </item>
                                                    </item>
                                                </item>
                                            </item>
                                        </item>
                                    </item>
                                </item>
                            </item>
                        </item>
                    </item>
                </item>
            </argument>
        </arguments>
    </referenceBlock>
</body>

In the layout, we should add new custom-field-group UI Component. The component has its own template Custom_Checkout\view\web\template\checkout\field-group.html where all fields are rendered. Also, the custom-field-group component has "0" value for sortOrder node. It allows rendering the component before all fields declared as part of the shipping-address-fieldset component.

Also, there is a field-group UI Component with its own displayArea setting.

The Custom_Checkout\view\web\template\checkout\field-group.html template file:

<div class="custom">
<!-- ko foreach: getRegion('field-group') -->
<!-- ko template: getTemplate() --><!-- /ko -->
<!--/ko-->
</div>

The template allows rendering all components added into the field-group region (also known as displayArea).

The Custom\Checkout\Plugin\AddressLayoutProcessor class file:

namespace Custom\Checkout\Plugin;

use Magento\Checkout\Block\Checkout\LayoutProcessor;

/**
 * Class AddressLayoutProcessor
 */
class AddressLayoutProcessor
{
    /**
     * @param LayoutProcessor $subject
     * @param array $jsLayout
     * @return array
     */
    public function afterProcess(LayoutProcessor $subject, array $jsLayout)
    {
        $fieldGroup = &$jsLayout['components']['checkout']['children']['steps']['children']['shipping-step']
            ['children']['shippingAddress']['children']['shipping-address-fieldset']
            ['children']['custom-field-group']['children']['field-group']['children'];

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

        $fieldGroup['country_id'] = $shippingAddressFields['country_id'];
        $fieldGroup['postcode'] = $shippingAddressFields['postcode'];

        $shippingAddressFields['country_id']['visible'] = false;
        $shippingAddressFields['postcode']['visible'] = false;

        return $jsLayout;
    }
}

The class is responsible for copying both country_id and postcode fields configurations into the newly created custom-field-group component.

The fields, once assigned to the custom-field-group should be marked as hidden (visible = true) in order to avoid duplication during rendering. The componentDisabled should not be used for disabling country_id and postcode due to other dependencies (e.g. region.js file) and shipping address processing mechanism.

The Custom\Checkout\etc\frontend\di.xml file:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Checkout\Block\Checkout\LayoutProcessor">
        <plugin name="customFieldGroupPlugin" type="Custom\Checkout\Plugin\AddressLayoutProcessor"/>
    </type>
</config>

The plugin approach used for the fields changes because fields should be copied with complete configuration. In case Layout Processor declared in a custom module the plugin will catch changes.

As a result, both the country_id and the postcode fields are rendered in the shipping address form and wrapped into the custom element as it below (I added few styles for the custom CSS class to make stand out in the form):

enter image description here

If you also would like to do modifications to a billing address form, the Custom\Checkout\Plugin\AddressLayoutProcessor class should be updated. All you have to do is to perform same manipulations with billing address for specific payment method as we have for shipping address fields.

Happy to help!


This is not a recommended way, it's simple but not elegant:

app/code/Vendor/Module/view/frontend/layout/checkout_index_index.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="checkout" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="content">
          <block class="Vendor\Salesman\Block\Checkout\Index" name="custom_checkout" before="-" template="Vendor_Module::checkout/index.phtml"/>
        </referenceContainer>
    </body>
</page>

app/code/Lime/Salesman/view/frontend/templates/checkout/index.phtml

<script>
  require([
      'jquery',
      'mage/mage'
  ], function($){
      $(document).ready(function () {
         //detect if the shipping form container loaded
         var existCondition = setInterval(function() {
            if ($('#shipping').length) {
              moveElement();
            }
         }, 100);

         function moveElement(){
             //get The field postcode and country
             var postcodeField = $("div[name='shippingAddress.postcode']");
             var countryField = $("div[name='shippingAddress.country_id']");
             // insert the wrapeer
             $( '<div class="wrapper"></div>' ).insertBefore( postcodeField);
             // move the fields to wrapper
             $(".wrapper").append(postcodeField);
             $(".wrapper").append(countryField);
         }
      });
    }
  });
</script>