2

Each field of the default Woocommerce form checkout has this markup:

<p class="form-row form-row-first validate-required" id="billing_first_name_field" data-priority="10">
     <label for="billing_first_name" class="">Name&nbsp;
           <abbr class="required" title="required">*</abbr>
     </label>
     <span class="woocommerce-input-wrapper">
          <input type="text" class="input-text " name="billing_first_name" id="billing_first_name" placeholder="" value="" autocomplete="given-name">
     </span>
</p>

Now, I want:

  1. Wrap each field in a wrapper like <div class="single-field-wrapper"><p [...] </div>
  2. Wrap the first two fields, 'billing_first_name' and 'billing_last_name' in a wrapper like <div class="first-and-second-field-wrapper"> [...] </div>

To achieve this, I've tried to use the filter hooks made available by function woocommerce_form_field( $key, $args, $value = null ) called in form-billing.php template in this way:

<?php
        $fields = $checkout->get_checkout_fields( 'billing' );

        foreach ( $fields as $key => $field ) {
            if ( isset( $field['country_field'], $fields[ $field['country_field'] ] ) ) {
                $field['country'] = $checkout->get_value( $field['country_field'] );
            }
            woocommerce_form_field( $key, $field, $checkout->get_value( $key ) );
        }
    ?>

So in functions.php I wrote:

function change_woocommerce_field_markup($field, $key, $args, $value){

  $field = '<div class="single-field-wrapper">'.$field.'</div>';

  if($key === 'billing_first_name') {

      $field = '<div class="first-and-second-field-wrapper">'.$field;
   }
  else if ($key === 'billing_last_name') {

      $field = $field.'</div>';
   }

   else {
      $field = $field;
   }

    return $field;
   } 

  add_filter("woocommerce_form_field","change_woocommerce_field_markup", 10, 4);

Unexpectedly, I get this markup:

<div class="first-and-second-field-wrapper">
       <div class="single-field-wrapper">
            <div class="single-field-wrapper">
            [here there are all the fields, one under the other ]
            </div>
       <div class="single-field-wrapper"></div> 
       <div class="single-field-wrapper"></div> 
       <div class="single-field-wrapper"></div> 
       <div class="single-field-wrapper"></div> 
       <div class="single-field-wrapper"></div>   
       <div class="single-field-wrapper"></div> 
       <div class="single-field-wrapper"></div> 
       <div class="single-field-wrapper"></div> 
 </div>

Anyone can explain why this unexpected behavior?

  • Can you show us the code that adds the div tag with the "single-field-wrapper" class? – user141080 Jul 28 '18 at 6:18
  • Sure, I missed it, now I've edited the post. It's in change_woocommerce_field_markup function.. – AmintaCode Jul 28 '18 at 8:49
4

There is no hook woocommerce_form_field, there is hook woocommerce_form_field_{$args[type]} (doc).

$args[type] can be (look here for available options):

  • text,
  • checkbox,
  • country,
  • ...

Code below will wrap 'billing_first_name' and 'billing_last_name' fields in a wrapper like <div class="first-and-second-field-wrapper"> [...] </div>.

function change_woocommerce_field_markup($field, $key, $args, $value) {

   $field = '<div class="single-field-wrapper">'.$field.'</div>';

   if($key === 'billing_first_name')
      $field = '<div class="first-and-second-field-wrapper">'.$field;
   else if ($key === 'billing_last_name')
      $field = $field.'</div>';

    return $field;
} 

add_filter("woocommerce_form_field_text","change_woocommerce_field_markup", 10, 4);

It will also wrap text type fields with <div class="single-field-wrapper">...</div>.
BUT
some text fields that have own type (like state or email) require additional hooks, for example:

add_filter("woocommerce_form_field_country","change_woocommerce_field_markup", 10, 4);
add_filter("woocommerce_form_field_email","change_woocommerce_field_markup", 10, 4);

UPDATE #1

Above code works in WC-v3.3.7.

In WC-v3.4.xx you get this:

