Dynamic checkout fields based on selected shipping method

By default WooCommerce checkout includes fields for address, post code, city and state, how ever when using a shipping method such as “Local pickup” there is no need to collect client address information.

Here’s some custom code that can be used to dynamically hide certain WC checkout fields based on which shipping method is selected.

Server-side

First, let’s define a function that determines whether some fields should be shown or not:

PHP
function current_shipping_method_requires_address() {

	$chosen_methods = WC()->session->get('chosen_shipping_methods');
	if(!is_array($chosen_methods) || empty($chosen_methods)) {
		return true;
	}
	$chosen_shipping_method = $chosen_methods[0];

	// Don't require address when "Local pickup" method is chosen.
	if(strpos($chosen_shipping_method, 'local_pickup') !== false) {
		return false;
	}

	return true;
}

In this case, we check for the presence of the string local_pickup in the name of the current shipping method.

Now let’s utilize this function to make the fields optional when applicable:

PHP
/** Don't require fields when local pickup shipping method is selected. */
add_filter('woocommerce_billing_fields', function($fields) {

	if(!current_shipping_method_requires_address()) {
		foreach([
			'billing_address_1',
			'billing_address_2',
			'billing_postcode',
			'billing_city',
			'billing_state',
		] as $field) {
			$fields[$field]['required'] = false;
		}
	}

	return $fields;
});

Now whenever the local pickup shipping method is selected, the checkout form submission will succeed even if the specified billing info is not filled in.

To apply this functionality for other shipping methods, simply inspect your checkout form shipping section to find the IDs of shipping methods:

Inspecting shipping methods in checkout form

and then alter the current_shipping_method_requires_address for the desired outcome.

Client-side

While the address-related fields are no longer required, they are still visible in the checkout form and they even have a red asterisk indicating that the fields are required.

To solve this problem you essentially need to reproduce the logic on the front-end as well.

The currentShippingMethodRequiresAddress is a JavaScript equivalent of the current_shipping_method_requires_address PHP function:

JavaScript
const currentShippingMethodRequiresAddress = () => {

	const data = Object.fromEntries(new FormData($('form[name="checkout"]')[0]).entries())
	const shippingMethod = data['shipping_method[0]']

	if(shippingMethod.indexOf('local_pickup') !== -1) {
		return false
	}

	return true
}

Then, using this function we can add the invisible class to fields that shouldn’t be visible, using the updated_checkout hook to make sure the changes are reflected when the user changes the shipping method, for example:

JavaScript
const ADDRESS_FIELDS = [
	'billing_address_1',
	'billing_address_2',
	'billing_postcode',
	'billing_city',
	'billing_state',
]

const updateFieldsVisibility = () => {

	if(currentShippingMethodRequiresAddress()) {
		ADDRESS_FIELDS.forEach(function(id) {
			$(`#\${id}_field`).removeClass('invisible')
		})
	} else {
		ADDRESS_FIELDS.forEach(function(id) {
			$(`#\${id}_field`).addClass('invisible')
		})
	}
}

$(document.body).on('updated_checkout', updateFieldsVisibility)

Then we’ll also need some CSS for the .invisible class to have any effect:

PHP
wp_add_inline_style('woocommerce-inline', '.form-row.invisible:not(#_) { display: none !important; }');

Here is the whole client-side code put together, such that it is only enqueued on the checkout page and the relevant JS and CSS additions are added using wp_add_inline_script and wp_add_inline_style respectively:

PHP
/** Hide hidden fields client-side. */
add_action('woocommerce_checkout_init', function() {

	add_action('wp_enqueue_scripts', function() {

		$js = <<<JS
			;jQuery && jQuery($ => {

				const currentShippingMethodRequiresAddress = () => {

					const data = Object.fromEntries(new FormData($('form[name="checkout"]')[0]).entries())
					const shippingMethod = data['shipping_method[0]']

					if(shippingMethod.indexOf('local_pickup') !== -1) {
						return false
					}

					return true
				}

				const ADDRESS_FIELDS = [
					'billing_address_1',
					'billing_address_2',
					'billing_postcode',
					'billing_city',
					'billing_state',
				]

				const updateFieldsVisibility = () => {

					if(currentShippingMethodRequiresAddress()) {
						ADDRESS_FIELDS.forEach(function(id) {
							$(`#\${id}_field`).removeClass('invisible')
						})
					} else {
						ADDRESS_FIELDS.forEach(function(id) {
							$(`#\${id}_field`).addClass('invisible')
						})
					}
				}

				$(document.body).on('updated_checkout', updateFieldsVisibility)

			});
		JS;

		wp_add_inline_script('wc-checkout', $js, 'after');
		wp_add_inline_style('woocommerce-inline', '.form-row.invisible:not(#_) { display: none !important; }');
	}, 100);
});

Complete code

Here is the final code put together, which you can place in your child theme’s functions.php file, for example.

PHP
function current_shipping_method_requires_address() {

	$chosen_methods = WC()->session->get('chosen_shipping_methods');
	if(!is_array($chosen_methods) || empty($chosen_methods)) {
		return true;
	}
	$chosen_shipping_method = $chosen_methods[0];

	// Don't require address when "Local pickup" method is chosen.
	if(strpos($chosen_shipping_method, 'local_pickup') !== false) {
		return false;
	}

	return true;
}

/** Don't require fields when local pickup shipping method is selected. */
add_filter('woocommerce_billing_fields', function($fields) {

	if(!current_shipping_method_requires_address()) {
		foreach([
			'billing_address_1',
			'billing_address_2',
			'billing_postcode',
			'billing_city',
			'billing_state',
		] as $field) {
			$fields[$field]['required'] = false;
		}
	}

	return $fields;
});

/** Hide hidden fields client-side. */
add_action('woocommerce_checkout_init', function() {

	add_action('wp_enqueue_scripts', function() {

		$js = <<<JS
			;jQuery && jQuery($ => {

				const currentShippingMethodRequiresAddress = () => {

					const data = Object.fromEntries(new FormData($('form[name="checkout"]')[0]).entries())
					const shippingMethod = data['shipping_method[0]']

					if(shippingMethod.indexOf('local_pickup') !== -1) {
						return false
					}

					return true
				}

				const ADDRESS_FIELDS = [
					'billing_address_1',
					'billing_address_2',
					'billing_postcode',
					'billing_city',
					'billing_state',
				]

				const updateFieldsVisibility = () => {

					if(currentShippingMethodRequiresAddress()) {
						ADDRESS_FIELDS.forEach(function(id) {
							$(`#\${id}_field`).removeClass('invisible')
						})
					} else {
						ADDRESS_FIELDS.forEach(function(id) {
							$(`#\${id}_field`).addClass('invisible')
						})
					}
				}

				$(document.body).on('updated_checkout', updateFieldsVisibility)
			});
		JS;

		wp_add_inline_script('wc-checkout', $js, 'after');
		wp_add_inline_style('woocommerce-inline', '.form-row.invisible:not(#_) { display: none !important; }');
	}, 100);
});

You can modify which shipping methods the code applies for and which fields are hidden, but the changes should be applied to both the server-side and client-side code.

Leave a Reply

Your email address will not be published. Required fields are marked *