Colores en el item de producto

En este tutorial vamos a ver cómo previsualizar los colores de un producto en el listado, sin tener que ingresar al detalle del producto:

HTML

1. Lo primero que vamos a hacer, es agregar un nuevo snipplet llamado item-colors.tpl dentro de la carpeta snipplets/grid con el siguiente código:

{% if product.variations %}
    {% set own_color_variants = 0 %}
    {% set custom_color_variants = 0 %}


    {% for variation in product.variations %}
        <div class="js-color-variant-available-{{ loop.index }} {% if variation.name in ['Color', 'Cor'] %}js-color-variant-active{% endif %}" data-value="variation_{{ loop.index }}" data-option="{{ loop.index0 }}" >
            {% if variation.name in ['Color', 'Cor'] %}
                {% if variation.options | length > 1 %}
                    <div class="item-colors">
                        <a href="{{ product_url_with_selected_variant }}" class="item-colors-bullet item-colors-bullet-text d-md-none w-auto px-2">{{ variation.options | length }} {{ 'colores' | translate }}</a>
                        <div class="d-none d-md-block">
                            {% for option in variation.options | take(5) if option.custom_data %}
                                <span title="{{ option.name }}" data-option="{{ option.id }}" class="js-color-variant item-colors-bullet {% if product.default_options[variation.id] == option.id %}selected{% endif %}" style="background: {{ option.custom_data }}"></span>
                            {% endfor %}


                            {% for option in variation.options %}
                                {% if option.custom_data %}
                                    {# Quantity of our colors #}
                                    {% set own_color_variants = own_color_variants + 1 %}
                                {% else %}
                                    {# Quantity of custom colors #}
                                    {% set custom_color_variants = custom_color_variants + 1 %}
                                {% endif %}
                            {% endfor %}


                            {% set more_color_variants = (own_color_variants - 5) + custom_color_variants %}


                            {% if own_color_variants and custom_color_variants %}
                                <a href="{{ product_url_with_selected_variant }}" class="item-colors-bullet w-auto" title="{{ 'Ver más colores' | translate }}">
                                    {% if own_color_variants > 5 %}
                                        +{{ more_color_variants }}
                                    {% else %}
                                        +{{ custom_color_variants }}
                                    {% endif %}
                                </a>
                            {% elseif own_color_variants > 5 %}
                                <a href="{{ product_url_with_selected_variant }}" class="item-colors-bullet w-auto" title="{{ 'Ver más colores' | translate }}">+{{ own_color_variants - 5 }}</a>
                            {% elseif custom_color_variants %}
                                <a href="{{ product_url_with_selected_variant }}" class="item-colors-bullet item-colors-bullet-text w-auto px-2" title="{{ 'Ver más colores' | translate }}">{{ custom_color_variants }} {{ 'colores' | translate }}</a>
                            {% endif %}
                        </div>
                    </div>
                {% endif %}
            {% endif %}
        </div>
    {% endfor %}
{% endif %}

2. Luego, vamos a buscar el snipplet item.tpl dentro de la carpeta snipplets/grid, puede ser que en tu diseño este snipplet se llame single_product.tpl y usamos el siguiente código:

{% set slide_item = slide_item | default(false) %}
{% set columns = settings.grid_columns %}
{% set has_color_variant = false %}
{% if settings.product_color_variants %}
    {% for variation in product.variations if variation.name in ['Color', 'Cor'] and variation.options | length > 1 %}
        {% set has_color_variant = true %}
    {% endfor %}
{% endif %}


<div class="js-item-product {% if slide_item %}swiper-slide{% else %}col{% if columns == 2 %}-6 col-md-3{% else %}-12 col-md-4{% endif %}{% endif %} item item-product{% if not product.display_price %} no-price{% endif %}" data-product-type="list" data-product-id="{{ product.id }}" data-store="product-item-{{ product.id }}">


    {% if settings.product_color_variants %}
        <div id="quick{{ product.id }}{% if slide_item and section_name %}-{{ section_name }}{% endif %}" class="js-product-container js-quickshop-container {% if product.variations %}js-quickshop-has-variants{% endif %}" data-variants="{{ product.variants_object | json_encode }}">
    {% endif %}


        {% set product_url_with_selected_variant = has_filters ?  ( product.url | add_param('variant', product.selected_or_first_available_variant.id)) : product.url  %}


        {% if has_color_variant %}


            {# Item image will be the first avaiable variant #}


            {% set item_img_spacing = product.featured_variant_image.dimensions['height'] / product.featured_variant_image.dimensions['width'] * 100 %}
            {% set item_img_srcset = product.featured_variant_image %}
            {% set item_img_alt = product.featured_variant_image.alt %}
        {% else %}


            {# Item image will be the first image regardless the variant #}


            {% set item_img_spacing = product.featured_image.dimensions['height'] / product.featured_image.dimensions['width'] * 100 %}
            {% set item_img_srcset = product.featured_image %}
            {% set item_img_alt = product.featured_image.alt %}
        {% endif %}


        <div class="item-image mb-2">
            <div style="padding-bottom: {{ item_img_spacing }}%;" class="p-relative" data-store="product-item-image-{{ product.id }}">
                <a href="{{ product_url_with_selected_variant }}" title="{{ product.name }}">
                    <img alt="{{ item_img_alt }}" data-sizes="auto" data-expand="-10" src="{{ 'images/empty-placeholder.png' | static_url }}" data-srcset="{{ item_img_srcset | product_image_url('small')}} 240w, {{ item_img_srcset | product_image_url('medium')}} 320w, {{ item_img_srcset | product_image_url('large')}} 480w" class="js-item-image lazyautosizes lazyload img-absolute img-absolute-centered fade-in" /> 
                    <div class="placeholder-fade"></div>
                </a>
                {% if settings.product_color_variants %}
                    {% include 'snipplets/labels.tpl' with {color: true} %}
                    {% include 'snipplets/grid/item-colors.tpl' %}
                {% else %}
                    {% include 'snipplets/labels.tpl' %}
                {% endif %}
            </div>
        </div>
        {% if settings.product_color_variants and product.variations %}


            {% for variation in product.variations if variation.name in ['Color', 'Cor'] and variation.options | length > 1 %}


                {# Hidden product form to update item image and variants #}
                
                <div class="js-item-variants hidden">
                    <form id="product_form" class="js-product-form" method="post" action="{{ store.cart_url }}">
                        {% if product.variations %}
                            {% include "snipplets/product/product-variants.tpl" with {quickshop: true} %}
                        {% endif %}
                        {% set state = store.is_catalog ? 'catalog' : (product.available ? product.display_price ? 'cart' : 'contact' : 'nostock') %}
                        {% set texts = {'cart': "Agregar al carrito", 'contact': "Consultar precio", 'nostock': "Sin stock", 'catalog': "Consultar"} %}


                        {# Add to cart CTA #}


                        <input type="submit" class="js-addtocart js-prod-submit-form {{ state }}" value="{{ texts[state] | translate }}" {% if state == 'nostock' %}disabled{% endif %} />
                    </form>
                </div>


            {% endfor %}
        {% endif %}
        <div class="item-description" data-store="product-item-info-{{ product.id }}">
            <a href="{{ product_url_with_selected_variant }}" title="{{ product.name }}" class="item-link">
                <div class="item-name mb-1" data-store="product-item-name-{{ product.id }}">{{ product.name }}</div>
                {% if product.display_price %}
                    <div class="item-price-container mb-1" data-store="product-item-price-{{ product.id }}">
                        <span class="js-compare-price-display price-compare" {% if not product.compare_at_price or not product.display_price %}style="display:none;"{% else %}style="display:inline-block;"{% endif %}>
                            {{ product.compare_at_price | money }}
                        </span>
                        <span class="js-price-display item-price">
                            {{ product.price | money }}
                        </span>


                    </div>
                {% endif %}
            </a>
        </div>
        {% include 'snipplets/payments/installments.tpl' %}


        {# Structured data to provide information for Google about the product content #}
        {% include 'snipplets/structured_data/item-structured-data.tpl' %}
    {% if settings.product_color_variants %}
        </div>
    {% endif %}
</div>

3. En el snipplet labels.tpl, puede ser que en tu diseño se encuentre directamente dentro del snipplet single_product.tpl, usamos el siguiente código:

{% if product.compare_at_price > product.price %}
{% set price_discount_percentage = ((product.compare_at_price) - (product.price)) * 100 / (product.compare_at_price) %}
{% endif %}


{% if color %}
  {% set show_labels = settings.product_color_variants %}
{% else %}
  {% set show_labels = not product.has_stock or product.free_shipping or product.compare_at_price or product.promotional_offer %}
{% endif %}


{% if show_labels %}
  <div class="labels">
    {% if not product.has_stock %}
      <div class="{% if product_detail %}js-stock-label {% endif %}label label-default">{{ "Sin stock" | translate }}</div>
    {% else %}
      {% if product_detail or color %}
        <div class="js-stock-label label label-default" {% if product.has_stock %}style="display:none;"{% endif %}>{{ "Sin stock" | translate }}</div>
      {% endif %}
      {% if product.compare_at_price or product.promotional_offer %}
        <div class="{% if not product.promotional_offer and product %}js-offer-label{% endif %} label label-primary" {% if (not product.compare_at_price and not product.promotional_offer) or not product.display_price %}style="display:none;"{% endif %}>
          {% if product.promotional_offer.script.is_percentage_off %}
            {{ product.promotional_offer.parameters.percent * 100 }}% OFF
          {% elseif product.promotional_offer.script.is_discount_for_quantity %}
            <div>{{ product.promotional_offer.selected_threshold.discount_decimal_percentage * 100 }}% OFF</div>
            <div class="label-small p-right-quarter p-left-quarter">{{ "Comprando {1} o más" | translate(product.promotional_offer.selected_threshold.quantity) }}</div>
          {% elseif product.promotional_offer %}
            {% if store.country == 'BR' %}
              {{ "Leve {1} Pague {2}" | translate(product.promotional_offer.script.quantity_to_take, product.promotional_offer.script.quantity_to_pay) }}
            {% else %}
              {{ "Promo" | translate }} {{ product.promotional_offer.script.type }} 
            {% endif %}
          {% else %}
            <span {% if product_detail or color %}class="js-offer-percentage"{% endif %}>{{ price_discount_percentage |round }}</span>% OFF
          {% endif %}
        </div>
      {% endif %}
      {% if product.free_shipping %}
        <div class="label label-secondary">{{ "Envío gratis" | translate }}</div>
      {% endif %}
    {% endif %}
  </div>
{% endif %}

4. En el snipplet installmets.tpl dentro de la carpeta snipplets/payments, para que se actualice la información de cuotas al cambiar un color, reemplazamos esta linea de código:

<div class="{% if product_detail %}js-max-installments-container js-max-installments text-center text-md-left{% else %}item-installments{% endif %}">

Por la siguiente:

<div class="js-max-installments-container js-max-installments {% if product_detail %}text-center text-md-left{% else %}item-installments{% endif %}">

5. Por úlitmo, en el snipplet product-variants.tpl dentro de la carpeta snipplets/product, usamos:

<div class="js-product-variants{% if quickshop %} js-product-quickshop-variants text-left{% endif %} form-row">
    {% for variation in product.variations %}
        <div class="js-product-variants-group {% if variation.name in ['Color', 'Cor'] %}js-color-variants-container{% endif %} {% if loop.length == 3 %} {% if quickshop %}col-4{% else %}col-12{% endif %} col-md-4 {% elseif loop.length == 2 %} col-6 {% else %} col {% if quickshop %}col-md-12{% else %}col-md-6{% endif %}{% endif %}" data-variation-id="{{ variation.id }}">
            {% embed "snipplets/forms/form-select.tpl" with{select_label: true, select_label_name: '' ~ variation.name ~ '', select_for: 'variation_' ~ loop.index , select_id: 'variation_' ~ loop.index, select_data_value: 'variation_' ~ loop.index, select_name: 'variation' ~ '[' ~ variation.id ~ ']', select_custom_class: 'js-variation-option js-refresh-installment-data'} %}
                {% block select_options %}
                    {% for option in variation.options %}
                        <option value="{{ option.id }}" {% if product.default_options[variation.id] == option.id %}selected="selected"{% endif %}>{{ option.name }}</option>
                    {% endfor %}
                {% endblock select_options%}
            {% endembed %}
        </div>
    {% endfor %}
</div>

CSS

Requisito:

Tener agregados en tu diseño las clases helpers. Podés seguir este este pequeño tutorial para hacerlo (simplemente es copiar y pegar algunas clases, no toma más de 1 minuto).

1.Agregar los estilos dentro del archivo static/style-critical.tpl 

Si en tu diseño usas una hoja de estilos para el CSS crítico, vamos a necesitar el siguiente código dentro de la misma.

.item-colors {
  position: absolute;
  bottom: 0;
  z-index: 9;
  width: 100%;
  padding: 5px 0;
}
.item-colors-bullet {
  display: inline-block;
  min-width: 18px;
  height: 18px;
  margin: 0 3px;
  font-size: 10px;
  text-transform: uppercase;
  line-height: 19px;
  vertical-align: top;
  border-radius: 18px;
  cursor: pointer;
  opacity: 0.8;
  -webkit-transition: all 0.4s ease;
  -ms-transition: all 0.4s ease;
  -moz-transition: all 0.4s ease;
  -o-transition: all 0.4s ease;
  transition: all 0.4s ease;
}
.item-colors-bullet:hover,
.item-colors-bullet.selected {
  opacity: 1;
}

2. Agregamos el siguiente SASS de colores en style-colors.scss.tpl (o la hoja de tu diseño que tenga los colores y tipografías de la tienda). Recordá que las variables de colores pueden variar respecto a tu diseño:

.item-colors {
  background: rgba($main-foreground, .6);
  &-bullet {
    color: $main-foreground;
  }
  &-bullet-text {
    color: $main-background;
  }
}

JS

⚠️ A partir del día 30 de enero de 2023, la librería jQuery será removida del código de nuestras tiendas, por lo tanto la función "$" no podrá ser utilizada.

1. El JavaScript necesitamos agregarlo en el archivo store.js.tpl (o donde tengas tus funciones de JS). El código que necesitamos es el siguiente:

{% if settings.product_color_variants %}

    {# Product color variations #}

    jQueryNuvem(document).on("click", ".js-color-variant", function(e) {
        e.preventDefault();
        $this = jQueryNuvem(this);


        var option_id = $this.data('option');
        $selected_option = $this.closest('.js-item-product').find('.js-variation-option option').filter(function(el) {
            return el.value == option_id;
        });
        
        $selected_option.prop('selected', true).trigger('change');
        var available_variant = jQueryNuvem(this).closest(".js-quickshop-container").data('variants');


        var available_variant_color = jQueryNuvem(this).closest('.js-color-variant-active').data('option');


        for (var variant in available_variant) {
            if (option_id == available_variant[variant]['option'+ available_variant_color ]) {

                if (available_variant[variant]['stock'] == null || available_variant[variant]['stock'] > 0 ) {

                    var otherOptions = getOtherOptionNumbers(available_variant_color);

                    var otherOption = available_variant[variant]['option' + otherOptions[0]];
                    var anotherOption = available_variant[variant]['option' + otherOptions[1]];


                    changeSelect(jQueryNuvem(this), otherOption, otherOptions[0]);
                    changeSelect(jQueryNuvem(this), anotherOption, otherOptions[1]);
                    break;

                }
            }
        }
        $this.siblings().removeClass("selected");
        $this.addClass("selected");
    });

    function getOtherOptionNumbers(selectedOption) {
        switch (selectedOption) {
            case 0:
                return [1, 2];
            case 1:
                return [0, 2];
            case 2:
                return [0, 1];
        }
    }

    function changeSelect(element, optionToSelect, optionIndex) {
        if (optionToSelect != null) {
            var selected_option_attribute = element.closest('.js-item-product').find('.js-color-variant-available-' + (optionIndex + 1)).data('value');
            var selected_option = element.closest('.js-item-product').find('#' + selected_option_attribute + " option").filter(function(el) {
                return el.value == optionToSelect;
            });

            selected_option.prop('selected', true).trigger('change');
        }
    }


    LS.registerOnChangeVariant(function(variant){
        {# Show product image on color change #}
        var current_image = jQueryNuvem('.js-item-product[data-product-id="'+variant.product_id+'"] .js-item-image');
        current_image.attr('srcset', variant.image_url);
    });

{% endif %}
    

2. También necesitamos agregar el JS que actualiza los precios, stock y etiquetas correspondientes, como la de stock y descuentos, al cambiar de variante.

Para eso, debemos buscar la siguiente función:

$(document).on("change", ".js-variation-option", function(e) {


    LS.changeVariant(changeVariant, '#single-product');

    (...)


});

Y reemplazarla por la siguiente:

jQueryNuvem(document).on("change", ".js-variation-option", function(e) {

    var $parent = jQueryNuvem(this).closest(".js-product-variants");
    var $variants_group = jQueryNuvem(this).closest(".js-product-variants-group");
    var $quickshop_parent_wrapper = jQueryNuvem(this).closest(".js-quickshop-container");

    {# If quickshop is used from modal, use quickshop-id from the item that opened it #}
    
    if($quickshop_parent_wrapper.hasClass("js-quickshop-modal")){
           var quick_id = jQueryNuvem(".js-quickshop-opened .js-quickshop-container").data("quickshopId");
    }else{
          var quick_id = $quickshop_parent_wrapper.data("quickshopId");
     }

    if($parent.hasClass("js-product-quickshop-variants")){

        var $quickshop_parent = jQueryNuvem(this).closest(".js-item-product");

        {# Target visible slider item if necessary #}
        
        if($quickshop_parent.hasClass("js-item-slide")){
            var $quickshop_variant_selector = '.js-swiper-slide-visible .js-quickshop-container[data-quickshop-id="'+quick_id+'"]';
        }else{
            var $quickshop_variant_selector = '.js-quickshop-container[data-quickshop-id="'+quick_id+'"]';
        }
        
        LS.changeVariant(changeVariant, $quickshop_variant_selector);

        {% if settings.product_color_variants %}
            {# Match selected color variant with selected quickshop variant #}


            if(($variants_group).hasClass("js-color-variants-container")){
                var selected_option_id = jQueryNuvem(this).find("option").filter((el) => el.selected).val();
                if($quickshop_parent.hasClass("js-item-slide")){
                    var $color_parent_to_update = jQueryNuvem('.js-swiper-slide-visible .js-quickshop-container[data-quickshop-id="'+quick_id+'"]');
                }else{
                    var $color_parent_to_update = jQueryNuvem('.js-quickshop-container[data-quickshop-id="'+quick_id+'"]');
                }
                $color_parent_to_update.find('.js-color-variant, .js-insta-variant').removeClass("selected");
                $color_parent_to_update.find('.js-color-variant[data-option="'+selected_option_id+'"], .js-insta-variant[data-option="'+selected_option_id+'"]').addClass("selected");
            }
        {% endif %} 
    } else {
        LS.changeVariant(changeVariant, '#single-product');
    }

    {# Offer and discount labels update #}

    var $this_product_container = jQueryNuvem(this).closest(".js-product-container");

    if($this_product_container.hasClass("js-quickshop-container")){
        var this_quickshop_id = $this_product_container.attr("data-quickshop-id");
        var $this_product_container = jQueryNuvem('.js-product-container[data-quickshop-id="'+this_quickshop_id+'"]');
    }
    var $this_compare_price = $this_product_container.find(".js-compare-price-display");
    var $this_price = $this_product_container.find(".js-price-display");
    var $installment_container = $this_product_container.find(".js-product-payments-container");
    var $installment_text = $this_product_container.find(".js-max-installments-container");
    var $this_add_to_cart = $this_product_container.find(".js-prod-submit-form");

    // Get the current product discount percentage value
    var current_percentage_value = $this_product_container.find(".js-offer-percentage");

    // Get the current product price and promotional price
    var compare_price_value = $this_compare_price.html();
    var price_value = $this_price.html();

    // Calculate new discount percentage based on difference between filtered old and new prices
    const percentageDifference = window.moneyDifferenceCalculator.percentageDifferenceFromString(compare_price_value, price_value);
    if(percentageDifference){
        $this_product_container.find(".js-offer-percentage").text(percentageDifference);
        $this_product_container.find(".js-offer-label").css("display" , "table");
    }

    if ($this_compare_price.css("display") == "none" || !percentageDifference) {
        $this_product_container.find(".js-offer-label").hide();
    }

    if ($this_add_to_cart.hasClass("nostock")) {
        $this_product_container.find(".js-stock-label").show();
    }
    else {
        $this_product_container.find(".js-stock-label").hide();
    }
    if ($this_price.css('display') == 'none'){
        $installment_container.hide();
        $installment_text.hide();
    }else{
        $installment_text.show();
    }
});

Configuraciones

En el archivo config/settings.txt vamos a agregar el checkbox que activa la funcionalidad dentro de la sección “Listado de productos”.

    title
        title = Variantes de color
    checkbox
        name = product_color_variants
        description = Mostrar variantes de color en listado de productos

Traducciones

En este paso agregamos los textos para las traducciones en el archivo config/translations.txt

es "colores"
pt "cores"
en "colors"
es_mx "colores"

es "Ver más colores"
pt "Ver mais cores"
en "See more colors"
es_mx "Ver más colores"

es "Variantes de color"
pt "Variações de cor"
en "Color variants"
es_mx "Variantes de color"

es "Mostrar variantes de color en listado de productos"
pt "Mostrar variações de cores na lista de produtos"
en "Show color variants in product list"
es_mx "Mostrar variantes de color en listado de productos"

Activación

Por último podés activar la funcionalidad desde el Administrador nube, en la sección de Personalizar tu diseño actual dentro de Listado de productos:

Recordá, que para que funcione el producto tiene que tener por lo menos 2 opciones de la variante “Color”.