Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Woocommerce how to ajaxify add to cart button for variable products using custom html radio buttons instead of dropdown menu - no plugin

I'm trying to put all of the pieces of this puzzle together. I've been reading all of the questions and answers on this subject for the past 3 days. So the general blueprint that i'm following is as follows:

  1. On single product page, first checking whether the type of product is "simple" or "variable".
  2. If product is "variable" then i'm using woocommerce_variable_add_to_cart(); function to output the proper html.
  3. Then trying to generate new and custom html (i.e "radio buttons") using the defualt html (i.e "dropdown menu") and woocommerce hooks.
  4. Then trying to give functionality to the new and custom html (i.e "radio buttons") using javascript.
  5. Then hiding the default dropdown menu using css.
  6. Then trying to send an ajax request to the wordpress.
  7. Then trying to process that ajax request on the backend and add the product to the cart.

Here is my code for each section:

  1. Checking whether the type of product is "variable" on the single product page:
global $post;

$product = wc_get_product($post->ID);

$product_type = $product->get_type();

  1. If the product type is "variable" then output the proper html:
if($product_type == 'variable'):
  woocommerce_variable_add_to_cart();
endif;
  1. Generating new and custom html (radio buttons) using php and woocommerce hooks:
add_filter('woocommerce_dropdown_variation_attribute_options_html', 'my_theme_variation_radio_buttons', 20, 2);

function my_theme_variation_radio_buttons($html, $args)
{
  $args = wp_parse_args(apply_filters('woocommerce_dropdown_variation_attribute_options_args', $args), array(
    'options'          => false,
    'attribute'        => false,
    'product'          => false,
    'selected'         => false,
    'name'             => '',
    'id'               => '',
    'class'            => '',
    'show_option_none' => __('Choose an option', 'woocommerce'),
  ));

  if (false === $args['selected'] && $args['attribute'] && $args['product'] instanceof WC_Product) {
    $selected_key     = 'attribute_' . sanitize_title($args['attribute']);
    $args['selected'] = isset($_REQUEST[$selected_key]) ? wc_clean(wp_unslash($_REQUEST[$selected_key])) : $args['product']->get_variation_default_attribute($args['attribute']);
  }

  $options               = $args['options'];
  $product               = $args['product'];
  $attribute             = $args['attribute'];
  $name                  = $args['name'] ? $args['name'] : 'attribute_' . sanitize_title($attribute);
  $id                    = $args['id'] ? $args['id'] : sanitize_title($attribute);
  $class                 = $args['class'];
  $show_option_none      = (bool)$args['show_option_none'];
  $show_option_none_text = $args['show_option_none'] ? $args['show_option_none'] : __('Choose an option', 'woocommerce');

  if (empty($options) && !empty($product) && !empty($attribute)) {
    $attributes = $product->get_variation_attributes();
    $options    = $attributes[$attribute];
  }

  $radios = '<div class="variation-radios">';

  if (!empty($options)) {
    if ($product && taxonomy_exists($attribute)) {
      $terms = wc_get_product_terms($product->get_id(), $attribute, array(
        'fields' => 'all',
      ));

      foreach ($terms as $term) {
        if (in_array($term->slug, $options, true)) {
          $id = $name . '-' . $term->slug;
          $radios .= '<input type="radio" data-checked="no" id="' . esc_attr($id) . '" name="' . esc_attr($name) . '" value="' . esc_attr($term->slug) . '" ' . checked(sanitize_title($args['selected']), $term->slug, false) . '><label for="' . esc_attr($id) . '">' . esc_html(apply_filters('woocommerce_variation_option_name', $term->name)) . '</label>';
        }
      }
    } else {
      foreach ($options as $option) {
        $id = $name . '-' . $option;
        $checked    = sanitize_title($args['selected']) === $args['selected'] ? checked($args['selected'], sanitize_title($option), false) : checked($args['selected'], $option, false);
        $radios    .= '<input type="radio" id="' . esc_attr($id) . '" name="' . esc_attr($name) . '" value="' . esc_attr($option) . '" id="' . sanitize_title($option) . '" ' . $checked . '><label for="' . esc_attr($id) . '">' . esc_html(apply_filters('woocommerce_variation_option_name', $option)) . '</label>';
      }
    }
  }

  $radios .= '</div>';

  return $html . $radios;
}


