Em aplicações web que lidam com grandes volumes de dados, é comum a necessidade de formulários de busca e filtragem que permitem múltiplos critérios. Para gerenciar a complexidade e a usabilidade, especialmente quando há muitos campos, a funcionalidade de expandir/recolher o formulário de filtros é extremamente útil. Este artigo demonstra como implementar um componente de formulário de busca avançada com essa caapcidade, utilizando CSS Grid para o layout e o Vue.js (com Element Plus) para a lógica interativa e a injeção dinâmica de estilos.
Layout com CSS Grid
A CSS Grid oferece uma maneira poderosa e flexível de criar layouts bidimensionais. Para um formulário de filtro, podemos definir um número fixo de colunas e posicionar os itens de formulário automaticamente. O desafio reside em ocultar dinamicamente um subconjunto desses campos quando o formulário está "recolhido", enquanto sempre exibimos os primeiros e o último (que geralmente contém os botões de ação).
A estrutura base do formulário de busca pode ser definida com CSS Grid da seguinte forma:
.search-filter-grid {
display: grid;
grid-template-columns: repeat(var(--filter-columns, 4), 1fr); /* Número de colunas configurável */
gap: 10px; /* Espaçamento entre os itens */
/* Estilos para os itens de formulário (Element Plus) */
:global(.el-form-item) {
margin-bottom: 0; /* Remove margem inferior padrão */
/* Garante que o último item (botões) ocupe todas as colunas restantes */
&:last-child {
grid-column-end: span var(--filter-columns, 4);
justify-self: end; /* Alinha os botões à direita */
}
}
}
No entanto, a parte mais complicada é ocultar os campos intermediários quando o formulário está recolhido. A seletor :nth-child(n + X) é ideal para isso, mas o valor X precisa ser dinâmico (baseado no número de colunas). Como o v-bind() do Vue no <style module> não pode ser usado diretamente dentro de seletores complexos como nth-child para valores arbitrários, precisamos injetar este estilo dinamicamente.
Componente de Filtro Expansível (FilterPanel.vue)
Vamos criar um componente Vue 3 para encapsular essa funcionalidade.
<script setup lang="ts">
import { computed, ref, useCssModule, watch, onUnmounted } from 'vue';
import type { FormInstance, FormProps } from 'element-plus';
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
// Definição das propriedades do componente
interface FilterPanelProps extends Omit<FormProps, 'inline'> {
gridColumns?: number; // Número de colunas para o layout Grid
}
const props = withDefaults(defineProps<FilterPanelProps>(), {
gridColumns: 4,
});
// Emits para notificar o componente pai sobre ações
const emits = defineEmits<{
(e: 'visibilityChange', visible: boolean): void;
(e: 'resetFilters'): void;
(e: 'applyFilters'): void;
}>();
const style = useCssModule(); // Obtém os estilos CSS do módulo
const formRef = ref<FormInstance | null>(null);
const isExpanded = ref(false); // Estado de expansão do formulário
// Propriedades do formulário herdadas
const inheritedFormProps = computed(() => {
const { gridColumns, ...restProps } = props;
return restProps;
});
// Manipulador para alternar a visibilidade do formulário
const toggleVisibility = () => {
isExpanded.value = !isExpanded.value;
emits('visibilityChange', isExpanded.value);
};
// Manipulador para redefinir os filtros
const handleReset = () => {
emits('resetFilters');
formRef.value?.resetFields(); // Reseta os campos do formulário Element Plus
};
// Manipulador para aplicar os filtros
const handleApply = () => {
emits('applyFilters');
formRef.value?.validate((isValid) => {
if (isValid) {
console.log('Filtros aplicados!');
} else {
console.log('Formulário inválido!');
}
});
};
// Referência para o elemento <style> injetado
const dynamicStyleElement = ref<HTMLStyleElement | null>(null);
// Função para injetar dinamicamente as regras CSS para ocultar/mostrar campos
const injectDynamicVisibilityStyles = (columns: number) => {
const { filterFormContainer } = style; // Obtém o nome da classe CSS do módulo
// A regra CSS oculta todos os itens de formulário a partir da 'columns'-ésima posição
// exceto o último item (que contém os botões de ação).
const cssRule = `
.${filterFormContainer}:not(.expanded) .el-form-item:nth-child(n + ${columns + 1}):not(:last-child) {
display: none;
}
`;
const cssTextNode = document.createTextNode(cssRule);
if (dynamicStyleElement.value) {
// Se o elemento <style> já existe, atualiza seu conteúdo
if (dynamicStyleElement.value.firstChild) {
dynamicStyleElement.value.removeChild(dynamicStyleElement.value.firstChild);
}
dynamicStyleElement.value.appendChild(cssTextNode);
} else {
// Caso contrário, cria e adiciona um novo elemento <style> ao cabeçalho do documento
const styleTag = document.createElement('style');
styleTag.appendChild(cssTextNode);
dynamicStyleElement.value = styleTag;
document.head.appendChild(styleTag);
}
};
// Observa mudanças na propriedade gridColumns para atualizar os estilos dinâmicos
watch(
() => props.gridColumns,
(newColumns) => {
injectDynamicVisibilityStyles(newColumns);
},
{ immediate: true } // Executa imediatamente na montagem do componente
);
// Limpa o elemento <style> injetado quando o componente é desmontado
onUnmounted(() => {
if (dynamicStyleElement.value && document.head.contains(dynamicStyleElement.value)) {
document.head.removeChild(dynamicStyleElement.value);
}
});
// Expõe o ref do formulário para acesso externo
defineExpose({
form: formRef,
});
</script>
<template>
<el-form
ref="formRef"
v-bind="inheritedFormProps"
@submit.prevent="handleApply"
@reset.prevent="handleReset"
:class="[style.filterFormContainer, { expanded: isExpanded }]"
:style="{ '--filter-columns': props.gridColumns }"
>
<slot></slot>
<el-form-item>
<el-button @click="toggleVisibility" :icon="isExpanded ? ArrowUp : ArrowDown">
{{ isExpanded ? 'Recolher' : 'Expandir' }}
</el-button>
<el-button type="primary" native-type="submit">Pesquisar</el-button>
<el-button native-type="reset">Limpar</el-button>
</el-form-item>
</el-form>
</template>
<style module lang="scss">
.filterFormContainer {
display: grid;
grid-template-columns: repeat(v-bind(props.gridColumns), 1fr);
gap: 10px;
/* Estilos específicos para os itens do Element Plus dentro deste container */
:global(.el-form-item) {
margin-bottom: 0;
/* O último item (botões) ocupa as colunas restantes */
&:last-child {
grid-column: v-bind('props.gridColumns + 1'); /* Inicia na próxima coluna após a última definida */
grid-column-end: span v-bind(props.gridColumns); /* Ocupa 'gridColumns' de largura */
justify-self: end; /* Alinha os botões à direita */
}
}
}
</style>
Como Utilizar o Componente
Para usar o FilterPanel, basta envolvê-lo em seus campos el-form-item e passar um objeto de modelo de dados:
<script setup lang="ts">
import { reactive, ref } from 'vue';
import FilterPanel from './FilterPanel.vue'; // Ajuste o caminho conforme necessário
const filterData = reactive({
name: '',
region: '',
category: '',
status: '',
dateRange: '',
priority: '',
owner: '',
creationDate: '',
lastUpdate: '',
tags: '',
});
// Opcional: manipular eventos do componente
const handleVisibilityChange = (isVisible: boolean) => {
console.log(`Formulário agora está ${isVisible ? 'expandido' : 'recolhido'}`);
};
const handleResetFilters = () => {
console.log('Filtros foram resetados.');
Object.keys(filterData).forEach(key => (filterData[key] = '')); // Limpa todos os campos
};
const handleApplyFilters = () => {
console.log('Filtros aplicados:', filterData);
// Aqui você faria a chamada à API ou lógica de filtragem de dados
};
</script>
<template>
<FilterPanel
:model="filterData"
label-width="120px"
:grid-columns="3"
@visibilityChange="handleVisibilityChange"
@resetFilters="handleResetFilters"
@applyFilters="handleApplyFilters"
>
<el-form-item label="Nome" prop="name">
<el-input v-model="filterData.name" placeholder="Digite o nome" />
</el-form-item>
<el-form-item label="Região" prop="region">
<el-input v-model="filterData.region" placeholder="Selecione a região" />
</el-form-item>
<el-form-item label="Categoria" prop="category">
<el-input v-model="filterData.category" placeholder="Escolha a categoria" />
</el-form-item>
<el-form-item label="Status" prop="status">
<el-input v-model="filterData.status" placeholder="Status do item" />
</el-form-item>
<el-form-item label="Intervalo de Datas" prop="dateRange">
<el-input v-model="filterData.dateRange" placeholder="Período" />
</el-form-item>
<el-form-item label="Prioridade" prop="priority">
<el-input v-model="filterData.priority" placeholder="Nível de prioridade" />
</el-form-item>
<el-form-item label="Proprietário" prop="owner">
<el-input v-model="filterData.owner" placeholder="Responsável" />
</el-form-item>
<el-form-item label="Data de Criação" prop="creationDate">
<el-input v-model="filterData.creationDate" placeholder="Data" />
</el-form-item>
<el-form-item label="Última Atualização" prop="lastUpdate">
<el-input v-model="filterData.lastUpdate" placeholder="Data" />
</el-form-item>
<el-form-item label="Tags" prop="tags">
<el-input v-model="filterData.tags" placeholder="Tags associadas" />
</el-form-item>
</FilterPanel>
</template>