v3.0.0-beta.17
Utilidades

Filter

Un componente versátil de filtros con definiciones de campo tipadas y v-model como dueño del estado aplicado.
Tests
100%

Uso

SFilter permite que los usuarios agreguen, configuren y apliquen filtros sobre un conjunto tipado de campos. El componente es totalmente controlado: la prop filters contiene la configuración de solo lectura y un v-model contiene los valores de filtro aplicados.

La prop filters es un registro de id → definición. Cada definición elige uno de siete valores de type: text, number, amount, date, dateRange, options, selection. TypeScript solo permite operadores compatibles con el type escogido.

El v-model es Record<string, { operator, value }>. Solo los filtros aplicados aparecen en el modelo. delete value.foo quita un filtro; value = {} los limpia todos.

Tipos de campo

text

Entrada de texto en una línea. El operador por defecto es contains.

number / amount / date

number y date renderizan una entrada simple acorde al tipo; amount añade un selector de divisa. Comparten el patrón de un input por operador (dos cuando el operador es between o notBetween).

dateRange

Selector de rango de fechas dedicado. El operador por defecto es between.

options

Lista de opciones discretas. choices puede ser un arreglo de string (id y label iguales) o de objetos { id, label }. Activa multiple: true para selección múltiple.

selection

Entrada de etiquetas libre — el usuario teclea sus propios valores.

Operadores de existencia

Todos los tipos soportan exist / notExist. No renderizan input de valor.

Operadores personalizados

Los operadores personalizados viven en customOperators (separado del arreglo operators predefinido). Cada uno tiene:

  • id — id único.
  • label — texto mostrado en el dropdown.
  • inputs — opcional 0, 1 o 2. Por defecto, el conteo natural del tipo (1 para casi todo; 2 para dateRange). Usa inputs: 0 para imitar operadores de existencia.
  • tag — formateador opcional para la insignia. String literal, función de (value) => string, o ausente (se usa el valor crudo).

Estado inicial

Inicializa el ref del v-model con filtros preaplicados. El componente renderiza las insignias y excluye esos campos de la lista "Agregar filtro". Útil para query params, presets guardados, e hidratación desde localStorage o backend.

Campos permanentes

Configura permanent: true para que un campo no se pueda eliminar ni limpiar. La insignia mantiene su contenido pero no renderiza control de remover; clear deja intactos los permanentes.

Ocultar botones de acción

Usa hideApplyButton y hideClearButton para controlar la visibilidad de los botones.

i18n / labels reactivos

Los labels de los operadores predefinidos se traducen automáticamente mediante el namespace $spartan.filter.operator.* — cuando cambia el idioma, vue-i18n re-renderiza las insignias por sí solo.

Todo lo demás que escribes en el objeto filterslabel, choices y los labels/tags de customOperators — son strings literales. Si los hardcodeas, no reaccionarán a un cambio de idioma. Para que sean conscientes del locale, envuelve filters en un computed que dependa del t de vue-i18n:

const { t } = useI18n();

const filters = computed(() => ({
    status: {
        type: 'options',
        label: t('myApp.statusLabel'),
        choices: [{ id: 'active', label: t('myApp.statusActive') }],
        operators: ['equal'],
    },
}));

Cuando cambia el idioma, t() recalcula, filters devuelve un objeto nuevo y las insignias se re-renderizan con los labels traducidos.

Filtros guardados

Provee la prop saved para renderizar el panel. El componente nunca muta el arreglo — la persistencia es responsabilidad del consumidor. Escucha:

  • @save="(name, snapshot) => …" — agrégalo a tu store.
  • @load="(snapshot) => …" — el v-model ya está actualizado cuando este evento se dispara.
  • @delete="(name) => …" — quítalo de tu store.

Validación

Cada campo puede declarar una función validate. Recibe (value, operator) y devuelve un mensaje de error string (null si es válido). Se soportan validaciones async.

<script setup lang="ts">
import { ref } from 'vue';
import { SFilter, type SFilterField, type SFilterValue } from '@placetopay/spartan-vue';