<div class="first-and-second-field-wrapper">
    <div class="single-field-wrapper">
        <div class="single-field-wrapper">
            [here there are all the fields, one under the other ]
        </div>
    <div class="single-field-wrapper"></div> 
    <div class="single-field-wrapper"></div> 
    <div class="single-field-wrapper"></div> 
    ....
    <div class="single-field-wrapper"></div>
</div>

because javascript sorts form rows inside .woocommerce-billing-fields__field-wrapper. Look at the file woocommerce/assets/js/frontend/address-i18n.js, from line 99.
JS finds all HTML tags with ".form-row" class inside wrapper, takes parent of first item (tag with class .form-row), sort items by priority and insert them into previously selected parent element.

For the test, change in file address-i18.js, in line 99
var fieldsets = $('.woocommerce-billing-fields__field-wrapper, ...
to
var fieldsets = $('.woocommerce-billing-fields__field-wrapper2, ...
and upload as address-i18.min.js.

Removing JS sorting is not a solution, it is just a test. Whithout sorting by JS you will get this:

<div class="first-and-second-field-wrapper">
    <div class="single-field-wrapper">
        <p class="form-row ..." id="billing_first_name_field"> <label for="billing_first_name"> ... </p>
    </div>
    <div class="single-field-wrapper">
        <p class="form-row ..." id="billing_last_name_field"> <label for="billing_last_name">... </p>
    </div>
</div>
<div class="single-field-wrapper">
   <p class="form-row ..." id="billing_company_field"> <label for="billing_company">...  </p>
</div>
  • function woocommerce_form_field( $key, $args, $value = null ) has - see at the end of source code - /** * General filter on form fields. * * @since 3.4.0 */ $field = apply_filters( 'woocommerce_form_field', $field, $key, $args, $value ); See line 2663 github.com/woocommerce/woocommerce/blob/master/includes/…. – AmintaCode Jul 28 '18 at 11:13
  • Moreover, your code unfortunately doesn't make any change to the actual behavior.. See !this image – AmintaCode Jul 28 '18 at 11:16
  • OK, I checked only in the documentation, and there was no this filter. – nmr Jul 28 '18 at 11:49
  • Ok, but what about the main topic? Still I can't understand why your and my solution don't work... – AmintaCode Jul 28 '18 at 12:06
  • 2
    Thanks for the correct answer! It's INCREDIBLE how the Javascript part of Woocommerce is totally undocumented, unpredictable and difficult to hook into.... – AmintaCode Jul 28 '18 at 14:17
1

I just ran into the exact same issue that I hadn't spotted before. I originally opened this ticket on the WC GitHub back in 2015, so it's great that they added the filter... but the JS-based reshuffling is a pain. However, nmr's answer pointed me in the right direction:

As the JS reshuffle works on the .form-row element, you just need to a) remove it from the field you want to wrap, and b) add it to your own new field wrapper:

function change_woocommerce_field_markup( $field, $key, $args, $value ) {

    //  Remove the .form-row class from the current field wrapper

    $field = str_replace('form-row', '', $field);

    //  Wrap the field (and its wrapper) in a new custom div, adding .form-row so the reshuffling works as expected, and adding the field priority

    $field = '<div class="form-row single-field-wrapper" data-priority="' . $args['priority'] . '">' . $field . '</div>';

    return $field;
}

add_filter( 'woocommerce_form_field', 'change_woocommerce_field_markup', 10, 4 );
  • Thanks, it worked great. In my case, I also added an ID with $key to fine tune the styling of the new wrapper: $field = '<div class="form-row single-field-wrapper" id="' . $key . '_wrapper" data-priority="' . $args['priority'] . '">' . $field . '</div>'; – Marcio Duarte Oct 14 at 21:05
  • I found out the this breaks the woocommerce-invalid class in the original wrapper (<p>). To solve this, a custom checkout.js is needed, where you change the jQuery selector to find the <p> again, on line 203 ($parent = $this.closest( '.form-row p' ),) – Marcio Duarte Oct 14 at 22:03

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.