Recently we had a client who was not very impressed with the checkboxes offered by WooCommerce product addons.  While it's a great plugin, the out-of-the-box styling does leave a little to be desired.  

We decided that images of these items would be a better way to get customers to commit to spending on these extra features and add a little bit more style to the page.

Adding the custom field to WooCommerce product addons

The first thing you need to do is add a place for us to attach images.  Luckily the people at Woocommerce are great about leaving hooks throughout their code for easy extension.  Start by adding this to your functions.php file.

/**
 * Add custom addon field
 */
function echo5_add_checkbox_image_field($post, $product_addons, $loop, $option) {
    wp_enqueue_media();
    ob_start();
    ?>
    <td class="checkbox_column">
        <input type="hidden" name="product_addon_option_image[<?php echo $loop; ?>][]" value="<?php echo esc_attr( $option['image']  ); ?>" class="image_attachment_id" />
        <?php if (is_numeric($option['image'])) { 
            $image_src = wp_get_attachment_image_src($option['image']);
            ?>
            <img class="image-preview" src="<?php echo $image_src[0]; ?>" width="60" height="60" style="max-height: 60px; width: 60px;">
        <?php } ?>
        <input type="button" class="button upload_image_button" value="<?php _e( 'Upload image' ); ?>" />
    </td>
    <?php
    $output = ob_get_clean();
    echo $output;

}
add_action('woocommerce_product_addons_panel_option_row', 'echo5_add_checkbox_image_field', 10, 4);

Let's break this down a bit.

wp_enqueue_media();

We need to go ahead and tell WordPress to get its media file assets ready so that we can use the native image uploader to add our images.  There are other options available, such as text fields or a file upload field, but these aren't nearly as user friendly. 

<input type="hidden" name="product_addon_option_image[<?php echo $loop; ?>][]" value="<?php echo esc_attr( $option['image']  ); ?>" class="image_attachment_id" />
<?php if (is_numeric($option['image'])) { 
    $image_src = wp_get_attachment_image_src($option['image']);
    ?>
    <img class="image-preview" src="<?php echo $image_src[0]; ?>" width="60" height="60" style="max-height: 60px; width: 60px;">
<?php } ?>
<input type="button" class="button upload_image_button" value="<?php _e( 'Upload image' ); ?>" />

Here we're creating a hidden field for the image ID so that users can't directly modify it.  Then if an image ID is already set, we'll retrieve it so we can display it to users.  And finally we have the upload button for adding or changing images.

Also let's go ahead and add headers to that table so we don't break the layout:

/**
 * Add checkbox headings to addon fields
 */
function echo5_add_checkbox_heading_fields($post, $addon, $loop) {
    echo '<th class="checkbox_column"><span class="column-title">Image</span></th>';
}
add_action('woocommerce_product_addons_panel_option_heading', 'echo5_add_checkbox_heading_fields', 10, 3);

Making the upload buttons work

So far we have added the upload buttons, but they don't do anything.  Let's enqueue our Javascript in our functions.php file.

/**
 * Admin scripts
 */
function echo5_admin_scripts_and_styles($hook) {
    wp_enqueue_script( 'echo5-admin', get_stylesheet_directory_uri() . '/admin.js' );
    global $post;
    wp_localize_script('echo5-admin', 'post', array(
            'post_id' => $post->ID
        )
    );
}
add_action( 'admin_enqueue_scripts', 'echo5_admin_scripts_and_styles' );

You may notice that after we enqueue the script we're localizing it as well.

global $post;
wp_localize_script('echo5-admin', 'post', array(
        'post_id' => $post->ID
    )
);

This is because we'll need the post ID to attach the image to later.  And then in our admin.js we'll add a slightly modified WordPress media uploader.

/**
 * Image uploader
 */
jQuery( document ).ready( function( $ ) {
	// Uploading files
	var file_frame;
	if (typeof wp.media !== 'undefined' && wp.media !== null) {
		var wp_media_post_id = wp.media.model.settings.post.id; // Store the old id
	}
	// var set_to_post_id = <?php echo $my_saved_attachment_post_id; ?>; // Set this
	var set_to_post_id = post.id;
	var $this = null;
    jQuery('body').on('click', '.upload_image_button', function( event ){
		event.preventDefault();
		$this = $(this);
		// If the media frame already exists, reopen it.
		if ( file_frame ) {
			// Set the post ID to what we want
			file_frame.uploader.uploader.param( 'post_id', set_to_post_id );
			// Open frame
			file_frame.open();
			return;
		} else {
			// Set the wp.media post id so the uploader grabs the ID we want when initialised
			wp.media.model.settings.post.id = set_to_post_id;
		}
		// Create the media frame.
		file_frame = wp.media.frames.file_frame = wp.media({
			title: 'Select a image to upload',
			button: {
				text: 'Use this image',
			},
			multiple: false	// Set to true to allow multiple files to be selected
		});
		// When an image is selected, run a callback.
		file_frame.on( 'select', function() {
			// We set multiple to false so only get one image from the uploader
			attachment = file_frame.state().get('selection').first().toJSON();
			// Do something with attachment.id and/or attachment.url here
			// $( '#image-preview' ).attr( 'src', attachment.url ).css( 'width', 'auto' );
			var imagePreview = $this.siblings('.image-preview');
			if (imagePreview.length) {
				$this.siblings('.image-preview').attr( 'src', attachment.url );
			} else {
				$this.before('<img class="image-preview" src="' + attachment.url + '" width="60" height="60">');
			}
			$this.siblings('.image_attachment_id').val( attachment.id );
			// Restore the main post ID
			wp.media.model.settings.post.id = wp_media_post_id;
		});
			// Finally, open the modal
			file_frame.open();
	});
	// Restore the main ID when the add media button is pressed
	jQuery( 'a.add_media' ).on( 'click', function() {
		wp.media.model.settings.post.id = wp_media_post_id;
	});
});

