Filtros y orden de productos

En este artículo vamos a agregar dos funcionalidades:

Filtros

Incluye la posibilidad de agregar y remover filtros en la página de categorías.

También agrega en la versión desktop una barra con las categorías a la izquierda de la grilla 


Orden de productos

Deja al usuario ordenar los productos con el siguiente criterio:

  • Precio: mayor a menor y menor a mayor.
  • Alfabético: A -Z y Z - A.
  • Tiempo: más nuevo al más viejo y más viejo al más nuevo.
  • Más vendidos.

HTML

Lo primero que vamos a hacer es crear los tpls necesarios para las funcionalidades.

1. Agregamos la carpeta con el nombre grid dentro de la carpeta snipplets 

2. Dentro creamos tres snipplets:

categories.tpl  

Representa a las categorías mostradas en una barra a la izquierda de la grilla, solo desde su versión desktop:

<div class="mb-4 pb-1">
    <div class="js-append-filters pb-5" style="display: none;">
        <div class="d-none d-sm-block" >
            <h5 class="mb-2">{{ 'Filtro aplicado' | translate }}</h5>
        </div>
    </div>


    {% if parent_category and parent_category.id!=0 %}
        <a href="{{ parent_category.url }}" title="{{ parent_category.name }}" class="category-back d-block{% if filter_categories %} mb-4{% endif %}">{% include "snipplets/svg/chevron-left.tpl" with {svg_custom_class: "icon-inline mr-2 svg-icon-text"} %}{{ parent_category.name }}</a>
    {% endif %}


    {% if filter_categories %}
        <div class="d-none d-md-block">
            <h3 class="title-section mb-4">{{ category.id!=0 ? category.name :("Categorías" | translate) }}</h3>
            <ul class="list-unstyled"> 
                {% for category in filter_categories %}
                    <li data-item="{{ loop.index }}" class="js-category-sidebar-item mb-2"><a href="{{ category.url }}" title="{{ category.name }}">{{ category.name }}</a></li>
                {% endfor %}
            </ul>
        </div>
    {% endif %}
</div>

filters.tpl

Como su nombre indica, son los filtros. Estos están divididos en 4 tipos:

FiltroDescripción
filter_colorsSon los colores principales como rojo, verde, amarillo, etc
filter_more_colorsSon los colores más personalizados, por ejemplo “Salmón”
filter_sizesSon los tamaños, por ejemplo para talles de ropa.
variants_propertySon el resto de filtros que no aplican a los 3 anteriores.


{% set default_lang = current_language.lang %}
{% set filter_colors = insta_colors|length > 0 %}
{% set filter_more_colors = other_colors|length > 0 %}
{% set filter_sizes = size_properties_values|length > 0 %}
{% set filter_other = variants_properties|length > 0 %}


{% if default_lang == 'pt' %}
    {% set color_name = 'Cor' %}
    {% set size_name = 'Tamanho' %}
{% endif %}
{% if default_lang == 'es' %}
    {% set color_name = 'Color' %}
    {% set size_name = 'Talle' %}
{% endif %}
{% if default_lang == 'en' %}
    {% set color_name = 'Color' %}
    {% set size_name = 'Size' %}
{% endif %}
<div id="filters">
    {% if filter_colors %}
        <div class="mb-4">
            <h6 class="mb-2">{{ 'Color' | translate }}</h6>
            {% for name,color in insta_colors %}
                <button type="button" class="btn btn-secondary btn-circle mr-2 mb-2" style="background-color: {{ color[name] }};" title="{{ name }}" onclick="LS.urlAddParam('{{ color_name|replace("'","%27") }}', '{{ name|replace("'","%27") }}');">
                </button>
            {% endfor %}
        </div>
    {% endif %}
    {% if filter_more_colors %}
        <div class="mb-4">
            <h6 class="mb-2">
                {% if filter_colors %}
                    {{ 'Más colores' | translate }}
                {% else %}
                    {{ 'Color' | translate }}
                {% endif %}
            </h6>
            {% for color in other_colors %}
                <button type="button" class="btn btn-secondary mr-2 mb-2" onclick="LS.urlAddParam('{{ color_name|replace("'","%27") }}', '{{ color|replace("'","%27") }}');">{{ color }}
                </button>
            {% endfor %}
        </div>
    {% endif %}
    {% if filter_sizes %}
        <div class="mb-4">
            <h6 class="mb-2">{{ 'Talle' | translate }}</h6>
            {% for size in size_properties_values %}
                <button type="button" class="btn btn-secondary mr-2 mb-2" onclick="LS.urlAddParam('{{ size_name|replace("'","%27") }}', '{{ size|replace("'","%27") }}');">{{ size }}
                </button>
            {% endfor %}
        </div>
    {% endif %}


    {% for variants_property in variants_properties %}
        {% if filter_other %}
            <div class="mb-4">
                <h6 class="mb-2">{{ variants_property }}</h6>
                {% for value in variants_properties_values[variants_property] %}
                    <button type="button" class="btn btn-secondary mr-2 mb-2" onclick="LS.urlAddParam('{{ variants_property|replace("'","%27") }}', '{{ value|replace("'","%27") }}');">{{value}}
                    </button>
                {% endfor %}
            </div>
        {% endif %}
    {% endfor %}