add_filter('woocommerce_variation_is_active', 'my_theme_variation_check', 10, 2);

function my_theme_variation_check($active, $variation)
{
  if (!$variation->is_in_stock() && !$variation->backorders_allowed()) {
    return false;
  }
  return $active;
}
  1. Giving functionality to "radio buttons" using javascript:
jQuery(document).ready($ => {
  $(document).on('change', '.variation-radio input', function () {
    $('.variation-radio input:checked').each(function (index, element) {

      var radioElement = $(element);
      var radioName = radioElement.attr('name');
      var radioValue = radioElement.attr('value');

      $('select[name="' + radioName + '"]').val(radioValue).trigger('change');
    });
  });
  $(document).on('woocommerce_update_variation_values', function () {
    $('.variation-radio input').each(function (index, element) {
      var radioElement = $(element);
      var radioName = radioElement.attr('name');
      var radioValue = radioElement.attr('value');
      radioElement.removeAttr('disabled');
      if ($('select[name="' + radioName + '"] option[value="' + radioValue + '"]').is(':disabled')) {
        radioElement.prop('disabled', true);
      }
    });
  });
  $("a.reset_variations").click(function () {
    $('input:radio[name="attribute_size"]').prop('checked', false); $(this).css('display', 'none');
  });
})
  1. Hiding the default dropdown menu using css:
table.variations select{
  display: none;
} 
  1. Sending an ajax request to the wordpress:
jQuery(document).ready($ => {
  $("button.single_add_to_cart_button").on('click', function (e) {
    e.preventDefault();
    var myBtn = $(this),
      $form = myBtn.closest('form.variations_form'),
      product_qty = $form.find('input[name=quantity]').val() || 1,
      product_id = $form.find('input[name=product_id]').val(),
      variation_id = $form.find('input[name=variation_id]').val() || 0,
      variation = {},
      keys = [],
      values = [];
    // Looping through the attributes names and save them as the keys array
    $('table tr td.label label').each(function (index, element) {
      let radioElement = $(element);
      keys[index] = radioElement.text();
    });
    // Looping through the attributes values and save them as the values array
    $('.variation-radios input:checked').each(function (index, element) {
      let radioElement = $(element);
      values[index] = radioElement.val();
    });
    // Looping through the variation object and save keys and values in that object
    $.each(keys, function (index, element) {
      variation[element] = values[index]
    })
    console.log(variation);

    var data = {
      action: 'woocommerce_add_variation_to_cart',
      product_id: product_id,
      quantity: product_qty,
      variation_id: variation_id,
      var: variation
    };

    $(document.body).trigger('adding_to_cart', [myBtn, data]);

    $.ajax({
      type: 'post',
      url: wc_add_to_cart_params.ajax_url,
      data: data,
      beforeSend: function (response) {
        myBtn.removeClass('added').addClass('loading');
      },
      complete: function (response) {
        myBtn.addClass('added').removeClass('loading');
      },
      success: function (response) {
        console.log(response);
        if (response.error && response.product_url) {
          window.location = response.product_url;
          return;
        } else {
          $(document.body).trigger('added_to_cart', [response.fragments, response.cart_hash, myBtn]);
        }
      },
    });

    return false;
  });
})
  1. Processing the ajax request on the backend and add the product to the cart:
add_action('wp_ajax_nopriv_woocommerce_add_variation_to_cart', 'my_theme_testing_add_to_cart_variable');

add_action('wp_ajax_woocommerce_add_variation_to_cart', 'my_theme_testing_add_to_cart_variable');

