3

I am trying to redesign the woocommerce single product page where instead of showign a dropdown for variation options, I would like them to be placed in separate divs and be displayed in flex boxes side by side. I dont understand what part is making the data take the 'dropdown list' form?

An illustration of what I am trying to achieve:

enter image description here




This is the html part (taken from wc-template-functions.php inside the includes folder) where instead of the select and option tags i need to insert each variation inside a separate div. I really need this. Any help?

This is my attempt inside woocommerce -> includes -> wc-template-functions.php, inside the function wc_dropdown_variation_attribute_options. It shows the variations but they arent clickable and cant pull the data to select and forward on add to cart / buy now event:

function wc_dropdown_variation_attribute_options( $args = array() ) {
        $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'            => '',
            )
        );

        // Get selected value.
        if ( false === $args['selected'] && $args['attribute'] && $args['product'] instanceof WC_Product ) {
            $selected_key = 'attribute_' . sanitize_title( $args['attribute'] );
            // phpcs:disable WordPress.Security.NonceVerification.Recommended
            $args['selected'] = isset( $_REQUEST[ $selected_key ] ) ? wc_clean( wp_unslash( $_REQUEST[ $selected_key ] ) ) : $args['product']->get_variation_default_attribute( $args['attribute'] );
            // phpcs:enable WordPress.Security.NonceVerification.Recommended
        }

        $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'];


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

    

        if ( ! empty( $options ) ) {
            if ( $product && taxonomy_exists( $attribute ) ) {
                // Get terms if this is a taxonomy - ordered. We need the names too.
                $terms = wc_get_product_terms(
                    $product->get_id(),
                    $attribute,
                    array(
                        'fields' => 'all',
                    )
                );

                foreach ( $terms as $term ) {
                    if ( in_array( $term->slug, $options, true ) ) {
                        $html .= '<div value="' . esc_attr( $term->slug ) . '" ' . selected( sanitize_title( $args['selected'] ), $term->slug, false ) . '>' . esc_html( apply_filters( 'woocommerce_variation_option_name', $term->name, $term, $attribute, $product ) ) . '</div>';
                    }
                }
            } else {
                foreach ( $options as $option ) {
                    // This handles < 2.4.0 bw compatibility where text attributes were not sanitized.
                    $selected = sanitize_title( $args['selected'] ) === $args['selected'] ? selected( $args['selected'], sanitize_title( $option ), false ) : selected( $args['selected'], $option, false );
                    $html    .= '<div value="' . esc_attr( $option ) . '" ' . $selected . '>' . esc_html( apply_filters( 'woocommerce_variation_option_name', $option, null, $attribute, $product ) ) . '</div>';
                }
            }
        }

        $html .= '</div>';

        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
        echo apply_filters( 'woocommerce_dropdown_variation_attribute_options_html', $html, $args );
    }
Vincenzo Di Gaetano
  • 3,892
  • 3
  • 13
  • 32
