Productos relacionados: Alternativos y complementarios

En este tutorial, vamos a agregar 2 espacios para productos relacionados (alternativos y complementarios) que se muestran al pie de la página de detalles del producto.

Los tipos de productos relacionados que se mostrarán son:

  • Productos de la misma categoría que el producto principal (este es el comportamiento default)
  • En caso que se relacionen productos manualmente desde el formulario de producto en el administrador, entonces pasaran a mostrarse:
    • Productos alternativos: Deberían ser los productos similares al producto principal
    • Productos complementarios: Deberían ser los productos que complementan al producto principal

HTML

1. Vamos a crear un nuevo snipplet con el nombre product-related.tpl dentro de la carpeta snipplets/product. El código para este es el siguiente:

{# Default related products visibility conditions #}

{% set related_products = [] %}
{% set related_products_ids_from_app = product.metafields.related_products.related_products_ids %}
{% set has_related_products_from_app = related_products_ids_from_app | get_products | length > 0 %}
{% if has_related_products_from_app %}
    {% set related_products = related_products_ids_from_app | get_products %}
{% endif %}
{% if related_products is empty %}
    {% set max_related_products_length = 8 %}
    {% set max_related_products_achieved = false %}
    {% set related_products_without_stock = [] %}
    {% set max_related_products_without_achieved = false %}

    {% if related_tag %}
        {% set products_from_category = related_products_from_controller %}
    {% else %}
        {% set products_from_category = category.products | shuffle %}
    {% endif %}

    {% for product_from_category in products_from_category if not max_related_products_achieved and product_from_category.id != product.id %}
        {%  if product_from_category.stock is null or product_from_category.stock > 0 %}
            {% set related_products = related_products | merge([product_from_category]) %}
        {% elseif (related_products_without_stock | length < max_related_products_length) %}
            {% set related_products_without_stock = related_products_without_stock | merge([product_from_category]) %}
        {% endif %}
        {%  if (related_products | length == max_related_products_length) %}
            {% set max_related_products_achieved = true %}
        {% endif %}
    {% endfor %}
    {% if (related_products | length < max_related_products_length) %}
        {% set number_of_related_products_for_refill = max_related_products_length - (related_products | length) %}
        {% set related_products_for_refill = related_products_without_stock | take(number_of_related_products_for_refill) %}


        {% set related_products = related_products | merge(related_products_for_refill)  %}
    {% endif %}
{% endif %}

{% set complementary_products = complementary_product_list | length > 0 %}

{# Show alternative products when there are default category alternatives with no complementaries or manually selected alternatives #}
{% set alternative_products = related_products | length > 0 and not (complementary_products and source_alternative == 'default') %}

{# Set related products classes #}

{% set section_class = 'section-products-related my-3' %}
{% set container_class = 'container' %}
{% set title_class = 'h3 text-center' %}
{% set products_container_class = 'position-relative swiper-container-horizontal' %}
{% set slider_container_class = 'swiper-container' %}
{% set swiper_wrapper_class = 'swiper-wrapper' %}
{% set slider_control_pagination_class = 'swiper-pagination' %}
{% set slider_control_class = 'icon-inline icon-w-8 icon-2x svg-icon-text' %}
{% set slider_control_prev_class = 'swiper-button-prev' %}
{% set slider_control_next_class = 'swiper-button-next' %}
{% set control_prev = include ('snipplets/svg/chevron-left.tpl', {svg_custom_class: slider_control_class}) %}
{% set control_next = include ('snipplets/svg/chevron-right.tpl', {svg_custom_class: slider_control_class}) %}

{# Alternative products #}

{% set alternative_data_component = source_alternative == 'default' ? 'related-products' : 'alternative-products' %}

{% if alternative_products %}
    {{ component(
        'products-section',{
            title: settings.products_related_title,
            id: 'related-products',
            data_component: alternative_data_component,
            products_amount: related_products | length,
            products_array: related_products,
            product_template_path: 'snipplets/grid/item.tpl',
            product_template_params: {'slide_item': true},
            slider_controls_position: 'bottom',
            slider_pagination: true,
            svg_sprites: false,
            section_classes: {
                section: 'js-related-products ' ~ section_class,
                container: container_class,
                title: title_class,
                products_container: products_container_class,
                slider_container: 'js-swiper-related ' ~ slider_container_class,
                slider_wrapper: swiper_wrapper_class,
                slider_control_pagination: 'js-swiper-related-pagination ' ~ slider_control_pagination_class,
                slider_control_prev_container: 'js-swiper-related-prev ' ~ slider_control_prev_class,
                slider_control_prev: 'icon-flip-horizontal',
                slider_control_next_container: 'js-swiper-related-next ' ~ slider_control_next_class,
            },
            custom_control_prev: control_prev,
            custom_control_next: control_next,
        }) 
    }}
{% endif %}

{# Complementary products #}

{% set complementary_section_id = 'complementary-products' %}

{% if complementary_products %}
    {{ component(
        'products-section',{
            title: 'Para comprar con este producto' | translate,
            id: complementary_section_id,
            data_component: complementary_section_id,
            products_amount: complementary_product_list | length,
            products_array: complementary_product_list,
            product_template_path: 'snipplets/grid/item.tpl',
            product_template_params: {'slide_item': true},
            slider_controls_position: 'bottom',
            slider_pagination: true,
            svg_sprites: false,
            section_classes: {
                section: 'js-complementary-products ' ~ section_class,
                container: container_class,
                title: title_class,
                products_container: products_container_class,
                slider_container: 'js-swiper-complementary ' ~ slider_container_class,
                slider_wrapper: swiper_wrapper_class,
                slider_control_pagination: 'js-swiper-complementary-pagination ' ~ slider_control_pagination_class,
                slider_control_prev_container: 'js-swiper-complementary-prev ' ~ slider_control_prev_class,
                slider_control_prev: 'icon-flip-horizontal',
                slider_control_next_container: 'js-swiper-complementary-next ' ~ slider_control_next_class,
            },
            custom_control_prev: control_prev,
            custom_control_next: control_next,
        }) 
    }}
{% endif %}

Podemos observar que estamos incluyendo el snipplet item.tpl de la carpeta snipplets/grid (basado en el theme Base), puede que en tu caso necesites incluir el snipplet single_product.tpl. Lo importante es usar el mismo snipplet que usamos para el item en los listados de productos, como los que están en los templates category.tpl o search.tpl.

Por otro lado es importante mencionar que estamos usando el componente privado de "products-section". Para más información las opciones de este componente te recomendamos este artículo.

2. Dentro de item.tpl o single_product.tpl vamos a agregar en el div donde tenemos las clases para las columnas de Bootstrap el condicional {% if slide_item %}js-item-slide swiper-slide{% endif %}. Debajo está un ejemplo de como aplicarlo:

<div class="{% if slide_item %}js-item-slide swiper-slide{% else %}col-12 col-sm-4{% endif %} item item-product">
... 
</div> 

3. Incluimos el snipplet de productos relacionados en el template product.tpl debajo de todo, de la siguiente forma:

{% include 'snipplets/product/product-related.tpl' %}

4. Por último para la parte de HTML, necesitamos agregar una carpeta SVG dentro de la carpeta snipplets. Acá vamos sumar los SVGs que usamos para los iconos en el carrito.

chevron-left.tpl

<svg class="{{ svg_custom_class }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 512"><path d="M231.293 473.899l19.799-19.799c4.686-4.686 4.686-12.284 0-16.971L70.393 256 251.092 74.87c4.686-4.686 4.686-12.284 0-16.971L231.293 38.1c-4.686-4.686-12.284-4.686-16.971 0L4.908 247.515c-4.686 4.686-4.686 12.284 0 16.971L214.322 473.9c4.687 4.686 12.285 4.686 16.971-.001z"/></svg>

chevron-right.tpl

<svg class="{{ svg_custom_class }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 512"><path d="M24.707 38.101L4.908 57.899c-4.686 4.686-4.686 12.284 0 16.971L185.607 256 4.908 437.13c-4.686 4.686-4.686 12.284 0 16.971L24.707 473.9c4.686 4.686 12.284 4.686 16.971 0l209.414-209.414c4.686-4.686 4.686-12.284 0-16.971L41.678 38.101c-4.687-4.687-12.285-4.687-16.971 0z"/></svg>

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).

Como en este ejemplo usamos un slider con Swiper, necesitamos agregar el plugin. Para ver cómo hacerlo podés leer este corto artículo y luego continuar con este tutorial.

Si preferís mostrar los productos en una grilla clásica sin slider o ya tenés Swiper incluido, podés evitar este paso.

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.

Necesitamos aplicar las funciones en el archivo store.js.tpl (o donde tengas tus funciones de JS) con el siguiente código:

{% if template == 'product' %}

    {# /* // Product Related */ #}
        
        // Set loop for related products products sliders

        {% set columns = settings.grid_columns %}
        const desktopColumns = {% if columns == 1 %}3{% else %}4{% endif %};

        function calculateRelatedLoopVal(sectionSelector) {                
            let productsAmount = jQueryNuvem(sectionSelector).attr("data-related-amount");
            let loopVal = false;
            const applyLoop = (window.innerWidth < 768 && productsAmount > {{ columns }}) || (window.innerWidth > 768 && productsAmount > desktopColumns);
            
            if (applyLoop) {
                loopVal = true;
            }

            return loopVal;
        }

        let alternativeLoopVal = calculateRelatedLoopVal(".js-related-products");
        let complementaryLoopVal = calculateRelatedLoopVal(".js-complementary-products");

        {# Alternative products #}

        createSwiper('.js-swiper-related', {
            lazy: true,
            watchOverflow: true,
            loop: alternativeLoopVal,
            centerInsufficientSlides: true,
            spaceBetween: 30,
            slidesPerView: {{ columns }},
            pagination: {
                el: '.js-swiper-related-pagination',
                clickable: true,
            },
            navigation: {
                nextEl: '.js-swiper-related-next',
                prevEl: '.js-swiper-related-prev',
            },
            breakpoints: {
                767: {
                    slidesPerView: desktopColumns,
                }
            }
        });

        {# Complementary products #}

        createSwiper('.js-swiper-complementary', {
            lazy: true,
            watchOverflow: true,
            loop: complementaryLoopVal,
            centerInsufficientSlides: true,
            spaceBetween: 30,
            slidesPerView: {{ columns }},
            pagination: {
                el: '.js-swiper-complementary-pagination',
                clickable: true,
            },
            navigation: {
                nextEl: '.js-swiper-complementary-next',
                prevEl: '.js-swiper-complementary-prev',
            },
            breakpoints: {
                767: {
                    slidesPerView: desktopColumns,
                }
            }
        });

{% endif %}

Configuraciones

En el archivo config/settings.txt vamos a agregar la opción para cambiar el título de la sección de alternativos (podes hacerlo para los complementarios también si lo necesitas) dentro de la sección “Detalle de producto”.

title
    title = Productos relacionados
i18n_input
    description = Título para los productos alternativos
    name = products_related_title

Traducciones

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

es "Productos relacionados"
pt "Produtos relacionados"
en "Related products"
es_mx "Productos relacionados"

es "Título para los productos alternativos"
pt "Título para os produtos alternativos"
es_mx "Título para los productos alternativos"

Dentro de config/defaults.txt vamos a agregar los textos predeterminados del mensaje.

products_related_title_es = Productos similares
products_related_title_en = Similar products
products_related_title_pt = Produtos similares

Activación

Una vez aplicados todos los cambios en tu código, necesitamos que te contactes con Tiendanube para terminar la activación y poder relacionar productos desde el administrador a socios@tiendanube.com

Listo, una vez que se relacionen productos desde el formulario de producto en el administrador, se verán los relacionados creados manualmente, de lo contrario se mostrarán los default mencionados al comienzo de este artículo. ¡Excelente!