function my_theme_testing_add_to_cart_variable()
{
  if (isset($_POST['product_id']) && $_POST['product_id'] > 0) {
    $product_id = apply_filters('woocommerce_add_to_cart_product_id', absint($_POST['product_id']));

    $quantity = empty($_POST['quantity']) ? 1 : wc_stock_amount($_POST['quantity']);

    $variation_id = isset($_POST['variation_id']) ? absint($_POST['variation_id']) : '';

    $attributes   = explode(',', $_POST['var']);

    $variation    = array();

    foreach ($attributes as $values) {

      $values = explode(':', $values);

      $variation['attributes_' . $values[0]] = $values[1];

    }

    $passed_validation = apply_filters('woocommerce_add_to_cart_validation', true, $product_id, $quantity);

    if ($passed_validation && WC()->cart->add_to_cart($product_id, $quantity, $variation_id, $variation)) {

      do_action('woocommerce_ajax_added_to_cart', $product_id);

      if (get_option('woocommerce_cart_redirect_after_add') == 'yes') {
        wc_add_to_cart_message($product_id);
      }

      WC_AJAX::get_refreshed_fragments();

    } else {

      $data = array(
        'error' => true,
        'product_url' => apply_filters('woocommerce_cart_redirect_after_error', get_permalink($product_id), $product_id)
      );

      wp_send_json($data);
    }

    die();
  }
}

PROBLEM

Everything works until step 7 with no error but when i run the whole thing, the single product page refreshes and variable product doesn't get added to the cart. And the wordpress error says "{your attribute field} is a required field"!

I think the bug could be somewhere in the ajax call when i'm trying to send variation object to the backend.

Although, i get the data absolutely fine on the backend but it doesn't add it to the cart.


Things that i've tried to debug it

I've tried to send the data in the ajax call as an array but didn't work either. I also tried to explode the variation data using both = and : but none has worked!

Hooof! It's been a long week so far :\ full of debugging, headaches and frustrations. Now when i try to run the whole shebang, i can't get it to work and i've been reading all of the Qs and As on SO but can't find the bug! I think i've been overthinking it for a couple of days and also there are lots of pieces to it.

So i think i need some extra pairs of eyes to hopefully detect the bug.

Thank you, i appreciate any insight(s)!


In addition, huge shout out to these fellows whom i've learned a lot from:

  • LoicTheAztec for this great answer and this and this and many more.
  • cfx for this great answer
  • Anthony Grist for this answer
  • helgatheviking for this answer
  • AND OTHERS

EDIT

The code works fine, bug wasn't in the code, it was in the data that i was provided with. I'll leave the code here just in case somebody needs it in the future.

like image 910
Ruvee Avatar asked Nov 06 '22 01:11

Ruvee


1 Answers

Short ansewer

The code works fine, i'll leave it here just in case somebody needs it in the future. Bug wasn't in the code, it was in the data that i was provided with.


Detailed Explanation

The code works fine. The data that i was provided with was manipulated for some reasons by my client so that each variable product wasn't a real variable product but at the same time the labels were typed in as variable products (yea i know it's confusing and not a standard practice), that's why whenever i tried to add them to cart, it would give the error saying {your-attribute} is a required field.

So we deleted each product data and add it back as a real and true variable product, then the code worked without us changing anything in it.


Take-away
So remember, whenever developing your app, there are always two sides to this coin! One side is your code and the other side is the data you're working on.

So, always always always, make sure the data you're working with is the way/format it's supposed to be. Also if you can't find any bug in your code, remember to check/debug the other side which is the data.
If you don't check the data first or at any debugging stage, then it'll be hard to track down the issue down the road!

This bug created a long delay in the project (about 2 weeks until we tracked down the bug in the data). So make sure to always check the both sides of the coin:

  • First, the data you're working with
  • Second, the code you wrote.
like image 127
Ruvee Avatar answered Nov 15 '22 11:11

Ruvee