Billing State / County is a required field. Error on Woocommerce/WordPress

Why `Billing State / County is a required field` warning still pops out when a filed is hidden in Woocommerce/WordPress. Here, we will explore how this occur and how to resolve it.

Billing State / County is a required field. Error on Woocommerce/WordPress

When customers select specific country like France, South Korea, Vietnam, Germany, the file State/County with be hidden. In a fresh Woocommerce/WordPress site, this field will be not required when it is hidden with certain country. However, in some site, the Billing State / County is a required field warning pops up suggesting the field is still required. Here, we will explore how this occur and how to resolve it.

Primary Plugins

  • Elementor
  • Woolementor
    • Use Billing Address widget
  • Woocommerce

Ideas

  • Explore how Woocommerce handle address field among various country
  • Explore how Woolementor's Address Form behave differently compared to woocommerce
  • Best Guess: Woocommerce's validation library(js) does not refect on the changes Woocommerce made for certain country(in making certain fields invisable)

Finding

  • Billing Address Widget does not work properly on none checkout page
  • Woolementor Billing Address Widget Demo
  • An extra "District" field appear on the demo, but disappear after messing around with the country dropdown (for instance, select China then change it back to France)

Woocommerce

how Woocommerce behave on changed country, in terms of interaction with the server (Ajax)

  • select Country/Region
  • ajax to /?wc-ajax=update_order_review
  • update .woocommerce-checkout-review-order-table & .woocommerce-checkout-payment according to resule

How #billing_state_field is hidden

since no information regarding the appearance of the state/county field is transferred after updating the country field, it is likly JavaScript controlling the process

  • some .js inject display: none in the DOM's style
  • validate-required class is removed
  • <abbr class="required" title="required">*</abbr> inside <label> is replace by <span class="optional">(optional)</span>
  • <input> type and class is change to hidden
  • after searching for code targetting #billing_state_field, found address-i18n.min.js?ver=5.2.2

Relevant source code
In countries that require state/county field

<p class="form-row address-field validate-state validate-required form-row-wide" id="billing_state_field"
  data-priority="80" data-o_class="form-row form-row-wide address-field validate-state" style="">
  <label
    for="billing_state" class="">State / County&nbsp;
    <abbr class="required" title="required">*</abbr>
  </label>
  <span
  class="woocommerce-input-wrapper">
    <input type="text" id="billing_state" name="billing_state" placeholder=""
    data-input-classes="" class="input-text">
  </span>
</p>

For countries that don't require state/county field

<p class="form-row address-field validate-state form-row-wide" id="billing_state_field" 
data-priority="80" data-o_class="form-row form-row-wide address-field validate-state" style="display: none;">
  <label for="billing_state" class="">State / County&nbsp;
    <span class="optional">(optional)</span>
  </label>
  <span class="woocommerce-input-wrapper">
    <input type="hidden" id="billing_state" name="billing_state" placeholder="" data-input-classes="" class="hidden">
  </span>
</p>
l(document.body).on('country_to_state_changing', function (e, a, i) {
    var d = i,
    r = 'undefined' != typeof n[a] ? n[a] : n['default'],
    t = d.find('#billing_postcode_field, #shipping_postcode_field'),
    i = d.find('#billing_city_field, #shipping_city_field'),
    a = d.find('#billing_state_field, #shipping_state_field');
    t.attr('data-o_class') || (t.attr('data-o_class', t.attr('class')), i.attr('data-o_class', i.attr('class')), a.attr('data-o_class', a.attr('class')));
    a = JSON.parse(wc_address_i18n_params.locale_fields);
    l.each(a, function (e, a) {
      var i = d.find(a),
      a = l.extend(!0, {
      }, n['default'][e], r[e]);
      'undefined' != typeof a.label && i.find('label').html(a.label),
      'undefined' != typeof a.placeholder && (i.find(':input').attr('placeholder', a.placeholder), i.find(':input').attr('data-placeholder', a.placeholder), i.find('.select2-selection__placeholder').text(a.placeholder)),
      'undefined' != typeof a.placeholder || 'undefined' == typeof a.label || i.find('label').length || (i.find(':input').attr('placeholder', a.label), i.find(':input').attr('data-placeholder', a.label), i.find('.select2-selection__placeholder').text(a.label)),
      'undefined' != typeof a.required ? o(i, a.required) : o(i, !1),
      'undefined' != typeof a.priority && i.data('priority', a.priority),
      'state' !== e && ('undefined' != typeof a.hidden && !0 === a.hidden ? i.hide().find(':input').val('') : i.show()),
      Array.isArray(a['class']) && (i.removeClass('form-row-first form-row-last form-row-wide'), i.addClass(a['class'].join(' ')))
    }),

Woolementor

How the Billing Address Widget in Woolementor is handling the process
Comparing to the original Woocommerce, for a country required state/county

  • class validate-state is missing
  • data-o_class attribute is missing in page load, but reappears after country selection
    • data-o_class="form-row form-row-wide address-field validate-required"
    • Woocommerce's validate-state is replaced by validate-required
<p class="form-row form-row-wide address-field validate-required" id="billing_state_field" data-priority="10">
  <label
    for="billing_state" class="">State / County&nbsp;
    <abbr class="required" title="required">*</abbr>
  </label>
  <span
    class="woocommerce-input-wrapper">
    <input type="text" class="input-text " name="billing_state" id="billing_state"
      placeholder="" value="" autocomplete="address-level1">
  </span>
</p>

For a country don't require state/county field

  • in attribute data-o_class, validate-required is kept (Possible cause of the warning)
<p class="form-row address-field form-row-wide" id="billing_state_field" data-priority="10" style="display: none;"
  data-o_class="form-row form-row-wide address-field validate-required">
  <label for="billing_state" class="">State /
    County&nbsp;
    <span class="optional">(optional)</span>
  </label>
  <span class="woocommerce-input-wrapper">
    <input
      type="hidden" id="billing_state" name="billing_state" placeholder="" class="hidden undefined">
  </span>
</p>

Solution

Keep the warning(/validation) but make the field state/county reappear

Hardcoding
states.php

	'FR' => array('A', 'B', 'C'), /* add state/county */

class-wc-countries.php

    'FR' => array(
        'postcode' => array(
            'priority' => 65,
        ),
        'state'    => array(
            'required' => true, /* change to true */
        ),
    ),

Hook
enable state/county field

function filter_woocommerce_get_country_locale($locales)
{

	foreach ($locales as $key => $value) {
		if ($key == "FR") {
			$locales[$key]['state']['required'] = true;
			$locales[$key]['state']['hidden'] = false;
			$locales[$key]['state']['placeholder'] = '';
			/* $locales[$key]['state']['label'] = 'Region'; */
		}
	}

	return $locales;
}
add_filter('woocommerce_get_country_locale', 'filter_woocommerce_get_country_locale', 10, 1);

make a list of selection

function custom_woocommerce_states($states)
{

	$states['FR'] = ''; /* display as text input */
    $states['FR'] = array( /* display as dropdown */
        'ABBRA' => 'display namea',
        'ABBRB' => 'display nameb',
    );

	return $states;
}
add_filter('woocommerce_states', 'custom_woocommerce_states');

Remove Validation