Docs›Componentes (Admin)›mad-grid/dash/kanban/calendar-filters
Componentes (Admin)

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>

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 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: required ou required="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 = value direto vivem em onSearch(). A trait foi enxugada — não existem mais $filterMap nem baseCriteriaRules(). Tudo que não for prop_name = value direto vai pro onSearch() (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 string ou array. 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ó MadDataGrid usa o estilo 0-arg + closure: onSearch(): void monta $this->searchQuery = function (Builder $q) { ... } (propriedade do proprio MadDataGrid, nao da trait — o grid aplica essa closure manualmente em _buildQuery()). MadKanban, MadCalendarComponent e MadDashboard usam 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 num MadKanban) faz o filtro ser silenciosamente ignorado.
  • $dateField obrigatório em date-range/preset.
  • dtIni/dtFim aceitam ambos formatos (display d/m/Y e DB Y-m-d). Trait normaliza antes do SQL via _normalizeDateToDb().
  • Borda inferior de período NUNCA leva hora. applyPeriodoToQuery() gera dt >= 'Y-m-d' pro início e dt <= 'Y-m-d 23:59:59' pro fim — de propósito assimétrico. Em coluna DATE (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/$dtFim fora do helper (lógica custom em onSearch()), replique exatamente esse padrão — não gere >= 'Y-m-d 00:00:00' na mão.
  • modelHasColumn() é fail-open. Se o $model passado pra applyAutoFiltersToQuery()/baseQuery() for null, 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 { ... }