This has been changed so that any button with the class 'upload_image_button' will open the WordPress.  Once selected the image is inserted into the hidden input we added earlier.

Saving the image field in our product addon

Up until this point, we've added the inputs and now have a working WordPress media uploader, the image is not being saved with the addon.  In order to capture this input, we simply need to hook into the WooCommerce product addons save hook and grab the data from our image field.


/**
 * Save custom addon field
 */
function echo5_save_checkbox_image_field($data, $i) {
    $addon_option_image = $_POST['product_addon_option_image'][$i];
    for ( $ii = 0; $ii < sizeof( $data['options'] ); $ii++ ) {
        $image    = sanitize_text_field( stripslashes( $addon_option_image[ $ii ] ) );
        $data['options'][$ii]['image'] = $image;
    }
    return $data;
}
add_filter('woocommerce_product_addons_save_data', 'echo5_save_checkbox_image_field', 10, 2);

And now we can save images to our products!

Adding the images to the frontend

In our case, we only wanted to show these images for checkboxes.  We can easily override WooCommerce product addons templates by adding a "woocommerce-product-addons/addons" folder in our theme directory.

<?php // your-theme/woocommerce-product-addons/addons/checkbox.php ?>
<?php foreach ( $addon['options'] as $i => $option ) :

	$price = apply_filters( 'woocommerce_product_addons_option_price',
		$option['price'] > 0 ? '(' . wc_price( get_product_addon_price_for_display( $option['price'] ) ) . ')' : '',
		$option,
		$i,
		'checkbox'
	);

	$selected = isset( $_POST[ 'addon-' . sanitize_title( $addon['field-name'] ) ] ) ? $_POST[ 'addon-' . sanitize_title( $addon['field-name'] ) ] : array();
	if ( ! is_array( $selected ) ) {
		$selected = array( $selected );
	}

	$current_value = ( in_array( sanitize_title( $option['label'] ), $selected ) ) ? 1 : 0;

	// Image label
	$image = $image_class = null;
	if (is_numeric($option['image'])) {
		$image = wp_get_attachment_image($option['image'], 'option');
		$image_class = 'addon-has-image';
	}

	?>


	<p class="form-row form-row-wide addon-wrap-<?php echo sanitize_title( $addon['field-name'] ) . '-' . $i; ?> <?php echo $image_class; ?>">
		<input type="checkbox" id="<?php echo sanitize_title( $option['label'] ); ?>" class="addon addon-checkbox" name="addon-<?php echo sanitize_title( $addon['field-name'] ); ?>[]" data-raw-price="<?php echo esc_attr( $option['price'] ); ?>" data-price="<?php echo get_product_addon_price_for_display( $option['price'] ); ?>" value="<?php echo sanitize_title( $option['label'] ); ?>" <?php checked( $current_value, 1 ); ?> />
		<label for="<?php echo sanitize_title( $option['label'] ); ?>">
			<?php echo $image; ?>
			<span class="label-desc"><?php echo wptexturize( $option_label[0] . ' ' . $price ); ?></span>
		</label>
	</p>

<?php endforeach; ?>

If you just want to display an image next to your checkbox, you're done!  Otherwise if you want to add a bit more styling you can do so now.  We decided to go with clickable boxes using a pure CSS solution.

Bonus Round

Images or any other number of options can be added to your WooCommerce product addons.  You can also override any of the templates in your theme's "woocommerce-product-addons/addons" folder.  

But since we only wanted to display the option for the checkboxes, we wanted to hide the extra columns and fields we added in.  Unfortunately there aren't any CSS classes specific to the field type, but we can easily add a function to check when the field type is switched and hide the fields if checkboxes is not selected.

/**
 * Hide checkbox columns on type change
 */
 jQuery(document).ready(function($){
 	$('.product_addon_type').change(function() {
 		if ($(this).val() == 'checkbox') {
	 		$('.checkbox_column').show();
 		} else {
	 		$('.checkbox_column').hide();
 		}
 	});
 });

And now our image field and column is hidden for all addon types that aren't checkboxes.

If you have any questions feel free to comment below or if you need help with a project give us a shout!