</div>

sort-by.tpl

Es el select para ordenar productos.

{% set sort_text = {
'user': 'Destacado',
'price-ascending': 'Precio: Menor a Mayor',
'price-descending': 'Precio: Mayor a Menor',
'alpha-ascending': 'A - Z',
'alpha-descending': 'Z - A',
'created-ascending': 'Más Viejo al más Nuevo',
'created-descending': 'Más Nuevo al más Viejo',
'best-selling': 'Más Vendidos',
} %}


<div class="form-group">
    <select class="form-select js-sort-by">
        {% for sort_method in sort_methods %}
            {# This is done so we only show the user sorting method when the user chooses it #}
            {% if sort_method != 'user' or category.sort_method == 'user' %}
                <option value="{{ sort_method }}" {% if sort_by == sort_method %}selected{% endif %}>{{ sort_text[sort_method] | t }}</option>
            {% endif %}
        {% endfor %}
    </select>
    <div class="form-select-icon">
        {% include "snipplets/svg/chevron-down.tpl" with {svg_custom_class: "icon-inline icon-w-14 icon-lg svg-icon-text"} %}
    </div>
</div>

En el theme Base, para el select usamos el snipplet form-select.tpl pero en este ejemplo usamos el select sin snipplet para hacerlo más simple.

3. Llamamos a estos snipplets dentro del template category.tpl

Lo primero que hacemos es crear la siguiente variable que sirve para determinar si la categoría en la cual el usuario está, tiene filtros para mostrar o no. Lo agregamos apenas comienza el template:

{% set has_filters = insta_colors|length > 0 or other_colors|length > 0 or size_properties_values|length > 0 or variants_properties|length > 0 %}

Luego creamos la columna donde se muestran los filtros y categorías a la izquierda de la grilla de productos.

<div class="js-product-table row">
    <div class="d-none d-sm-block col-sm-2">
        {% snipplet "grid/categories.tpl" %}
        {% if has_filters %}
            <h3 class="mb-4">{{ "Filtros" | translate }}</h3>
            {% snipplet "grid/filters.tpl" %}
        {% endif %}
    </div>
    <div class="col-12 col-sm-10">
        <div class="row">
            {% include 'snipplets/product_grid.tpl' %}
        </div>
    </div>
</div>

Luego agregamos en una fila arriba de la columna y de la grilla, el botón para filtrar (que solo se ve en celulares) y el select para ordenar productos:

<div class="row mb-3 align-items-center">
    {% if products %}
        {% set columns = settings.grid_columns %}
        <div class="col-6{% if columns == 2 %} col-sm-9{% else %} col-sm-9{% endif %}">
        {% if has_filters %}
            <a href="#" class="js-modal-open filter-link d-sm-none" data-toggle="#nav-filters">
                {{ 'Filtrar' | t }} {% include "snipplets/svg/filter.tpl" with {svg_custom_class: "icon-inline icon-w-16"} %} 
            </a>           
            {% embed "snipplets/modal.tpl" with{modal_id: 'nav-filters', modal_class: 'filters modal-docked-small', modal_position: 'left', modal_transition: 'slide', modal_width: 'full'  } %}
                {% block modal_head %}
                    {{'Filtros' | translate }}
                {% endblock %}
                {% block modal_body %}
                    {% snipplet "grid/filters.tpl" %}
                {% endblock %}
            {% endembed %}
        {% endif %}
        </div>
        <div class="col-6{% if columns == 2 %} col-sm-3{% else %} col-sm-3{% endif %} text-right">
            {% include 'snipplets/grid/sort-by.tpl' %}
        </div>
    {% endif %}
</div>
<div class="row d-sm-none">
    <div class="js-append-filters col-12 mb-3 mt-3" style="display: none;">
    </div>
</div>

En este código hay que destacar lo siguiente:

  • js-append-filters es una clase que usamos para mostrar los filtros aplicados, que son agregados usando JavaScript
  • Si bien los filtros se muestran en una columna en desktop, para mobile se muestran dentro de un modal. Es por esto que incorporamos el snipplet de filters.tpl usando un embed del snipplet modal.tpl

4. Ahora necesitamos crear el snipplet para el componente modal o popup dentro de la carpeta snipplets. Este tpl se llama modal.tpl y el código es:

{# /*============================================================================
  #Modal
==============================================================================*/

#Properties
    // ID
    // Position - Top, Right, Bottom, Left
    // Transition - Slide and Fade
    // Width - Full and Box
    // modal_form_action - For modals that has a form


#Head
    // Block - modal_head
#Body
    // Block - modal_body
#Footer
    // Block - modal_footer


#}


{% set modal_overlay = modal_overlay | default(true) %}


<div id="{{ modal_id }}" class="js-modal modal modal-{{ modal_class }} modal-{{modal_position}} transition-{{modal_transition}} modal-{{modal_width}} transition-soft" style="display: none;">
    {% if modal_form_action %}
    <form action="{{ modal_form_action }}" method="post" class="{{ modal_form_class }}">
    {% endif %}
    <div class="js-modal-close modal-header">
        <span class="modal-close">
            {% include "snipplets/svg/times.tpl" with {svg_custom_class: "icon-inline svg-icon-text"} %}
        </span>
        {% block modal_head %}{% endblock %}
    </div>
    <div class="modal-body">
        {% block modal_body %}{% endblock %}
    </div>
    {% if modal_footer %}
        <div class="modal-footer d-md-block">
            {% block modal_foot %}{% endblock %}
        </div>
    {% endif %}
    {% if modal_form_action %}
    </form>
    {% endif %}
</div>

5. 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 el select, el icono de filtrado y el modal.

times.tpl

<svg class="{{ svg_custom_class }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"/></svg>

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-down.tpl

<svg class="{{ svg_custom_class }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M441.9 167.3l-19.8-19.8c-4.7-4.7-12.3-4.7-17 0L224 328.2 42.9 147.5c-4.7-4.7-12.3-4.7-17 0L6.1 167.3c-4.7 4.7-4.7 12.3 0 17l209.4 209.4c4.7 4.7 12.3 4.7 17 0l209.4-209.4c4.7-4.7 4.7-12.3 0-17z"/></svg>

filter.tpl

<svg class="{{ svg_custom_class }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M463.952 0H48.057C5.419 0-16.094 51.731 14.116 81.941L176 243.882V416c0 15.108 7.113 29.335 19.2 40l64 47.066c31.273 21.855 76.8 1.538 76.8-38.4V243.882L497.893 81.941C528.042 51.792 506.675 0 463.952 0zM288 224v240l-64-48V224L48 48h416L288 224z"/></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).

1. Agregamos el siguiente SASS de colores en static/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 y tipografías pueden variar respecto a tu diseño:

@mixin prefix($property, $value, $prefixes: ()) {
  @each $prefix in $prefixes {
      #{'-' + $prefix + '-' + $property}: $value;
  }
    #{$property}: $value;
}


/* // Modals */


.modal{
  color: $main-foreground;
  background-color:$main-background;
}


{# /* // Buttons */ #}


.btn{
  text-decoration: none;
  text-align: center;
  border: 0;
  cursor: pointer;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  text-transform: uppercase;
  background: none;
  @include prefix(transition, all 0.4s ease, webkit ms moz o);
  &:hover,
  &:focus{
    outline: 0;
    opacity: 0.8;
  }
  &[disabled],
  &[disabled]:hover{
    opacity: 0.5;
    cursor: not-allowed;
    outline: 0;
  }
  &-default{
    padding: 10px 15px; 
    background-color: rgba($main-foreground, .2);
    color: $main-foreground;
    fill: $main-foreground;
    font-weight: bold;
  }
  &-secondary{
    padding: 10px 15px; 
    background-color: $main-background;
    color: $main-foreground;
    fill: $main-foreground;
    border: 1px solid $main-foreground;
  }
  &-block{
    float: left;
    width: 100%;
  }
  &-small{
    display: inline-block;
    padding: 10px;
    font-size: 10px;
    letter-spacing: 2px;
  }
  &-circle{
    height: 32px;
    border-radius: 50%;
  }
}


button{
  @extend %body-font;
  cursor: pointer;
  &:focus{
    outline: 0;
    opacity: 0.8;
  }
}
{# /* // Forms */ #}


input,
textarea {
  font-family: $body-font;
}


.form-control {
  display: block;
  padding: 10px 8px;
  width: 100%;
  border: 0;
  border-bottom: 1px solid rgba($main-foreground, .5);
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  color: $main-foreground;
  background-color: $main-background;
  &:focus{
    outline: 0;
  }
  &-inline{
    display: inline;
  }
}


.form-control::-webkit-input-placeholder { 
  color: $main-foreground;
}
.form-control:-moz-placeholder {
  color: $main-foreground;
}
.form-control::-moz-placeholder {
  color: $main-foreground;
}
.form-control:-ms-input-placeholder {
  color: $main-foreground;
}


.form-select{
  display: block;
  padding: 10px 0;
  width: 100%;
  border: 0;
  border-bottom: 1px solid rgba($main-foreground, .5);
  border-radius: 0;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  color: $main-foreground;
  background-color: $main-background;
  &-icon{
    background: $main-background;
  }
}

2. 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, pero si no es el caso entonces podés unificar el CSS de los pasos 2 y 3 en un solo archivo.

{# /* // Forms */ #}


.form-group {
  position: relative;
  width: 100%;
}
.form-group .form-select-icon{
  position: absolute;
  bottom: 12px;
  right: 0;
  pointer-events: none;
}
.form-row {
  width: auto;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -ms-flex-wrap: wrap;
  flex-wrap: wrap;
  margin-right: -5px;
  margin-left: -5px;
  clear: both;
}


.form-row > .col,
.form-row > [class*=col-]{
  padding-right: 5px;
  padding-left: 5px;
}


.form-label {
  display: block;
  font-size: 10px;
  text-transform: uppercase;
}

3. Agregar los estilos dentro del archivo static/style-async.tpl 

@mixin prefix($property, $value, $prefixes: ()) {
  @each $prefix in $prefixes {
      #{'-' + $prefix + '-' + $property}: $value;
  }
    #{$property}: $value;
}


{# /* // Forms */ #}


.form-group{
  .form-label{
    float: left;
    width: 100%;
    margin-bottom: 10px;
  }
  .alert{
    margin: 10px 0 0 0;
  }
}




/* // Modals */


.modal {
  position: fixed;
  top: 0;
  display: block;
  width: 80%;
  height: 100%;
  padding: 10px;
  -webkit-overflow-scrolling: touch;
  overflow-y: auto;
  transition: all .2s cubic-bezier(.16,.68,.43,.99);
  z-index: 20000;
  &-header{
    width: calc(100% + 20px);
    margin: -10px 0 10px -10px;
    padding: 10px 15px;
    font-size: 20px;
  }
  &-footer{
    padding: 10px;
    clear: both;
  }
  &-full {
    width: 100%;
  }
  &-docked-sm{
    width: 100%;
  }
  &-docked-small{
    width: 80%;
  }
  &-top{
    top: -100%;
    left: 0;
  }
  &-bottom{
    top: 100%;
    left: 0;
  }
  &-left{
    left: -100%;
  }
  &-right{
    right: -100%;
  }
  &-centered{
    height: 100%;
    width: 100%;
  }
  &-top.modal-show,
  &-bottom.modal-show {
    top: 0;
  }
  &-left.modal-show {
    left: 0;
  }
  &-right.modal-show {
    right: 0;
  }
  &-close { 
    display: inline-block;
    padding: 1px 5px 5px 0;
    margin-right: 5px;
    vertical-align: middle;
    cursor: pointer;
  }
  .tab-group{
    margin:  0 -10px 20px -10px;
  }
}


.modal-overlay{
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: #00000047;
  z-index: 10000;
}


/* // Min width 768px */


@media (min-width: 768px) { 


/* //// Components */


 /* Modals */


  .modal{
    &-centered{
      height: 80%;
      width: 80%;
      left: 10%;
      margin: 5% auto;
    }
    &-docked-sm{
      width: 500px;
    }
    &-docked-small{
      width: 350px;
    }
  }
}

JS

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

{#/*============================================================================
      #Modals
    ==============================================================================*/ #}


    var $modal_close = $('.js-modal-close');
    var $modal_open = $('.js-modal-open');
    
    $modal_open.click(function (e) {
        e.preventDefault(); 
        var $modal_id = $(this).data('toggle');
        $(".js-modal-overlay").fadeToggle();
        if ($($modal_id).hasClass("modal-show")) {
            $($modal_id).removeClass("modal-show").delay(200).hide(0);
        } else {
            $($modal_id).detach().insertAfter(".js-modal-overlay").show(0).addClass("modal-show");
        }             
    });


    $modal_close.click(function (e) {
        e.preventDefault();  
        $(this).closest(".js-modal").removeClass("modal-show").delay(200).hide(0); 
        $(".js-modal-overlay").fadeOut(300);     
    });


    $(".js-modal-overlay").click(function (e) {
        e.preventDefault();  
        $(".js-modal.modal-show").removeClass("modal-show").delay(200).hide(0);   
        $(this).fadeOut(300);   
    });

2. Por último agregamos esta parte de código para el JS que aplica los filtros y para el select de orden de productos:

{% if template == 'category' %}

    {# /* // Show filters */ #}

    LS.showWhiteListedFilters("{{ filters|json_encode() }}");

    {# /* // Sort by */ #}

    $('.js-sort-by').change(function () {
        var params = LS.urlParams;
        params['sort_by'] = $(this).val();
        var sort_params_array = [];
        for (var key in params) {
            if ($.inArray(key, ['results_only', 'page']) == -1) {
                sort_params_array.push(key + '=' + params[key]);
            }
        }
        var sort_params = sort_params_array.join('&');
        window.location = window.location.pathname + '?' + sort_params;
    });

{% endif %}

Traducciones

Para terminar agregamos los textos para las traducciones en el archivo config/translations.txt

--- --- Sort By


es "Ordenar por:"
pt "Ordenar por:"
en "Sort by:"
es_mx "Ordenar por:"


es "Destacado"
pt "Destaque"
en "Featured"
es_mx "Destacado"


es "Precio: Menor a Mayor"
pt "Preço: Menor ao Maior"
en "Price: Low to High"
es_mx "Precio: Menor a Mayor"


es "Precio: Mayor a Menor"
pt "Preço: Maior ao Menor"
en "Price: High to Low"
es_mx "Precio: Mayor a Menor"


es "A - Z"
pt "A - Z"
en "A - Z"
es_mx "A - Z"


es "Z - A"
pt "Z - A"
en "Z - A"
es_mx "Z - A"


es "Más Viejo al más Nuevo"
pt "Mais Antigo ao mais Novo"
en "Oldest to Newest"
es_mx "Más Viejo al más Nuevo"


es "Más Nuevo al más Viejo"
pt "Mais Novo ao mais Antigo"
en "Newest to Oldest"
es_mx "Más Nuevo al más Viejo"


es "Más Vendidos"
pt "Mais Vendidos"
en "Best Selling"
es_mx "Más Vendidos"


--- --- Filtros


es "Filtrar por:"
pt "Filtrar por:"
en "Filter by:"
es_mx "Filtrar por:"


es "Filtros"
pt "Filtros"
en "Filters"
es_mx "Filtros"


es "Filtro aplicado:"
pt "Filtro aplicado:"
en "Applied filter:"
es_mx "Filtro aplicado:"


es "Filtrar"
pt "Filtrar"
en "Filter"
es_mx "Filtrar"


es "No tenemos productos en esas variantes. Por favor, intentá con otros filtros."
pt "Não temos produtos com estas variações. Por favor, tente com outros filtros"
en "We don&#39;t have any product with those variants. Please, try with other filters"
es_mx "No tenemos productos con esas variables. Intenta con otros filtros."


es "Más colores"
pt "Mais cores"
en "More colors"
es_mx "Más colores"


es "Categorías"
pt "Categorias"
en "Categories"
es_mx "Categorías"


es "Categorías principales"
pt "Categorias principais"
en "Main categories"
es_mx "Categorías principales"


es "Mostrar más categorías"
pt "Mostrar mais categorias"
en "Show more categories"
es_mx "Mostrar más categorías"


es "Talle"
pt "Tamanho"
en "Size"
es_mx "Talla"


es "Color"
pt "Cor"
en "Color"
es_mx "Color"

Listo, ya tenés en tu diseño ambas funcionalidad aplicadas ¡Excelente!