Filter
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— opcional0,1o2. Por defecto, el conteo natural del tipo (1para casi todo;2paradateRange). Usainputs: 0para 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 filters — label, 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) => …"— elv-modelya 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 campo | Operadores permitidos |
|---|---|
text | contains, notContains, startsWith, endsWith, equal, notEqual, exist, notExist |
number | equal, notEqual, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual, between, notBetween, exist, notExist |
amount | igual que number |
date | equal, before, after, today, yesterday, lastWeek, lastMonth, lastYear, exist, notExist |
dateRange | between, notBetween, lastWeek, lastMonth, lastYear, exist, notExist |
options | equal, notEqual, exist, notExist |
selection | equal, 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
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
filters | Record<string, SFilterField> | — | Definiciones de campo de solo lectura, llaveadas por id. |
modelValue | SFilterValue | {} | El v-model de filtros aplicados: Record<string, { operator, value }>. |
saved | SFilterSaved[] | — | Entradas guardadas opcionales. Renderiza el panel cuando está definido (incluso si está vacío). |
hideApplyButton | boolean | false | Oculta el botón Aplicar. |
hideClearButton | boolean | false | Oculta el botón Limpiar. |
applyWhenClear | boolean | false | Al limpiar, también emite apply con el valor resultante. |
immediateApply | boolean | false | Emite apply al montar con el modelValue inicial. |
responsive | boolean | true | Si los popovers internos adoptan layout responsivo. |
Eventos
| Evento | Payload | Descripción |
|---|---|---|
update:modelValue | SFilterValue | Plumbing estándar del v-model. |
apply | SFilterValue | El usuario hizo clic en Aplicar (o immediateApply se disparó al montar). |
clear | SFilterValue | El usuario hizo clic en Limpiar. Payload es el valor resultante (los permanentes se preservan). |
save | name: string, snapshot: SFilterValue | El usuario guardó el valor actual con nombre name. Persiste como prefieras. |
load | snapshot: SFilterValue | Se cargó una entrada guardada. El v-model ya fue actualizado; este evento es posterior. |
delete | name: string | El 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 twoInputs — dateRange 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.