Raging Vids
  • 121
  • 1
  • 8
  • 3
    Use the [`woocommerce_dropdown_variation_attribute_options_html`](https://woocommerce.github.io/code-reference/files/woocommerce-includes-wc-template-functions.html#source-view.3056) hook to replace select options with divs. – Vincenzo Di Gaetano Apr 18 '21 at 20:51

1 Answers1

7

I thought about not using any hooks to convert attribute select options to separate divs. I will rather use jQuery.

The reason is simple. WooCommerce uses several scripts based on the select options elements, and replacing the options with divs could generate unexpected conflicts.

The idea is this: create a copy of the select option product attributes (hidden from users) and replace all select options in div via the jQuery unwrap() function. I was inspired by this answer: Is it possible to convert a select menu to buttons?

All select options of each product attribute will be converted into div (even more than one).

UPDATED

The previous code did not allow to automatically hide the attribute options (div) that do not correspond to any product variation. The code has now been updated to allow the user to:

  • select and deselect the div by clicking on the same element
  • automatically hide attributes that do not correspond to any product variation (based on one or more selected divs)

The code works with any number of attributes on the product page.

JQUERY

add_action( 'wp_footer', 'converts_product_attributes_from_select_options_to_div' );
function converts_product_attributes_from_select_options_to_div() {

    ?>
        <script type="text/javascript">

            jQuery(function($){

                // clones select options for each product attribute
                var clone = $(".single-product div.product table.variations select").clone(true,true);

                // adds a "data-parent-id" attribute to each select option
                $(".single-product div.product table.variations select option").each(function(){
                    $(this).attr('data-parent-id',$(this).parent().attr('id'));
                });

                // converts select options to div
                $(".single-product div.product table.variations select option").unwrap().each(function(){
                    if ( $(this).val() == '' ) {
                        $(this).remove();
                        return true;
                    }
                    var option = $('<div class="custom_option is-visible" data-parent-id="'+$(this).data('parent-id')+'" data-value="'+$(this).val()+'">'+$(this).text()+'</div>');
                    $(this).replaceWith(option);
                });
                
                // reinsert the clone of the select options of the attributes in the page that were removed by "unwrap()"
                $(clone).insertBefore('.single-product div.product table.variations .reset_variations').hide();

                // when a user clicks on a div it adds the "selected" attribute to the respective select option
                $(document).on('click', '.custom_option', function(){
                    var parentID = $(this).data('parent-id');
                    if ( $(this).hasClass('on') ) {
                        $(this).removeClass('on');
                        $(".single-product div.product table.variations select#"+parentID).val('').trigger("change");
                    } else {
                        $('.custom_option[data-parent-id='+parentID+']').removeClass('on');
                        $(this).addClass('on');
                        $(".single-product div.product table.variations select#"+parentID).val($(this).data("value")).trigger("change");
                    }
                    
                });

                // if a select option is already selected, it adds the "on" attribute to the respective div
                $(".single-product div.product table.variations select").each(function(){
                    if ( $(this).find("option:selected").val() ) {
                        var id = $(this).attr('id');
                        $('.custom_option[data-parent-id='+id+']').removeClass('on');
                        var value = $(this).find("option:selected").val();
                        $('.custom_option[data-parent-id='+id+'][data-value='+value+']').addClass('on');
                    }
                });

                // when the select options change based on the ones selected, it shows or hides the respective divs
                $('body').on('check_variations', function(){
                    $('div.custom_option').removeClass('is-visible');
                    $('.single-product div.product table.variations select').each(function(){
                        var attrID = $(this).attr("id");
                        $(this).find('option').each(function(){
                            if ( $(this).val() == '' ) {
                                return;
                            }
                            $('div[data-parent-id="'+attrID+'"][data-value="'+$(this).val()+'"]').addClass('is-visible');
                        });
                    });
                });

            });

        </script>
    <?php

}

The code has been tested and works. Add it to your active theme's functions.php.

CSS

/* adds style to divs */
/* by default all divs are hidden */
div.custom_option {
    display: none;
    border: 2px solid #ccc;
    margin-right: 5px;
    padding: 2px 5px;
    cursor: pointer;
}

/* show only divs with class "is-visible" */
div.custom_option.is-visible {
    display:inline-block;
}

/* adds the style to the selected div */
div.custom_option.on {
    background-color: #777;
    color: white;
}

Add the CSS in the style sheet of your active theme.

BEFORE

enter image description here


AFTER

enter image description here


USAGE

enter image description here

Vincenzo Di Gaetano
  • 3,892
  • 3
  • 13
  • 32
  • 1
    Thanks for this! Is it possible to add colors to the terms like based on an if argument for when the name is Green? –  May 07 '21 at 11:29
  • 1
    Sure. You can add your own custom logic before the line: `var option = ...`. Also you can already use the attribute slug as a selector. For example: `div[data-value=green]`. – Vincenzo Di Gaetano May 07 '21 at 20:56
  • How could I add the selected option as text next to the label name? So for color when selected next the Color label, you can have it show Color: Green or Color: Yellow based upon the selection? – Greenhoe Apr 05 '22 at 13:45
  • @Greenhoe inside the `$(document).on('click', '.custom_option', function(){` block add these lines: `if ( $(this).parent().siblings('.label').find('.value-selected').length > 0 ) { $(this).parent().siblings('.label').find('.value-selected').text($(this).text()); } else { $(this).parent().siblings('.label').append(''+$(this).text()+''); }`. – Vincenzo Di Gaetano Apr 05 '22 at 20:22
  • Thank you, that works but it only ads the label once a variation is clicked on, how can I make it default load with the default variation? For example if the color green is set as default for the product, on the page load it shows nothing then if I click Green it will add the Green next to the label instead of lading it by default. – Greenhoe Apr 06 '22 at 00:00
  • Hi Vincenzo, I found a bug in the original code. This works great for saved attributes that are used globally across the site. But when you use a custom product attribute for a singular product and you don't have it saved as a global attribute a few of the key features don't work. The first one is the default attribute selected value isn't auto-selected, and then the hide feature where if an attribute should be hidden because it's not an option doesn't work either. Again this is only for "Custom product attributes" that are not saved as global attributes. – Greenhoe Sep 28 '22 at 15:40