Learn how to add custom fields to settings in WooCommerce meta boxes — and give back to WooCommerce when you find some code that can be improved.
There are times when you need to “mess” with meta boxes in the WooCommerce back end. Meta boxes are the draggable boxes you find in the product, order, and coupon edit pages.
WooCommerce meta boxes contain the current custom post type settings for your products. Think of the “regular price” — that’s a custom field you can find in the “Product Data” meta box in the “product” custom post type edit screen.
Today, we’ll study the woocommerce_wp_text_input
function. It provides a neat way to generate an additional text input in case you need additional settings for your products.
For example, suppose you have a “regular price” and a “sale price” but you also want to have a “list price” or “msrp” — a manufacturer’s suggested retail price (MSRP). There’s two ways you could do this:
- NOT the best way: You could write the MSRP into the HTML description field for every product yourself, but this locks the price as static text in the content where it really doesn’t belong. You might lose track of it or accidentally write over it, and you can’t reuse it on other products without doing the same thing.
- The BEST way: Instead, you can use
woocommerce_wp_text_input
to quickly generate an input field on the back end with the right classes, name and id for the product. That means you can store the additional price in the database once the current post is saved and use it on other products.
As usual, we’ll study the WooCommerce core function code for woocommerce_wp_text_input
in this tutorial, see where and why it’s used, and finally, we’ll cover a quick case study. Enjoy!
Function code & usage
Our woocommerce_wp_text_input
function can be found under \woocommerce\includes\admin\wc-meta-box-functions.php
:
/**
* Output a text input box.
*
* @param array $field
*/
function woocommerce_wp_text_input( $field ) {
global $thepostid, $post;
$thepostid = empty( $thepostid ) ? $post->ID : $thepostid;
$field['placeholder'] = isset( $field['placeholder'] ) ? $field['placeholder'] : '';
$field['class'] = isset( $field['class'] ) ? $field['class'] : 'short';
$field['style'] = isset( $field['style'] ) ? $field['style'] : '';
$field['wrapper_class'] = isset( $field['wrapper_class'] ) ? $field['wrapper_class'] : '';
$field['value'] = isset( $field['value'] ) ? $field['value'] : get_post_meta( $thepostid, $field['id'], true );
$field['name'] = isset( $field['name'] ) ? $field['name'] : $field['id'];
$field['type'] = isset( $field['type'] ) ? $field['type'] : 'text';
$field['desc_tip'] = isset( $field['desc_tip'] ) ? $field['desc_tip'] : false;
$data_type = empty( $field['data_type'] ) ? '' : $field['data_type'];
switch ( $data_type ) {
case 'price':
$field['class'] .= ' wc_input_price';
$field['value'] = wc_format_localized_price( $field['value'] );
break;
case 'decimal':
$field['class'] .= ' wc_input_decimal';
$field['value'] = wc_format_localized_decimal( $field['value'] );
break;
case 'stock':
$field['class'] .= ' wc_input_stock';
$field['value'] = wc_stock_amount( $field['value'] );
break;
case 'url':
$field['class'] .= ' wc_input_url';
$field['value'] = esc_url( $field['value'] );
break;
default:
break;
}
// Custom attribute handling
$custom_attributes = array();
if ( ! empty( $field['custom_attributes'] ) && is_array( $field['custom_attributes'] ) ) {
foreach ( $field['custom_attributes'] as $attribute => $value ) {
$custom_attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $value ) . '"';
}
}
echo '<p class="form-field ' . esc_attr( $field['id'] ) . '_field ' . esc_attr( $field['wrapper_class'] ) . '">
<label for="' . esc_attr( $field['id'] ) . '">' . wp_kses_post( $field['label'] ) . '</label>';
if ( ! empty( $field['description'] ) && false !== $field['desc_tip'] ) {
echo wc_help_tip( $field['description'] );
}
echo '<input type="' . esc_attr( $field['type'] ) . '" class="' . esc_attr( $field['class'] ) . '" style="' . esc_attr( $field['style'] ) . '" name="' . esc_attr( $field['name'] ) . '" id="' . esc_attr( $field['id'] ) . '" value="' . esc_attr( $field['value'] ) . '" placeholder="' . esc_attr( $field['placeholder'] ) . '" ' . implode( ' ', $custom_attributes ) . ' /> ';
if ( ! empty( $field['description'] ) && false === $field['desc_tip'] ) {
echo '<span class="description">' . wp_kses_post( $field['description'] ) . '</span>';
}
echo '</p>';
}
As you can see at the very beginning, woocommerce_wp_text_input
declares two globals: $thepostid
and $post
.
In other words, this is a back end function, because it’s looking for the current post object and post ID. It won’t work on the front end — for example, if you wanted to create a custom checkout field.
The function accepts one argument called $field
. It’s clearly an array because the first few lines of the function look for array values:
$field['placeholder']
$field['class']
$field['style']
$field['wrapper_class']
etc.
So, let’s see which array keys we can pass to woocommerce_wp_text_input
:
- ‘placeholder’
- ‘class’ = ‘short’ by default
- ‘style’
- ‘wrapper_class’
- ‘value’ = get_post_meta( $thepostid, $field[‘id’], true ) by default
- ‘name’ = $field[‘id’] by default
- ‘type’ = ‘text’ by default
- ‘desc_tip’
- ‘data_type’
That’s odd! Can we improve this code in WooCommerce?
A few notes on some odd aspects of the code we just covered:
- In the
woocommerce_wp_text_input
function (and I find this very strange to be honest) there is no initial check just after the global declaration to see if$post
exists as an object. What if this function is misused for some reason? - Also, there is no check whatsoever to see if the passed array has at least the
$field['id'] value
, which you can see is the only real requirement for the HTML output later on in the function.
That’s pretty poor from a development point of view, so it’s worth knowing why that is so if you’re relatively inexperienced. We don’t want to encourage bad coding habits!
Here’s a quick example of why this otherwise useful function is currently a bit deficient in he way it is written. Let’s say I want to add a field below the “regular price” and “sale price” fields in the edit product page.
Under \woocommerce\includes\admin\meta-boxes\views\html-product-data-general.php
I find a nice hook so that I can inject my own code in that exact position, called woocommerce_product_options_pricing
:
Now, if I hook into that and trigger the woocommerce_wp_text_input
function with an empty array…
add_action( 'woocommerce_product_options_pricing', 'bbloomer_below_sale_price' );
function bbloomer_below_sale_price() {
woocommerce_wp_text_input( array() );
}
… I get some really ugly output:
This is a good thing for all developers to learn: a good function should check in the very early stages that the data it requires actually exists, otherwise there is no point in moving forward.
Great! We found a potential enhancement to WooCommerce. It’s not really a bug in this case since it doesn’t break the screen output. Let’s help WooCommerce fix this by posting an issue [enhancement] on their Github page — that’s the beauty of open-source!
Seriously, get involved and give back to the project! It’s that simple and very rewarding as we all learn and improve together. You can jump right in by tackling a “good first issue” like our small enhancement here over at GitHub.
Finishing up our example…
So, let’s imagine this time around that $field['id']
` is defined — the function works!
add_action( 'woocommerce_product_options_pricing', 'bbloomer_below_sale_price' );
function bbloomer_below_sale_price() {
woocommerce_wp_text_input( array( 'id' => 'whatever' ) );
}
Output:
Success! Except…the field label is missing, so we ought to consider $field['label']
another requirement to ensure good output.
One last note on data_type
— you can pass the “price,” “decimal,” “stock,” and “url” values to add the right input classes to format the text.
WooCommerce case study
We’ve already started using woocommerce_wp_text_input
previously, so we only need to “make it official” now.
How do we add the MSRP to the edit product page, just below the sale price?
Easy!
Because it’s a price, let’s set the correct data_type
:
/**
* @snippet MSRP input @ edit product page
* @how-to Get CustomizeWoo.com FREE
* @author Rodolfo Melogli
* @compatible WooCommerce 6
* @donate $9 https://businessbloomer.com/bloomer-armada/
*/
add_action( 'woocommerce_product_options_pricing', 'bbloomer_msrp_backend_output' );
function bbloomer_msrp_backend_output() {
woocommerce_wp_text_input(
array(
'id' => 'msrp',
'label' => 'MSRP',
'data_type' => 'price',
)
);
}
Oh, I forgot to add the “($)” suffix to the label to keep it consistent with the prices above: ‘MSRP ($)’. You can do that with whatever currency symbol you need.
The only problem is that if we enter a price value here and update the product, the data won’t be saved. That’s because no one told WooCommerce to save it! We need to code that too.
Thankfully, I have a code snippet ready for that since I developed something very similar based on RRP. (That’s the European version of MSRP, I believe.) Check out that tutorial because it shows how to display the MSRP/RRP on the front end.
Let’s complete the job so our MSRP data is saved:
/**
* @snippet MSRP input @ edit product page
* @how-to Get CustomizeWoo.com FREE
* @author Rodolfo Melogli
* @compatible WooCommerce 6
* @donate $9 https://businessbloomer.com/bloomer-armada/
*/
add_action( 'woocommerce_product_options_pricing', 'bbloomer_msrp_backend_output' );
function bbloomer_msrp_backend_output() {
woocommerce_wp_text_input(
array(
'id' => 'msrp',
'label' => 'MSRP',
'data_type' => 'price',
)
);
}
add_action( 'save_post_product', 'bbloomer_save_msrp' );
function bbloomer_save_msrp( $product_id ) {
global $typenow;
if ( 'product' === $typenow ) {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
if ( isset( $_POST['msrp'] ) ) {
update_post_meta( $product_id, 'msrp', $_POST['msrp'] );
}
}
}
This should now save the MSRP and display it after a product edit page reload. it will appear inside the input field. (Remember, the input field value is get_post_meta( $thepostid, $field['id'], true )
by default).
Conclusion
Wherever you can add custom fields in the back end, you can use woocommerce_wp_text_input
to generate the HTML output.
Don’t forget you also need to save the value, otherwise the input will keep displaying as empty.
We’ve seen how to add the MSRP, but you can also add a field to define all of the following — and more:
[ products ]
shipping carrier name[ products ]
product brand[ products ]
additional URL[ products ]
“lowest price”[coupons]
URL to redeem a coupon[orders]
a custom message for the accountant
There are millions of applications for woocommerce_wp_text_input
! If you know of one or you’ve coded it already, feel free to share it in the comments! We’d also love to hear how you’ve found and fixed issues in WooCommerce like our small enhancement here. Thanks for reading!
Rodolfo Melogli is an author, WooCommerce expert, and WordCamp speaker, Rodolfo has worked as an independent WooCommerce freelancer since 2011. He teaches WooCommerce development at Business Bloomer.