How to customize WooCommerce shipping class listing

The simplest thing you might want to achieve is adding a new column, but even this simplicity warrants a couple of commentary, since it has a couple of gotchas that might bind your fingers to its door if carelessly opened. A bit more difficult is adding a contextual action for each row in the shipping class listing.

Adding a new column

Adding a new column to Woo Commerce’s shipping class listing can be broken down into the following steps:

registering the new column’s heading (that is, letting Woo Commerce know that we would like this new column displayed);
providing a value for this new column, for each shipping class record.

First of all, for registering the new column and its heading we will need to use the woocommerce_shipping_classes_columns filter hook. The handler for this hook receives the list of currently registered columns, as an associative array:

– keys are column identifiers (or column codes, if it sounds better to you) and
– values are column labels, which will be the ones actually displayed in the column heading.

Thus, whoever uses this hook can alter the list of columns by:

– removing an existing column using its identifier or code;
– modifying an existing column label using its identifier or code;
adding a new column, using a new identifier and a code (and this is actually the thing we are after).

Here’s an example:

function s03_add_custom_shipping_class_columns(array $columns) {
	$columns['s03_custom_value'] = 'Custom value';
	return $columns;
}

add_filter('woocommerce_shipping_classes_columns', 
	's03_add_custom_shipping_class_columns', 
	10, 
	1);

Decently simple, right? It’s worth also adding that the column identifier (or code) is also used (by Woo Commerce) as a CSS class for each table cell it renders for that column. To illustrate the point, below you can find an excerpt from the original Woo Commerce code (woocommerce/includes/admin/settings/views/html-admin-page-shipping-classes.php):

foreach ( $shipping_class_columns as $class => $heading ) {
	echo '<td class="' . esc_attr( $class ) . '">';
	...
}

Render a cell value for the new column

Now that we’re done with this little piggy, it’s time to make use of this column and actually display a value for each cell. This task too requires some fiddling to get going. For reasons known only to them, the core Woo Commerce developers do provide us with a hook to render some HTML for each cell, but they do NOT provide any parameter to that hook, which means we cannot vary the contents based on anything that’s trustworthy enough.

And, while it is true that they render shipping class grid using Backbone JS templates and that we can insert such a template using the hook that’s available to us, we still need to add our custom data fields to the data set that’s being passed to Backbone JS.

So, let’s first render the Backbone JS template for our custom column cell. We need to use a hook that has a dynamic name: woocommerce_shipping_classes_column_[column_identifier]. In our case, this means: woocommerce_shipping_classes_column_s03_custom_value.

function s03_render_shipping_class_column_custom_value() {
	echo '<div class="view">{{ data.custom_value }}</div>';
}

add_action('woocommerce_shipping_classes_column_s03_custom_value', 
	's03_render_shipping_class_column_custom_value', 
	10);

The {{data.custom_value}} construct is what renders our data field, named custom_value. Unfortunately, there is no straightforward and 100% clean way of providing custom_value. There, is, however, a decent compromise to do so, by means of the woocommerce_get_shipping_classes filter hook.

This is a good change for us to alter the list of all shipping classes, before they are being rendered, but be advised that this can potentially be invoked in other places too. At any rate, the parameter passed to this hook is an array of shipping classes, each a WP_Term instance. While not ideal (I did say compromise, didn’t I?), the solution is to iterate through each record and add a new field to it (since PHP conveniently still allows it):

function s03_add_shipping_classes_custom_value(array $shpClasses) {
	foreach ($shpClasses as $sc) {
		/** @var WP_Term $sc */
		$sc->custom_value = 'Custom value for ' . $sc->slug;
	}
	return $shpClasses;
}

add_filter('woocommerce_get_shipping_classes', 
	's03_add_shipping_classes_custom_value', 
	10, 
	1);

And before someone notes that we might have extended WP_Term, please bear in mind that the WP_Term class is actually final so… no, we could not have extended it. With all this being said and done, here’s the end result:

Modify available row actions

By row actions I mean the action links that are available when one hovers over each row in the Woo Commerce shipping class listing grid. And if you though adding the column gave us the fits, wait until you try modifying the available row actions, because there is no official way of accomplishing it: no hook, no wingardium leviosa, no nothing. This, then, means we actually have to earn our bucks!

The solution proposed here can be rather fragile and care needs to be taken when updating Woo Commerce itself, because it may actually break. This being said, there are a couple of things to consider before writing the actual code:

– if you remember, the list is rendered using Backbone JS templates;
– we cannot not know WHEN the actual DOM elements will become available, since there is no event triggered by Woo Commerce;
– we might hook a timer using window.setTimeout(…) on document ready, but what might be a decent value?

So we’re left with attempting to search for a way to know when an element with a given selector is added to the DOM. jQuery doesn’t do it by default, but there is a library that does it. Finally, we have a grip on the matter!

The element we’ll be watching can be anything we know for sure will be there, but seeing as we just added a new column, let’s use its markup as trigger. Since Woo Commerce adds the column identifier as a class, we can use the CSS class as a selector (.s03_custom_value):

(function($) {
	"use strict";

	function _addCustomRowAction($parentRow, shippingClassId) {
		var linkHtml = [
			'<a href="#" ', 
					'class="s03_bau_link" ', 
					'data-id="' + shippingClassId + '">',
				'Make boo',
			'</a>'
		].join('');
		
		$parentRow
			.find('.row-actions')
			.append(' | ' + linkHtml);
	}

	$.initialize('.s03_custom_value', function() {
		var $me = $(this);
		var $parentRow = $me
			.parent('tr');

		//Following check required, since css class 
		///	is also applied to heading cells
		var shippingClassId = parseInt($parentRow
			.attr('data-id'));

		if (!isNaN(shippingClassId)) {
			_addCustomRowAction($parentRow, shippingClassId);
		}
	});

	$(document).on('click', '.s03_bau_link', function() {
		alert('Boo! (#id: ' + $(this).attr('data-id') + ')!');
	});
})(jQuery);

The thing mostly speaks for itself, but let’s write down a couple of notes:

– the $.initialize call will watch for when an element with the given selector will become available in the DOM and will call the handler we provided;
– the handler is called for EACH element with that class, which actually includes the column heading as well;
– to distinguish, we’re looking for the record identifier, stored by Woo Commerce in the row element’s (tr) data-id attribute.