mad-grid/dash/kanban/calendar-filters
Filtros declarativos para grids/dashboards/kanbans/calendários. 6 styles (toolbar/chips/drawer/modal/form/sidebar).
Tag de filtros declarativos para listagens, dashboards, kanbans e calendários.
Renderiza UX rica (toolbar/chips/drawer/modal/form/sidebar) a partir de fields
mad-* declarados como filhos, com auto-discovery de props públicas, persistência
em sessão, chips de filtros ativos, clear individual/global e integração nativa
com MadFiltersTrait.
Quatro aliases, mesma engine. Use o nome que mais ajuda a leitura — o compiler e os renderers são compartilhados.
| Tag | Use em | Base PHP |
|---|---|---|
<mad-dash-filters> |
Dashboards | MadDashboard |
<mad-grid-filters> |
Listagens | MadDataGrid |
<mad-kanban-filters> |
Kanbans | MadKanban |
<mad-calendar-filters> |
Calendários | MadCalendarComponent |
Todos implementam MadFilterable + use MadFiltersTrait. Se você usar
MadComponent puro, basta declarar a trait e a interface — qualquer
<mad-*-filters> funciona.
Quando usar
- Lista/Kanban/Calendar/Dashboard com mais de 1 filtro
- Quer poder trocar UX (toolbar/drawer/modal/sidebar/chips) sem reescrever PHP
- Quer chips de filtros ativos, contagem total, clear-all/clear-individual de graça
- Quer persistência em sessão de período + filtros
NÃO usar quando:
- Filtro por coluna individual (cabeçalho da grid) →
<mad-col filter>/<mad-col-filter> - Listagem sem filtros — basta
<mad-grid self>
Estrutura mínima
<mad-grid-filters style="toolbar">
<mad-input-field name="busca" label="Busca" placeholder="Nome ou cod..." />
<mad-dbcombo-field name="status_id" label="Status"
model="EstadoPedido" display="nome" />
<mad-date-field name="dtIni" label="De" />
<mad-date-field name="dtFim" label="Até" />
</mad-grid-filters>
<mad-grid self per-page="15">...</mad-grid>
Controller:
use Illuminate\Database\Eloquent\Builder;
class PedidoList extends MadDataGrid
{
protected string $model = 'Pedido';
protected bool $rememberFilters = true;
// Período via dtIni/dtFim na coluna dt_pedido
protected string $periodType = 'date-range';
protected string $dateField = 'dt_pedido';
// Props auto-discovered (string ou array publica = filtro com nome de coluna)
public string $busca = '';
public string $status_id = ''; // nome = coluna → auto-filter
// busca tem lógica id-or-text → fora do auto-apply
protected array $skipAutoFilter = ['busca'];
public function onSearch(): void
{
$this->searchQuery = function (Builder $q) {
if ($this->busca !== '') {
$v = trim($this->busca);
is_numeric($v)
? $q->where('id', '=', (int) $v)
: $q->where('obs', 'like', "%{$v}%");
}
};
}
}
Atributos do wrapper
| Attr | Valores | Default | Descrição |
|---|---|---|---|
style |
toolbar | chips | drawer | modal | form | sidebar | sidebar-left | sidebar-right |
toolbar |
Renderer (ver tabela de styles abaixo) |
name |
string | gerado | ID interno do drawer/modal (use com trigger="manual") |
title |
string | Filtros |
Cabeçalho do drawer/modal/sidebar/form |
icon |
string Lucide | sliders-horizontal |
Ícone do header (form/section) |
cols |
1 | 2 | 3 | 4 |
2 |
Colunas do grid (style="form" apenas) |
trigger |
auto | manual |
auto |
manual desliga o botão default — caller wira via open-drawer="..." |
layout |
auto | custom |
auto |
custom força tags de layout dentro como template — ver "Layout custom" |
Styles
toolbar (default)
Mini-buttons inline com popover por filtro. Apply submete tudo de uma vez. Sem wrap externo — renderiza inline.
<mad-grid-filters style="toolbar">
<mad-input-field name="busca" label="Busca" />
<mad-dbcombo-field name="cliente_id" label="Cliente" model="Cliente" display="nome" />
</mad-grid-filters>
chips
Chips de filtros ativos + popover "+ Adicionar filtro". Período
(mad-period-monthyear) renderiza como segmented acima.
<mad-grid-filters style="chips">
<mad-period-monthyear />
<mad-dbcombo-field name="vendedor_id" label="Vendedor" model="Vendedor" display="nome" />
</mad-grid-filters>
drawer
Botão "Filtros (N)" abre <mad-drawer> lateral com form completo. Ideal para
muitos filtros.
<mad-grid-filters style="drawer" title="Filtros">
<mad-input-field name="busca" label="Busca" />
<mad-dbcombo-field name="cliente_id" label="Cliente" model="Cliente" display="nome" />
<mad-date-field name="dtIni" label="De" />
<mad-date-field name="dtFim" label="Até" />
</mad-grid-filters>
Trigger manual:
<mad-grid-filters style="drawer" name="meus-filtros" trigger="manual">...</mad-grid-filters>
<mad-btn open-drawer="meus-filtros">Abrir filtros</mad-btn>
modal
Mesmo do drawer mas em modal centralizado.
<mad-grid-filters style="modal" title="Filtros avançados">
<mad-input-field name="busca" label="Busca" />
<mad-dbcombo-field name="cliente_id" label="Cliente" model="Cliente" display="nome" />
</mad-grid-filters>
form
Formulário inline acima da listagem. Todos os campos sempre visíveis em
mad-form-grid configurável. Apply submete tudo de uma vez. Bom para
listagens com 2-4 filtros principais sempre acessíveis.
<mad-grid-filters style="form" title="Filtros" :cols="3">
<mad-input-field name="busca" label="Busca" placeholder="N. ou obs..." />
<mad-dbcombo-field name="cliente_id" label="Cliente" model="Cliente" display="nome" />
<mad-dbcombo-field name="status_id" label="Status" model="EstadoPedido" display="nome" />
<mad-date-field name="dtIni" label="De" />
<mad-date-field name="dtFim" label="Até" />
</mad-grid-filters>
sidebar / sidebar-left / sidebar-right
Sidebar persistente de 260px com campos agrupados. REQUER wrap pai com
.mad-gridf-layout-left ou .mad-gridf-layout-right e conteúdo principal
dentro de <mad-grid-main>:
<div class="mad-gridf-layout-left">
<mad-grid-filters style="sidebar-left" title="Filtros">
<mad-input-field name="busca" label="Busca" />
<mad-dbcombo-field name="cliente_id" label="Cliente" model="Cliente" display="nome" />
</mad-grid-filters>
<mad-grid-main>
<mad-grid self per-page="15">...</mad-grid>
</mad-grid-main>
</div>
Sem o wrap, sidebar renderiza como bloco normal.
Layout custom — <mad-form-grid>, <mad-tabs>, etc
Em form/drawer/modal/sidebar, envolva campos com tags de layout para
controle total. Auto-detectado pelo renderer de cada style — se houver
qualquer tag de layout dentro do wrapper, ativa layout="custom".
Reconhecidas: <mad-form-grid>, <mad-form-section>, <mad-form-stack>,
<mad-tabs>/<mad-tabs-list>, <mad-accordion>, <mad-card>.
<mad-grid-filters style="form" title="Filtros">
<mad-form-section title="Período" icon="calendar">
<mad-form-grid :cols="2">
<mad-date-field name="dtIni" label="De" />
<mad-date-field name="dtFim" label="Até" />
</mad-form-grid>
</mad-form-section>
<mad-form-section title="Filtros principais" icon="filter">
<mad-form-grid :cols="3">
<mad-input-field name="busca" label="Busca" />
<mad-dbcombo-field name="cliente_id" label="Cliente" model="Cliente" display="nome" />
<mad-dbcombo-field name="status_id" label="Status" model="EstadoPedido" display="nome" />
</mad-form-grid>
</mad-form-section>
</mad-grid-filters>
Tabs por grupo:
<mad-grid-filters style="drawer" title="Filtros">
<mad-tabs default="basico" variant="underline">
<mad-tabs-list>
<mad-tab name="basico" icon="filter">Básico</mad-tab>
<mad-tab name="avancado" icon="sliders-horizontal">Avançado</mad-tab>
</mad-tabs-list>
<mad-tab-panel name="basico">
<mad-form-stack>
<mad-input-field name="busca" label="Busca" />
<mad-dbcombo-field name="cliente_id" label="Cliente" model="Cliente" display="nome" />
</mad-form-stack>
</mad-tab-panel>
<mad-tab-panel name="avancado">
<mad-form-grid :cols="2">
<mad-date-field name="dtIni" label="De" />
<mad-date-field name="dtFim" label="Até" />
</mad-form-grid>
</mad-tab-panel>
</mad-tabs>
</mad-grid-filters>
Forçar opt-in/opt-out explícito:
<mad-grid-filters style="form" layout="auto">
{{-- Ignora tags de layout, usa grid padrão --}}
</mad-grid-filters>
<mad-grid-filters style="form" layout="custom">
{{-- Força custom mesmo sem layout tag detectada --}}
</mad-grid-filters>
Restrição: toolbar e chips styles NÃO suportam layout custom — cada
filtro vira popover/chip independente.
Child tags suportados
| Tag | Tipo lógico | Uso típico |
|---|---|---|
<mad-input-field> |
input |
Texto livre (busca por nome/cod/obs) |
<mad-search-field> |
search |
Soft search (contains/like) |
<mad-dbcombo-field> |
dbcombo |
Combo single de model |
<mad-dbselect-field> |
dbselect |
Select multiplo de model |
<mad-dbunique-search-field> |
dbsearch |
Async unique search (autocomplete) |
<mad-select-field> |
select |
Select estático |
<mad-date-field> |
date |
Data única |
<mad-daterange-field> |
daterange |
Range de datas (dtIni + dtFim em 1 campo) |
<mad-switch-field> |
switch |
Boolean toggle |
<mad-checkbox-field> |
checkbox |
Checkbox |
<mad-number-field> |
number |
Número |
<mad-period-monthyear> |
period-monthyear |
Segmented mês/ano (special) |
Atributos comuns: name (obrigatório, bate com prop pública no controller),
label, placeholder, required.
Filtro obrigatório (required)
Campo com required dentro do bloco vira condição para buscar (5.108.1+):
- Aplicar/Buscar e Atualizar com o campo vazio não consultam: o erro aparece no próprio campo ("O campo Fim é obrigatório."), como no formulário.
- A listagem abre vazia (como
require-filter), sem o botão "Carregar registros", e nenhuma ação (página, por página, busca rápida) consulta enquanto um obrigatório estiver vazio. <mad-daterange-field required>exige as duas datas; o erro aparece no campo.- Só o atributo literal conta:
requiredourequired="true".:required="..."(expressão) não ativa a regra.
<mad-grid-filters style="form" apply-label="Buscar">
<mad-date-field name="inicio" label="Inicio" required />
<mad-date-field name="fim" label="Fim" required />
</mad-grid-filters>
<mad-grid self load-hint="Informe um periodo para buscar"> ... </mad-grid>
Período automático
A trait suporta 4 modos via protected string $periodType:
periodType |
Props ativas | UI típico |
|---|---|---|
none (default em grids) |
nenhuma | sem período |
month-year |
$mes + $ano |
<mad-period-monthyear /> |
date-range |
$dtIni + $dtFim |
<mad-date-field name="dtIni"> + <mad-date-field name="dtFim"> |
preset |
$preset (hoje/ontem/7d/30d/mes/mes_anterior/trimestre/ano/ano_anterior) |
<mad-select-field name="preset"> |
Em date-range e preset exige protected string $dateField = 'coluna_data'.
Em month-year exige colunas denormalizadas no model — default
['mes' => 'mes', 'ano' => 'ano']. Override via $periodFields:
protected array $periodFields = ['mes' => 'mes_venc', 'ano' => 'ano_venc'];
Tudo aplicado automaticamente no Eloquent Builder da query principal —
applyPeriodoToQuery() e applyAutoFiltersToQuery() (camadas do
MadFiltersTrait) rodam antes de onSearch() em cada host (_buildQuery()
no MadDataGrid, baseQuery() no MadDashboard, etc).
Auto-discovery de filter props
Toda public string/array prop NÃO-reservada vira filtro automático.
Nome da prop = nome da coluna. Op = = (string) ou in (array).
public string $cliente_id = ''; // vira $q->where('cliente_id', '=', val)
public string $vendedor_id = '';
public array $tags = []; // vira $q->whereIn('tags', [...])
Reservadas (gerenciadas pela trait, não redeclarar):
form, mes, ano, dtIni, dtFim, preset.
Trait tenta skipar automaticamente props cujo nome não bate com coluna do
model (via modelHasColumn()) — mas o guard é fail-open quando o model
não resolve ou o schema é desconhecido. Ver gotcha abaixo antes de assumir que
typos são sempre ignorados.
$skipAutoFilter — desliga auto-apply em props específicas
Use quando o filtro tem lógica custom (id-or-text, soft match, range, operador diferente, coluna com nome diferente da prop):
use Illuminate\Database\Eloquent\Builder;
protected array $skipAutoFilter = ['busca', 'preco_min', 'clienteId'];
public function onSearch(): void
{
$this->searchQuery = function (Builder $q) {
// Busca id-or-text
if ($this->busca !== '') {
$v = trim($this->busca);
is_numeric($v)
? $q->where('id', '=', (int) $v)
: $q->where('nome', 'like', "%{$v}%");
}
// Prop com nome diferente da coluna
if ($this->clienteId !== '') {
$q->where('cliente_id', '=', $this->clienteId);
}
// Op diferente
if ($this->preco_min !== '') {
$q->where('preco', '>=', $this->preco_min);
}
};
}
Decisão de design: filtros que não são
prop_name = valuedireto vivem emonSearch(). A trait foi enxugada — não existem mais$filterMapnembaseCriteriaRules(). Tudo que não forprop_name = valuedireto vai proonSearch()(op diferente, coluna alias, sempre-ligado, OR/AND complexos).
Opt-out auto-merge — controle total via onSearch()
Para controle total dos filtros (sem auto-discovery), desligue o auto-merge —
nesse modo onSearch() precisa chamar os helpers da trait manualmente se
ainda quiser período/unit aplicados:
use Illuminate\Database\Eloquent\Builder;
class PedidoList extends MadDataGrid
{
protected string $periodType = 'date-range';
protected string $dateField = 'dt_pedido';
// Trait só cuida de state/UI/persist, não toca na query
protected bool $autoMergeDashFilters = false;
public string $busca = '';
public string $statusFiltro = '';
public function onSearch(): void
{
$this->searchQuery = function (Builder $q) {
// Helper da trait — adiciona período conforme $periodType
$this->applyPeriodoToQuery($q, $this->model);
if ($this->statusFiltro !== '') {
$q->where('estado_pedido_id', '=', $this->statusFiltro);
}
if ($this->busca !== '') {
$v = trim($this->busca);
is_numeric($v)
? $q->where('id', '=', (int) $v)
: $q->where('obs', 'like', "%{$v}%");
}
};
}
}
Soft delete (deleted_at) já é tratado pelo trait SoftDeletes do model —
não precisa filtrar manualmente.
Helpers da trait disponíveis no onSearch() (recebem o Builder direto):
| Helper | Uso |
|---|---|
applyPeriodoToQuery($q, $model) |
Adiciona filtro de período conforme $periodType |
applyUnitToQuery($q, $model) |
Adiciona filtro unit multi-tenant (se $applyUnitFilter=true) |
applyAutoFiltersToQuery($q, $model, $opts = []) |
Aplica auto-discovery (props públicas → $q->where) |
$this->mes, $this->ano, $this->dtIni, $this->dtFim, $this->preset |
State de período cru (string) |
Persistência de sessão
protected bool $rememberFilters = true;
Persiste:
- mes/ano/dtIni/dtFim/preset
- TODAS as public filter props (incluindo
$skipAutoFilter)
Storage key: mad_filters_<class>_state (separado do grid state padrão).
Handlers automáticos
A trait expõe (todos públicos, chamáveis via mad:click / submit):
| Method | Quando |
|---|---|
onShow() |
Submit do form (Apply) — alias onFiltrar() |
onAtualizar() |
Refresh sem mudar filtros — alias onRefresh() |
onLimpar() |
Reset all + persist |
clearFilter($name) |
Reset um filtro específico ('_period' limpa mes+ano) |
setProp($prop, $value) |
Set genérico + apply |
setMesAno($mes, $ano) |
Set período + apply (usado pelos presets) |
Trigger pós-apply (host-controlled)
| Host | Comportamento default |
|---|---|
MadDashboard |
forceFullRender() — re-render do dashboard inteiro |
MadDataGrid |
page = 1; loadData() — recarrega grid resetando paginação |
MadKanban |
loadData() — recarrega stages/cards (query por coluna via _buildBaseCardQuery()) |
MadCalendarComponent |
saveFilterSession() + forceFullRender() — re-render reconstrói o calendário (Alpine), que refaz fetch via o endpoint static getEvents |
Override no controller para comportamento custom:
protected function applyFiltersChanged(): void
{
$this->page = 1;
$this->loadData();
// ... lógica extra ...
}
Override onLimpar para re-seed defaults
Se você seedou defaults em mount() e quer preservá-los após Limpar:
public function mount(array $params = []): void
{
$this->dtIni = '2020-01-01';
$this->dtFim = date('Y-12-31');
parent::mount($params);
}
public function onLimpar(): void
{
$this->busca = '';
$this->dtIni = '2020-01-01';
$this->dtFim = date('Y-12-31');
foreach ($this->discoverFilterProps() as $p) {
if (in_array($p, ['dtIni', 'dtFim'], true)) continue;
$this->$p = is_array($this->$p) ? [] : '';
}
$this->mes = '';
$this->ano = '';
$this->preset = '';
$this->page = 1;
$this->syncFormFields();
$this->saveFilterSession();
$this->loadData();
}
Drill-down — passar filtros para outra tela
<mad-btn navigate="OutraTela" :params="$that->drillToParams(['extra' => 'X'])">
Abrir com filtros
</mad-btn>
drillToParams($extra = []) retorna array com período + todas as filter props
não-vazias + extras.
Mapeamento style → renderer interno
style |
Renderer blade |
|---|---|
toolbar |
components.dash-filters-toolbar |
chips |
components.dash-filters-chips |
drawer |
components.dash-filters-drawer |
modal |
components.dash-filters-modal |
form |
components.dash-filters-form |
sidebar / sidebar-left / sidebar-right |
components.dash-filters-sidebar |
Exemplo end-to-end — listagem completa
@php
// baseQuery() (MadFiltersTrait) devolve um Builder Eloquent ja com
// periodo/unit/auto-filters + onSearch aplicados — mesmo filtro ativo na grid.
$filteredQuery = $that->baseQuery(\App\Models\Pedido::class);
@endphp
<mad-page-container>
<mad-page-header title="Pedidos" icon="shopping-cart">
<actions>
<mad-btn navigate="PedidoForm" variant="primary" icon="plus">Novo</mad-btn>
</actions>
</mad-page-header>
<mad-page-content>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:20px;">
<mad-db-metric-card model="Pedido" total="count" :query="$filteredQuery" label="Total" icon="hash" />
<mad-db-metric-card model="Pedido" field="valor" total="sum" :query="$filteredQuery" label="Valor" icon="circle-dollar-sign" format="money:R$" />
<mad-db-metric-card model="Pedido" total="count" :query="$filteredQuery" label="Período" icon="calendar" />
</div>
<mad-grid-filters style="toolbar">
<mad-input-field name="busca" label="Busca" placeholder="N. do pedido ou obs..." />
<mad-dbcombo-field name="status_id" label="Status" model="EstadoPedido" display="nome" />
<mad-date-field name="dtIni" label="De" />
<mad-date-field name="dtFim" label="Até" />
</mad-grid-filters>
<mad-grid self per-page="15" sticky>
<mad-columns>
<mad-col field="id" label="N." width="65" center sort />
<mad-col field="dt_pedido" label="Data" width="100" center sort date="d/m/Y" />
<mad-col field="valor" label="Valor" right sort money="R$" total="sum" />
<mad-col field="obs" label="Obs" edit edit-type="text" edit-mode="inline" />
</mad-columns>
<mad-actions>
<mad-nav icon="pencil" label="Editar" target="PedidoForm::onEdit({id})" />
</mad-actions>
</mad-grid>
</mad-page-content>
</mad-page-container>
Gotchas
- Sidebar requer wrap pai (
.mad-gridf-layout-*+<mad-grid-main>). Sem isso renderiza como bloco normal (não fica lateral). - Props públicas devem ser
stringouarray. Outros tipos (int, bool tipados) não são detectadas pela auto-discovery. - NÃO redeclare props reservadas (
form,mes,ano,dtIni,dtFim,preset) — a trait é dona delas. - Submit submete TODOS os fields no form único do wrapper — popovers do toolbar não têm forms aninhados.
onSearch()assinatura varia por host. SóMadDataGridusa o estilo 0-arg + closure:onSearch(): voidmonta$this->searchQuery = function (Builder $q) { ... }(propriedade do proprioMadDataGrid, nao da trait — o grid aplica essa closure manualmente em_buildQuery()).MadKanban,MadCalendarComponenteMadDashboardusam o estilo builder-native direto:onSearch(Builder $q, ?string $model = null): void— aplica$q->where(...)direto no builder recebido, sem closure (o$modelé opcional, aceita 1 ou 2 args). A trait detecta a assinatura via reflection em_applyOnSearch()e chama a forma certa — declarar a assinatura errada pro host (ex: 0-arg numMadKanban) faz o filtro ser silenciosamente ignorado.$dateFieldobrigatório emdate-range/preset.- dtIni/dtFim aceitam ambos formatos (display
d/m/Ye DBY-m-d). Trait normaliza antes do SQL via_normalizeDateToDb(). - Borda inferior de período NUNCA leva hora.
applyPeriodoToQuery()geradt >= 'Y-m-d'pro início edt <= 'Y-m-d 23:59:59'pro fim — de propósito assimétrico. Em colunaDATE(comparada como string em alguns drivers),'2026-06-01 00:00:00'é uma string MAIOR que'2026-06-01'e o registro da borda some do resultado. Se você consumir$dtIni/$dtFimfora do helper (lógica custom emonSearch()), replique exatamente esse padrão — não gere>= 'Y-m-d 00:00:00'na mão. modelHasColumn()é fail-open. Se o$modelpassado praapplyAutoFiltersToQuery()/baseQuery()fornull, vazio, ou não resolver pra uma classe real, o guard libera TODAS as props auto-discovered sem checar coluna — typo no nome da prop não vira "ignorado silenciosamente", vira$q->where('coluna_inexistente', ...). Sempre chame esses helpers com o model resolvido (ex:\App\Models\Pedido::class), igual o exemplo end-to-end abaixo.
NUNCA fazer
{{-- ERRADO: filtro em prop não-string/array — auto-discovery não detecta --}}
public int $clienteId = 0;
public bool $ativo = false;
{{-- CERTO: usar string sempre (mesmo para IDs) --}}
public string $clienteId = '';
public string $ativo = '';
{{-- ERRADO: redeclarar prop reservada --}}
public string $mes = ''; // trait já tem essa prop — colide
{{-- CERTO: usar a prop $mes da trait via $periodType="month-year" --}}
{{-- ERRADO: misturar $filterMap com onSearch() (removido na trait nova) --}}
protected array $filterMap = ['x' => ['op' => '>=', 'col' => 'preco']];
{{-- CERTO: $skipAutoFilter + lógica no onSearch() --}}
protected array $skipAutoFilter = ['x'];
public function onSearch(): void { ... }