const filters = {
    bin: {
        type: 'text',
        label: 'BIN de la tarjeta',
        operators: ['equal'],
        validate: async (value: any) => (/^\d{6}$/.test(value) ? null : 'El BIN debe tener 6 dígitos'),
    },
    amount: {
        type: 'amount',
        label: 'Monto',
        currency: 'USD',
        operators: ['between'],
        validate: (value: any, operator: string) =>
            operator === 'between' && parseFloat(value[0]) > parseFloat(value[1])
                ? 'El valor mínimo debe ser menor que el máximo'
                : null,
    },
} satisfies Record<string, SFilterField>;

const value = ref<SFilterValue>({});
</script>

<template>
    <SFilter v-model="value" :filters="filters" />
</template>

Referencia de operadores

OPERATORS_BY_TYPE se exporta públicamente para que puedas construir interfaces propias contra el mismo set.

import { OPERATORS_BY_TYPE } from '@placetopay/spartan-vue';

console.log(OPERATORS_BY_TYPE.text); // ['contains', 'notContains', ...]
Tipo de campoOperadores permitidos
textcontains, notContains, startsWith, endsWith, equal, notEqual, exist, notExist
numberequal, notEqual, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual, between, notBetween, exist, notExist
amountigual que number
dateequal, before, after, today, yesterday, lastWeek, lastMonth, lastYear, exist, notExist
dateRangebetween, notBetween, lastWeek, lastMonth, lastYear, exist, notExist
optionsequal, notEqual, exist, notExist
selectionequal, notEqual, exist, notExist

Métodos expuestos

El componente expone apply y clear via template ref.

<template>
    <SFilter ref="filterRef" v-model="value" :filters="filters" />
    <button @click="filterRef?.apply()">Aplicar</button>
    <button @click="filterRef?.clear()">Limpiar</button>
</template>

Props

PropTipoPor defectoDescripción
filtersRecord<string, SFilterField>Definiciones de campo de solo lectura, llaveadas por id.
modelValueSFilterValue{}El v-model de filtros aplicados: Record<string, { operator, value }>.
savedSFilterSaved[]Entradas guardadas opcionales. Renderiza el panel cuando está definido (incluso si está vacío).
hideApplyButtonbooleanfalseOculta el botón Aplicar.
hideClearButtonbooleanfalseOculta el botón Limpiar.
applyWhenClearbooleanfalseAl limpiar, también emite apply con el valor resultante.
immediateApplybooleanfalseEmite apply al montar con el modelValue inicial.
responsivebooleantrueSi los popovers internos adoptan layout responsivo.

Eventos

EventoPayloadDescripción
update:modelValueSFilterValuePlumbing estándar del v-model.
applySFilterValueEl usuario hizo clic en Aplicar (o immediateApply se disparó al montar).
clearSFilterValueEl usuario hizo clic en Limpiar. Payload es el valor resultante (los permanentes se preservan).
savename: string, snapshot: SFilterValueEl usuario guardó el valor actual con nombre name. Persiste como prefieras.
loadsnapshot: SFilterValueSe cargó una entrada guardada. El v-model ya fue actualizado; este evento es posterior.
deletename: stringEl usuario eliminó una entrada guardada. Actualiza tu store.

Migración desde v1

La API v2 es un cambio limpio respecto de v1. No hay adaptador en runtime. A continuación los 6 escenarios canónicos de migración.

1. Filtro de texto simple

// ANTES
const fields = ref<TField[]>([
    {
        id: 'name',
        name: 'Nombre',
        interfaces: { oneInput: { type: 'text', operators: ['contains'] } },
    },
]);

function onApply(applied: TField[]) {
    const used = applied.filter((f) => f.state);
    refetch(used);
}

<SFilter :fields="fields" @apply="onApply" />
// DESPUÉS
import type { SFilterValue } from '@placetopay/spartan-vue';

const filters = {
    name: { type: 'text', label: 'Nombre', operators: ['contains'] },
} as const;

const value = ref<SFilterValue>({});

function onApply(v: SFilterValue) {
    refetch(v);
}

<SFilter v-model="value" :filters="filters" @apply="onApply" />

2. Select múltiple con operadores

