13

How do you wrap two checkout form elements inside a div?

For example let's say I wanted to wrap these country and zip/postcode fields in a div with the class of example-class, how would I do this?

enter image description here

What I have tried

I tried to achieve this by adding them as children of <item name="shippingAddress" xsi:type="array"> but that just causes errors on the frontend. Although I did get a blank text input without a label inside .example-class there were errors on the frontend.

The error: Cannot read property 'indexedOptions' of undefined

This is my quick attempt:

Magento_Checkout/web/template/shipping-address/form.html

<div id="shipping-new-address-form" class="fieldset address">
    <div class="testing">
        <!-- ko foreach: getRegion('example-class') -->
        <!-- ko template: getTemplate() --><!-- /ko -->
        <!--/ko-->
    </div>
    <!-- ko foreach: getRegion('additional-fieldsets') -->
    <!-- ko template: getTemplate() --><!-- /ko -->
    <!--/ko-->
</div>

checkout_index_index.xml

<item name="example-for-adding-class" xsi:type="array">
    <item name="component" xsi:type="string">uiComponent</item>
    <item name="config" xsi:type="array">
        <item name="deps" xsi:type="array">
            <item name="0" xsi:type="string">checkoutProvider</item>
        </item>
    </item>
    <item name="displayArea" xsi:type="string">example-class</item>
    <item name="children" xsi:type="array">
        <!-- The following items override configuration of corresponding address attributes -->
        <item name="region" xsi:type="array">
            <!-- Make region attribute invisible on frontend. Corresponding input element is created by region_id field -->
            <item name="visible" xsi:type="boolean">false</item>
        </item>
        <item name="region_id" xsi:type="array">
            <item name="component" xsi:type="string">Magento_Ui/js/form/element/region</item>
            <item name="config" xsi:type="array">
                <item name="template" xsi:type="string">ui/form/field</item>
                <item name="elementTmpl" xsi:type="string">ui/form/element/select</item>
                <item name="customEntry" xsi:type="string">shippingAddress.region</item>
            </item>
            <item name="validation" xsi:type="array">
                <item name="required-entry" xsi:type="boolean">true</item>
            </item>
            <!-- Value of region_id field is filtered by the value of county_id attribute -->
            <item name="filterBy" xsi:type="array">
                <item name="target" xsi:type="string"><![CDATA[${ $.provider }:${ $.parentScope }.country_id]]></item>
                <item name="field" xsi:type="string">country_id</item>
            </item>
        </item>
        <item name="postcode" xsi:type="array">
            <!-- post-code field has custom UI component -->
            <item name="component" xsi:type="string">Magento_Ui/js/form/element/post-code</item>
            <item name="sortOrder" xsi:type="string">2</item>
            <item name="validation" xsi:type="array">
                <item name="required-entry" xsi:type="string">true</item>
            </item>
        </item>
        <item name="country_id" xsi:type="array">
            <item name="sortOrder" xsi:type="string">1</item>
        </item>
    </item>
</item>

There has to be an easier way to do this, either I'm missing something or this is the definition of over-engineering. Adding a div over two elements should never be this difficult.

11

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!

  • Amazing, thank you! I would have never have managed that, interesting how it requires backend work to. I was tackling this from a purely FE point of view. If no one comes up with a simpler solution in a few days (as I think some other people are currently looking at this) I'll mark it as accepted. Thanks again. – Ben Crook Nov 10 '17 at 11:25
  • Excellent answer :) – Keyur Shah Nov 15 '17 at 10:23
1

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>
  • I've upvoted as I'm confident it would work but I agree it isn't very clean, I wouldn't use this unless there was no cleaner method. Thanks. – Ben Crook Nov 10 '17 at 11:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.