// ANTES
{
    id: 'brand',
    name: 'Brand',
    interfaces: {
        options: {
            multiple: true,
            operators: ['equal', 'notEqual', 'contains', 'notContains'],
            options: ['Nike', 'Adidas', { id: 'pma', label: 'Puma' }],
        },
    },
}
// DESPUÉS
const filters = {
    brand: {
        type: 'options',
        label: 'Brand',
        multiple: true,
        operators: ['equal', 'notEqual'],
        choices: ['Nike', 'Adidas', { id: 'pma', label: 'Puma' }],
    },
} as const;

contains / notContains ya no son válidos en un campo options — TypeScript lo detecta en tiempo de compilación. Si necesitas búsqueda libre por la misma columna, declara un filtro text aparte.

3. Operador personalizado

// ANTES
{
    id: 'price',
    name: 'Price',
    interfaces: {
        oneInput: {
            type: 'amount',
            currency: 'USD',
            operators: [
                'equal',
                { id: 'thousands', label: 'Miles', tag: (v) => `${v}K` },
            ],
        },
    },
}
// DESPUÉS
const filters = {
    price: {
        type: 'amount',
        label: 'Price',
        currency: 'USD',
        operators: ['equal'],
        customOperators: [{ id: 'thousands', label: 'Miles', tag: (v: any) => `${v}K` }],
    },
} as const;

4. Rango de fechas

// ANTES
{
    id: 'createdAt',
    name: 'Creado',
    interfaces: {
        twoInputs: { type: 'date', operators: ['between', 'lastWeek'] },
    },
}
// DESPUÉS
const filters = {
    createdAt: {
        type: 'dateRange',
        label: 'Creado',
        operators: ['between', 'lastWeek'],
    },
} as const;

Ya no existe la interfaz twoInputsdateRange es un tipo de campo propio con su unión de operadores.

5. Filtros guardados

// ANTES
const fields = ref([...]);
const saved = ref<TSaveData[]>([]);

<SFilter
    :fields="fields"
    :saved="saved"
    @save="(all) => saved.value = all"
    @load="(filters) => { /* el componente ya mutó fields */ refetch(); }"
/>
// DESPUÉS
import type { SFilterValue, SFilterSaved } from '@placetopay/spartan-vue';

const value = ref<SFilterValue>({});
const saved = ref<SFilterSaved[]>([]);

function onSave(name: string, snapshot: SFilterValue) {
    saved.value = [...saved.value, { name, snapshot }];
}
function onLoad(_snapshot: SFilterValue) {
    // value ya fue actualizado vía v-model
    refetch(value.value);
}
function onDelete(name: string) {
    saved.value = saved.value.filter((s) => s.name !== name);
}

<SFilter
    v-model="value"
    :filters="filters"
    :saved="saved"
    @save="onSave"
    @load="onLoad"
    @delete="onDelete"
/>

Si tienes datos guardados con shape v1 (por ejemplo en localStorage), corre esta transformación una vez al adoptar v2:

function migrateSavedV1ToV2(v1: { name: string; filters: any[] }[]) {
    return v1.map(({ name, filters }) => ({
        name,
        snapshot: Object.fromEntries(
            filters
                .filter((f) => f.state)
                .map((f) => [f.id, { operator: f.state.operator, value: f.state.value }]),
        ),
    }));
}

6. Estado inicial / presets

// ANTES — solo posible mutando la definición del campo
const fields = ref<TField[]>([
    {
        id: 'status',
        name: 'Estado',
        interfaces: { options: { operators: ['equal'], options: ['active', 'paused'] } },
        state: { operator: 'equal', value: 'active' }, // ← preaplicado por mutación
    },
]);
// DESPUÉS — inicializa el v-model
const filters = {
    status: {
        type: 'options',
        label: 'Estado',
        choices: ['active', 'paused'],
        operators: ['equal'],
    },
} as const;

const value = ref<SFilterValue>({
    status: { operator: 'equal', value: 'active' }, // ← preaplicado por v-model
});

<SFilter v-model="value" :filters="filters" />

Sirve para query params, filtros desde backend e hidratación desde localStorage.

Copyright © 2026