Docs›Componentes (Admin)›mad-grid (MadDataGrid)
Componentes (Admin)

mad-grid (MadDataGrid)

Tabela reativa com search, sort, filter, paginate, export.

O MadDataGrid é o componente de listagem reativa do MAD. Combina PHP server-side (estado, query, ações) com Alpine.js client-side (paginação, sort, filtros). As colunas e ações são declaradas no Blade via tags <mad-grid>, <mad-col>, <mad-act>.

Estrutura básica

Controller (PHP)

<?php
use Mad\Grid\MadDataGrid;
use Mad\Ui\MadToast;
use Mad\Http\MadResponse;

class MinhaListagem extends MadDataGrid
{
    protected static string $wrapper = self::INTERNAL;
    protected string $model      = 'MeuModel';
    protected string $database   = 'minierp';
    protected string $actionSide = 'left';

    protected function view(): string|array
    {
        return ['minha-listagem', ['__component' => $this]];
    }
}

View (Blade)

<mad-page-container>
    <mad-page-header title="Minha Listagem" icon="list" breadcrumb="Modulo > Listagem">
        <actions>
            <mad-btn target="MeuFormulario" variant="primary" icon="plus">Novo</mad-btn>
        </actions>
    </mad-page-header>

    <mad-page-content>
        <mad-grid self per-page="15">
            <mad-columns>
                <mad-col field="id" label="Cod." width="70" center sort />
                <mad-col field="nome" label="Nome" sort filter />
            </mad-columns>
            <mad-actions>
                <mad-act method="onEditar" icon="pencil" label="Editar" />
            </mad-actions>
        </mad-grid>
    </mad-page-content>
</mad-page-container>

Propriedades do Controller

Propriedade Tipo Default Descricao
$model string '' Classe do model Eloquent para auto-query
$database string MAIN_DATABASE Conexao do banco
$perPage int 15 Registros por pagina (prop publica, serializada)
$with array [] Relacoes Eloquent para eager-load na query da pagina (anti-N+1). Transforms que navegam relacao ($row['__record']->cliente->nome) leem do cache do eager load em vez de 1 SELECT por linha
$defaultSort string '' Sort padrao: 'nome ASC' ou 'ano DESC, mes ASC'
$actionSide string 'right' Lado das acoes: 'left' ou 'right'. Convencao do projeto costuma sobrescrever para 'left'
$searchable bool false Barra de busca rapida no grid
$searchColumns array [] Campos para busca global. Suporta notacao ponto: ['nome', 'familia.nome']
$refreshable bool false Botao de refresh na toolbar (chama onReload()). Opt-in
$sticky bool false Thead e quebras de grupo fixos ao rolar
$exportable bool true Botoes de exportacao (CSV, XLSX, PDF)
$exportTitle string '' Titulo da exportacao (header do PDF e nome do sheet do XLSX). Vazio usa $title ou o nome da classe
$exportFilename string '' Nome-base do arquivo exportado (sem extensao). Vazio usa $exportTitle
$rememberFilters bool false Persistir filtros/sort na sessao
$groupBy string/array '' Campo(s) para agrupamento: 'ano' ou ['ano', 'mes']
$groupMask string/array '' Template do grupo: '{ano}' ou ['{ano}', 'Mes {mes}']
$groupTotal bool false Subtotais por grupo

Alem destas, o grid herda periodType, dateField, applyUnitFilter, unitField, skipAutoFilter etc. da MadFiltersTrait (filtro de periodo, multi-tenant por unit e auto-discovery de filtros) — ver /docs/components-admin/grid-filters.

Atributos da tag <mad-grid>

<mad-grid self
    per-page="15"
    searchable
    :search-columns="['nome', 'cod_barras', 'familia_produto.nome']"
    sticky
    refreshable
    no-export
    no-column-chooser
    no-auto-load
    action-side="left"
    :group-by="'ano'"
    :group-mask="'{ano}'"
    group-total
>
Atributo Descricao
model="Pessoa" Classe do model para auto-query (so necessario fora do padrao subclasse/columns())
per-page="15" Registros por pagina
self Grid inline — colunas/acoes declaradas no Blade dentro da subclasse MadDataGrid
searchable Mostra a barra de busca rapida na toolbar
:search-columns="[...]" Campos para a busca (notacao ponto: 'familia.nome')
refreshable Mostra o botao de refresh na toolbar (chama onReload())
sticky Thead e quebras de grupo fixos ao rolar
no-export Desabilita os botoes de exportacao
no-column-chooser Esconde o seletor de colunas (column chooser) da toolbar
no-auto-load Carga adiada ("Carregar registros ao abrir = Nao" do MadBuilder 4): a listagem abre vazia, sem consultar o banco, e so carrega na primeira acao do usuario — Buscar, busca rapida, filtro de coluna, ordenacao ou o botao "Carregar registros" do estado vazio. Indicado para tabelas com milhares de registros. Limpar filtros volta ao estado vazio; exportar antes de carregar mostra um aviso. Ver "Bases grandes" abaixo
action-side="left" Coluna de acoes a esquerda (default: right) — actions-left e o atalho booleano equivalente
:group-by="'ano'" Campo para agrupamento. Aceita caminho de relacao de N niveis (group-by="rubrica->codigo", group-by="cidade->estado->nome") — ver "Quebra por campo de outra tabela". Aceita sufixo de GRANULARIDADE em campo de data: group-by="data_venda|day" (tambem |week, |month, |year) — ver "Quebra por dia/mes/ano". Para multi-nivel via Blade, use string com virgula: group-by="ano,mes" (NAO :group-by="['ano','mes']" — o array e convertido com (string) no _renderInlineGrid() e vira a string literal "Array")
group-by="data_venda|day" Granularidade da CHAVE da quebra num campo de data: day (2026-03-09), week (semana ISO), month (2026-03), year (2026). Sem o sufixo, uma coluna datetime cria um grupo por segundo. Combina com caminho de relacao (venda->data|day) e com multi-nivel (data_venda|day,vendedor_id)
:group-mask="'{ano}'" Template de exibicao do grupo. Aceita {relacao->campo}. Multi-nivel: :group-mask="['{ano}', 'Mes {mes}']"
group-total Subtotais por grupo
group-band="cells" Banda da quebra alinhada as colunas: cada subtotal fica SOB a sua coluna e o rotulo ocupa em colspan as colunas livres ate a primeira totalizada. Default (inline) e o texto corrido de sempre. Neste modo o sticky da quebra e desligado (relatorio usa per-page="0")
group-total-label="Sub-Totais da {group}" Rotulo do subtotal da quebra. Aceita {group} (o label do grupo) e a mesma sintaxe de mascara do group-mask, formatador incluido
row-detail="{descricao} conf. {documento}" Segunda linha descritiva por registro (tela e PDF; CSV/XLSX ignoram). Mesma sintaxe do group-mask — ver "Linha descritiva"
export-title="Analitico — {PERIOD}" Titulo da exportacao. Vence a propriedade $exportTitle do PHP; aceita {PERIOD}
export-subtitle="Exercicio 2026" Alimenta o {SUBTITLE} das bandas do PDF
export-filename="analitico" Nome-base do arquivo exportado (sem extensao); aceita {PERIOD}
card-view / card-default / card-cols="3" Visualizacao em cards — ver secao "Card View"

Bases grandes — no-auto-load

Sem o atributo, a listagem dispara COUNT(*) + SELECT ... LIMIT na abertura, sem nenhum filtro. Em tabelas com milhares de registros isso e lento e inutil, porque o usuario sempre filtra antes. Com no-auto-load:

<mad-grid self per-page="15" no-auto-load>
    <mad-columns>...</mad-columns>
</mad-grid>
  • A tela abre com a dica "Use os filtros e clique em Buscar para carregar os registros" e um botao Carregar registros (chama onReload()), entao mesmo um grid sem <mad-grid-filters> nem searchable tem como carregar.
  • A primeira acao explicita (Buscar/onShow, busca rapida, filtro de coluna, sort, refresh, por pagina) carrega e "arma" o grid: dali em diante paginar e ordenar funcionam normalmente.
  • Limpar filtros (onLimpar / limpar filtros de coluna) volta ao estado vazio em vez de recarregar a tabela inteira.
  • Exportar CSV/Excel/PDF antes de carregar devolve um aviso (o export carrega todas as linhas).
  • Cada abertura da pagina comeca vazia, mesmo com $rememberFilters = true.
  • Subclasse manual que chama parent::mount() pode declarar public bool $autoLoad = false; para pular a query do mount() — o atributo no Blade so e visto na renderizacao. Grids gerados pelo MadBuilder nao precisam: o mount() deles nao chama o pai.
  • No GridBuilder: ->noAutoLoad().

Colunas — <mad-col>

Atributos basicos

<mad-col field="nome" label="Nome" />
<mad-col field="id" label="Cod." width="70" center sort />
<mad-col field="valor" label="Valor" right sort money="R$" total="sum" />
<mad-col field="dt_pedido" label="Data" width="100" center sort date="d/m/Y" />
Atributo Descricao
field Campo do model. Suporta {relacao->campo} para relacionamentos
label Texto do cabecalho
width Largura fixa: "70" (px)
center / right / align="..." Alinhamento
sort (alias sortable) Habilita ordenacao por clique
money="R$" Formata como moeda com prefixo, 2 casas decimais (R$ 1.234,56)
num / number Formata como numero simples (sem prefixo). Casas decimais via valor: number="3"
date="d/m/Y" Formata como data
total="sum" Totaliza no rodape: sum, avg, count, min, max, last (ultimo valor na ordem de exibicao)
total-mask="Total: {value}" Mascara do total no rodape E no subtotal da quebra. {value} e o numero calculado
running Saldo acumulado: a coluna passa a exibir a soma corrente do proprio campo linha a linha — ver "Saldo acumulado"
running="{credito} - {debito}" Saldo acumulado do delta da expressao (mesma DSL do evaluate). O field pode ser sintetico (nao existir na tabela)
running-reset="group" Zera o acumulador na quebra do nivel 0. group:N zera no nivel N; none (default) nunca zera
running-start="1000" Saldo inicial de cada escopo. Aceita numero ou expressao
html Marca o conteudo renderizado (de transform/evaluate) como HTML puro, sem escapar
hide (alias hidden) Coluna oculta

Toda coluna declarada via <mad-col> entra no column chooser por padrao (toggle mostrar/ocultar pelo usuario). Nao ha atributo Blade pra tirar uma coluna do chooser — isso so e possivel declarando colunas em PHP via columns() e chamando GridColumn::make(...)->notHideable().

Campo de relacionamento

{{-- Exibe o nome da familia do produto navegando a relacao Eloquent
     "familia_produto()" do model — resolvido (lazy-load) pelo template
     {relacao->campo} --}}
<mad-col field="{familia_produto->nome}" label="Familia" sort />

Filtro de coluna simples

{{-- Filtro padrao (like, debounced ao digitar) --}}
<mad-col field="obs" label="Obs" filter />

{{-- Filtro select (lista fixa de opcoes) --}}
<mad-col field="status" label="Status" filter="select" :filter-opts="['A' => 'Ativo', 'I' => 'Inativo']" />

{{-- Filtro com operador especifico (token criptografado — col-filter seguro) --}}
<mad-col field="id" label="Cod." col-filter="=" />
<mad-col field="valor" label="Valor" col-filter=">=" />

filter e o modo "live" simples (text/select/date). col-filter="op" ativa o popover com botoes Filtrar/Limpar e gera um token assinado (MadStateCrypt) — o operador nunca chega cru do client. Popover com input de texto padrao, sem dbcombo:

<mad-col field="obs" label="Obs" filter-popover filter-op-select />

Filtro de coluna com dbcombo

<mad-col field="{estado->nome}" label="Status">
    <mad-col-filter op="=" field="estado_id">
        <mad-dbcombo-field name="filter_value" label="Status"
            model="Estado" display="nome" order-by="ordem"
            placeholder="Todos" />
    </mad-col-filter>
</mad-col>

Coluna computada — evaluate

Calcula o valor da celula a partir de outros campos da mesma linha, sem precisar de transform em PHP. Placeholders {campo} (campo simples) ou {rel->campo} (relacionamento); operadores + - * / ( ):

<mad-col field="subtotal" label="Subtotal" money="R$" evaluate="{valor} * {quantidade}" />

<mad-col field="comissao" label="Comissao" money="R$"
    evaluate="{produto->tipo_produto->percentual} * {valor} / 100" />

A expressao roda num avaliador aritmetico restrito (apenas digitos, + - * / ( ) apos resolver os placeholders) — nunca expressao PHP arbitraria. O resultado e calculado em loadData(), antes da normalizacao da linha, e pode ser combinado com money/date/total normalmente.

Edicao inline

A edicao inline tem dois eixos independentes: quando ativa (edit-mode) e que editor abre (edit-type). Default de edit-mode e dblclick.

{{-- Texto — duplo clique (default) --}}
<mad-col field="obs" label="Obs" edit />

{{-- Texto — sempre editavel (sem clique) --}}
<mad-col field="obs" label="Obs" edit edit-type="text" edit-mode="inline" />

{{-- Monetario — clique no icone de lapis --}}
<mad-col field="valor" label="Valor" money="R$" edit edit-type="money" edit-decimals="2" edit-prefix="R$" edit-mode="click" />
edit-mode Comportamento
dblclick (default) Duplo clique na celula abre o editor
click Icone de lapis visivel; clique abre o editor
inline Editor sempre visivel, sem clique

edit-type suportados e seus atributos extras:

edit-type Editor Atributos extras
text (default) input texto —
textarea textarea edit-rows="3"
number input numerico simples edit-decimals="2"
numeric mad-numeric-field (decimal pt-BR formatado) edit-decimals="2" edit-prefix="kg" edit-suffix="un"
money numerico monetario pt-BR edit-decimals="2" edit-prefix="R$"
date input de data (madDatePicker) — (formato interno fixo Y-m-d; so configuravel via PHP fluent GridColumn::editDate($formato))
datetime mad-datetime-field —
select <select> com opcoes fixas :edit-opts="['1' => 'Ativo', '0' => 'Inativo']"
dbcombo mad-dbcombo-field (options carregadas no render) edit-model="Estado" edit-display="nome" edit-key="id" edit-database="..." edit-order-by="nome" :edit-filters="[['ativo','=','1']]"
dbunique-search mad-dbunique-search-field (busca AJAX server-side) mesmos de dbcombo + edit-min-length="2"
color mad-color-field (Pickr) :edit-colors="['#000','#fff']" (paleta opcional)
spinner mad-spinner-field (numero com +/-) edit-min="0" edit-max="100" edit-step="1"
{{-- Select fixo --}}
<mad-col field="status" label="Status" edit edit-type="select"
    :edit-opts="['A' => 'Ativo', 'I' => 'Inativo']" />

{{-- dbcombo — carrega TODAS as opcoes do model no render --}}
<mad-col field="estado_id" label="Estado" edit edit-type="dbcombo"
    edit-model="Estado" edit-display="nome" edit-order-by="nome" />

{{-- dbunique-search — busca AJAX, melhor para tabelas grandes --}}
<mad-col field="cliente_id" label="Cliente" edit edit-type="dbunique-search"
    edit-model="Cliente" edit-display="nome" edit-min-length="2" />

{{-- spinner numerico com limites --}}
<mad-col field="qtd" label="Qtd" edit edit-type="spinner" edit-min="0" edit-max="999" edit-step="1" />

So coluna com edit e gravavel

A gravacao da edicao inline aceita apenas colunas declaradas com edit (ou editable() no PHP fluent). Coluna que aparece na listagem sem edit e recusada, e coluna que a listagem nunca mostrou tambem — a celula volta ao valor do banco e a tentativa fica registrada no log.

Isso vale por listagem: marcar edit na coluna e o que autoriza a escrita daquele campo. Se uma tela precisa gravar um campo que nao deve aparecer para edicao manual, faca no controller (acao ou formulario), nao pela grid.

{{-- gravavel --}}
<mad-col field="obs" label="Obs" edit />

{{-- somente leitura: a edicao inline nao grava esta coluna --}}
<mad-col field="criado_em" label="Criado em" />

Toda edicao inline salva via MadDataGrid::onInlineSave(int $id, string $field, mixed $value) (metodo built-in, nao precisa implementar) — grava o campo no $model configurado e atualiza so a linha (manageRow, sem full re-render).

Transform e display condition

{{-- Transform: formata o valor exibido --}}
<mad-col field="obs" label="Obs" transform="MinhaClasse::formatarObs" />

{{-- Display condition: mostra/esconde a coluna inteira --}}
<mad-col field="obs_comercial" label="Obs. Comercial"
    display-condition="MinhaClasse::mostrarObsComercial" />

No PHP:

// Transform de COLUNA — assinatura completa: ($value, $object, $row, $column, $lastRow)
//   $value   = valor cru da celula
//   $object  = stdClass (cast do row array)  ← NUNCA tipar como array
//   $row     = array associativo da linha
//   $column  = GridColumn instance (opcional)
//   $lastRow = stdClass da linha anterior ou null (opcional)
//
// IMPORTANTE: o 2o argumento e stdClass, NAO array.
// Usar `object $row` ou omitir o type-hint. NUNCA usar `array $row`.
public static function formatarObs(mixed $value, object $row): string
{
    return $value ? mb_strimwidth((string)$value, 0, 60, '...') : '-';
}

// Se precisar acessar como array, use o 3o argumento:
public static function formatarComArray(mixed $value, object $obj, array $row): string
{
    return ($row['nome'] ?? '') . ': ' . (string)$value;
}

// Display condition: sem params → bool (mostra/esconde coluna)
public static function mostrarObsComercial(): bool
{
    return true; // logica de permissao
}

NUNCA tipar 2o argumento de transform como array

// ERRADO: TypeError — GridColumn passa (object) $row, nao array
public static function meuTransform(mixed $value, array $row): string { ... }

// CERTO: stdClass
public static function meuTransform(mixed $value, object $row): string
{
    $nome = $row->nome ?? '';   // acesso via ->
    return "{$nome}: {$value}";
}

// CERTO: se precisa de array, use o 3o parametro
public static function meuTransform(mixed $value, object $obj, array $row): string
{
    $nome = $row['nome'] ?? ''; // acesso via []
    return "{$nome}: {$value}";
}

Badge (status colorido)

<mad-col field="status" label="Status"
    badge="A:success:Ativo|I:danger:Inativo|P:warning:Pendente" />

{{-- Ou via expressao PHP --}}
<mad-col field="status" label="Status" :badge="$badgeMap" />

Acoes — <mad-act>

Acao simples

<mad-act method="onEditar" icon="pencil" label="Editar" />
<mad-act method="onExcluir" icon="trash-2" label="Excluir" danger confirm="Tem certeza?" />

{{-- mad:click e um alias de method= (mesmo efeito, sintaxe MadWire) --}}
<mad-act mad:click="onEditar" icon="pencil" label="Editar" />

O target do <mad-nav> aceita 3 formatos. navigate="..." e um alias exato de target="..." (mesmo parsing) — use o nome que ler melhor:

{{-- 1. Classe apenas — abre via show() --}}
<mad-nav icon="pencil" label="Editar" target="MeuForm" />

{{-- 2. Classe::metodo({campo}) — chama metodo com valor do registro --}}
<mad-nav icon="pencil" label="Editar" target="MeuForm::onEdit({id})" />

{{-- 3. Classe::metodo({campo1},{campo2}) — multiplos parametros --}}
<mad-nav icon="eye" label="Ver" target="MeuDetalhe::onShow({id},{tipo})" />
Formato URL gerada Quando usar
MeuForm class=MeuForm&method=show&id={id} Formulario simples, ID como key
MeuForm::onEdit({id}) class=MeuForm&method=onEdit&id={id} Formulario com metodo especifico
MeuForm::onEdit({id},{tipo}) class=MeuForm&method=onEdit&id={id}&tipo={tipo} Metodo com multiplos params

Os {campo} entre chaves sao substituidos pelo valor da coluna do registro na linha.

REGRA CRITICA: o nome do parametro PHP DEVE ser identico ao nome do {campo} no target. O MadHandler resolve argumentos por nome ($data['campo']). Se o nome nao bater, o handler nao encontra a key e passa o $data inteiro como array, causando TypeError.

{{-- target envia key "id" --}}
<mad-nav target="MeuForm::onEdit({id})" />

{{-- target envia key "sessionid" (nome exato do campo do model) --}}
<mad-nav target="SqlLogList::filterSession({sessionid})" />

{{-- target envia keys "id" e "tipo" --}}
<mad-nav target="Detalhe::onShow({id},{tipo})" />
// CERTO: parametro $id bate com {id}
public function onEdit(int $id): void { ... }

// CERTO: parametro $sessionid bate com {sessionid}
public function filterSession(string $sessionid): void { ... }

// ERRADO: $sessionId (camelCase) != {sessionid} (lowercase) → TypeError
public function filterSession(string $sessionId): void { ... }

// ERRADO: $session_id (snake_case) != {sessionid} → TypeError
public function filterSession(string $session_id): void { ... }
{{-- Exemplo real: abrir edicao de pedido --}}
<mad-nav icon="pencil" label="Editar" target="PedidoVendaForm::onEdit({id})" />

{{-- Abrir como drawer --}}
<mad-nav icon="eye" label="Ver" target="MeuForm::onEdit({id})" drawer />

Para parametros nomeados via expressao PHP (em vez do {campo} inline), use :params — tem prioridade sobre os params extraidos do target/navigate:

<mad-nav icon="file" label="Relatorio" target="RelatorioForm"
    :params="['pedido_id' => '{id}', 'modo' => 'visualizar']" />

Row-attach — form anexado a linha (row)

O atributo row no <mad-nav> faz o form abrir anexado a linha clicada — uma <tr class="mad-dg-attach-row"> extra inserida logo abaixo, com o form dentro (<td colspan>), estilo "quick edit". O usuario edita sem perder o contexto visual da listagem.

{{-- Mesmo form, apresentacao diferente: quem decide e o chamador --}}
<mad-nav icon="pencil" label="Editar" target="UserForm::onEdit({id})" row />

Precedencia dos atributos de apresentacao em GridAction::getNavAttr(): row > drawer > auto-detect (o $wrapper declarado no form).

Nenhuma mudanca e necessaria na classe do form — nem no onSave. O compilador (MadGridCompiler) emite navRow => true, a action vira onclick="Mad.rowAttach(this, 'UserForm@onEdit', {id:2}, '/url/amigavel')" e o client faz um GET com os headers X-Mad-Partial: 1 + X-Mad-Row-Attach: 1. Esse ultimo header faz o servidor devolver o componente cru, sem o chrome de drawer/modal.

O closeDrawer() do onSave continua valendo: a op close_overlay e interceptada no client — se o wrapper esta dentro de tr.mad-dg-attach-row, remove a <tr>; senao dispara o evento de drawer normal.

return (new MadResponse())
    ->toast(__('admin.user_saved'), 'success')
    ->closeDrawer()                            // fecha a attach-row
    ->manageRow($user->id, UserList::class);   // atualiza a linha

Na view do form da pra adaptar o layout ao contexto com MadComponent::isRowAttach(): bool — o flag e detectado no show() pelo header e persistido no mad_state, entao vale tambem nos re-renders do MadWire (ex.: erro de validacao):

<mad-form submit="onSave">
    <mad-form-section :title="__('admin.user_data')" icon="user">
        {{-- campos principais, sempre visiveis --}}
    </mad-form-section>

    @if(!$that->isRowAttach())
        {{-- pesado/secundario: so na cortina ou pagina cheia --}}
        <mad-tabs> ... </mad-tabs>
    @endif

    <mad-btn variant="primary" type="submit" icon="save">Salvar</mad-btn>
</mad-form>

Campos que ficaram fora da view compacta nao se perdem: os valores seguem no state do form e sao re-gravados intactos no save.

Situacao Comportamento
Clicar de novo na mesma linha toggle — fecha
Abrir outra linha single-open — fecha a anterior
Botao X do chrome fecha client-side, sem round-trip
Erro de validacao attach permanece aberta e compacta
Card-view (sem <tr>) fallback: abre drawer normal (Mad.get)
Sort/filtro/paginacao/busca re-render da grid descarta a attach-row

NUNCA usar mad-act para navegar para outro formulario

{{-- ERRADO: mad-act chama metodo do PROPRIO controller da listagem --}}
<mad-act method="onEditar" icon="pencil" label="Editar" />
{{-- Isso exige um metodo onEditar() no controller da listagem --}}

{{-- CERTO: mad-nav navega direto para o formulario destino --}}
<mad-nav icon="pencil" label="Editar" target="MeuForm::onEdit({id})" />
{{-- Nenhum metodo necessario no controller da listagem --}}

Quando usar mad-act vs mad-nav

Preciso... Usar
Abrir outro formulario/pagina <mad-nav target="Classe::metodo({id})">
Executar acao na propria listagem (aprovar, cancelar, excluir) <mad-act method="onMetodo">
Abrir formulario em drawer <mad-nav target="Classe::onEdit({id})" drawer>
Abrir formulario anexado a linha (quick edit) <mad-nav target="Classe::onEdit({id})" row>

Grupo de acoes (dropdown)

<mad-action-group icon="more-horizontal" label="Acoes">
    <mad-act method="onAprovar" icon="check-circle" label="Aprovar" primary
        display-condition="MinhaClasse::podeAprovar"
        confirm="Confirmar aprovacao?" />

    <mad-act method="onCancelar" icon="ban" label="Cancelar" danger
        confirm="Tem certeza?"
        when-field="status_id" when-nin="8,9,10" />
</mad-action-group>

<mad-action-group> so aceita <mad-act>/<mad-nav> como filhos diretos — <mad-del> nao e reconhecido dentro do grupo.

Confirmacao: confirm vs confirm-popover

Ambos <mad-act> e <mad-nav> aceitam as duas variantes — disparam antes da acao/navegacao executar:

{{-- confirm: dialog modal centralizado --}}
<mad-act method="onExcluir" icon="trash-2" label="Excluir" danger confirm="Excluir este registro?" />

{{-- confirm-popover: popover ancorado no proprio botao (nao desloca o foco) --}}
<mad-act method="onExcluir" icon="trash-2" label="Excluir" danger confirm-popover="Excluir?" />

Use confirm-popover para acoes de linha (lista densa, exclusao rapida) e confirm para acoes com mais peso/contexto.

Exclusao embutida — <mad-del>

Atalho que gera um botao de exclusao sem precisar declarar <mad-act> + handler. Compila para method: 'onMadGridDelete':

<mad-del confirm="Excluir este registro?" />
<mad-del confirm="Excluir?" icon="trash" label="Remover" />

onMadGridDelete(int $id) ja vem implementado no MadGrid (modo zero-PHP via MadGrid::of() — ver secao final). Numa subclasse de MadDataGrid com <mad-grid self>, implemente onMadGridDelete(int $id) voce mesmo (ou prefira <mad-act method="onExcluir"> com seu proprio metodo) — sem o metodo publico definido na classe, o handler rejeita a chamada (method_exists falha) e o clique nao executa nada.

Acoes condicionais — @if / @elseif / @else

Dentro de <mad-actions> (ou direto no corpo do <mad-grid>), <mad-act>, <mad-nav> e <mad-del> podem ser envolvidos em blocos Blade @if/@elseif/@else — a condicao e uma expressao PHP normal do controller (nao recebe a linha; use when-field/display-condition para condicao por registro):

<mad-actions>
    @if(auth()->user()->can('pedidos.aprovar'))
        <mad-act method="onAprovar" icon="check" label="Aprovar" primary />
    @endif

    @if($this->modoCompacto)
        <mad-action-group icon="more-horizontal" label="Acoes">
            <mad-act method="onEditar" icon="pencil" label="Editar" />
            <mad-del confirm="Excluir?" />
        </mad-action-group>
    @else
        <mad-nav icon="pencil" label="Editar" target="PedidoForm::onEdit({id})" />
        <mad-del confirm="Excluir?" />
    @endif
</mad-actions>

Condicoes de exibicao de acoes

{{-- Via callable estatico --}}
<mad-act method="onAprovar" display-condition="MinhaClasse::podeAprovar" />

{{-- Via campo do registro: when-value (igualdade) / when-in / when-nin --}}
<mad-act method="onCancelar" when-field="status_id" when-nin="8,9,10" />
<mad-act method="onReabrir" when-field="status_id" when-value="9" />
<mad-act method="onEnviar" when-field="status_id" when-in="1,2,3" />

{{-- Operador customizado (default: eq) --}}
<mad-act method="onArquivar" when-field="dias_parado" when-op=">=" when-value="30" />
Atributo Efeito
when-field="campo" + when-value="x" Exibe se campo == x (operador default eq, customizavel via when-op)
when-field="campo" + when-in="1,2,3" Exibe se campo esta na lista
when-field="campo" + when-nin="1,2,3" Exibe se campo NAO esta na lista
when-op="..." Operador para o par when-field/when-value: eq (default), neq

O mesmo conjunto de atributos existe com o prefixo disabled- (disabled-field, disabled-value, disabled-op, disabled-in, disabled-nin) — controla se o botao aparece desabilitado (cinza, sem clique) em vez de oculto:

{{-- Botao visivel, mas desabilitado enquanto o pedido nao foi aprovado --}}
<mad-act method="onFaturar" icon="receipt" label="Faturar"
    disabled-field="status_id" disabled-nin="3,4" />

No PHP:

// Display condition de ACAO: recebe array (diferente do transform de COLUNA que recebe object)
public static function podeAprovar(array $row): bool
{
    return in_array((string)($row['status_id'] ?? ''), ['1', '2', '3'], true);
}

Transform de acao (muda label/icon dinamicamente)

<mad-nav icon="pencil" label="Editar" target="MeuForm"
    transform="MinhaClasse::transformarAcao" />
// Transform de ACAO: recebe array (diferente do transform de COLUNA que recebe object)
public static function transformarAcao(array $row): array
{
    if ($row['finalizado'] ?? false) {
        return ['label' => 'Visualizar', 'icon' => 'eye'];
    }
    return ['label' => 'Editar', 'icon' => 'pencil'];
}

Resumo de assinaturas — coluna vs acao

Tipo Callback 1o arg 2o arg Retorno
Transform de coluna transform="Classe::metodo" mixed $value object $row (stdClass) string
Transform de acao transform="Classe::metodo" array $row — array (label, icon, etc)
Display condition de coluna display-condition="Classe::metodo" — — bool
Display condition de acao display-condition="Classe::metodo" array $row — bool

Busca customizada — onSearch()

Para filtros avancados (form com campos de busca), sobrescreva onSearch(). O hook monta uma closure em $this->searchQuery que recebe o Eloquent Builder da query principal — o grid aplica ela automaticamente em _buildQuery() (soft delete ja e tratado pelo trait SoftDeletes do model, nao precisa filtrar deleted_at manualmente):

use Illuminate\Database\Eloquent\Builder;

public string $busca        = '';  // props publicas = serializadas no state
public string $statusFiltro = '';
public string $dtIni        = '';
public string $dtFim        = '';

public function onSearch(): void
{
    $this->searchQuery = function (Builder $q) {
        if (!empty($this->busca)) {
            $v = trim($this->busca);
            if (is_numeric($v)) {
                $q->where('id', '=', (int) $v);
            } else {
                $q->where('nome', 'like', "%{$v}%");
            }
        }

        if (!empty($this->statusFiltro)) {
            $q->where('estado_id', '=', $this->statusFiltro);
        }

        if (!empty($this->dtIni)) {
            $q->where('dt_pedido', '>=', $this->dtIni);
        }
    };
}

No Blade, o form de busca usa submit="onReload":

<mad-form submit="onReload">
    <mad-form-grid :cols="2">
        <mad-input-field name="busca" label="Busca rapida" placeholder="Nome ou codigo..." />
        <mad-dbcombo-field name="statusFiltro" label="Status"
            model="Estado" display="nome" placeholder="Todos" />
    </mad-form-grid>
    <div style="display:flex;gap:8px;margin-top:16px;">
        <mad-btn type="submit" variant="primary" icon="search">Buscar</mad-btn>
        <mad-btn variant="ghost" icon="x-circle" mad:click="onLimpar">Limpar</mad-btn>
    </div>
</mad-form>

Busca rapida embutida (searchable)

Para busca simples client→server no grid, sem form customizado:

<mad-grid self searchable :search-columns="['nome', 'cod_barras', 'familia_produto.nome']">
  • searchable — mostra input de busca na toolbar do grid
  • search-columns — campos para busca (notacao ponto gera subselect automatico)
  • Sem search-columns, busca em todos os campos de texto do model

Exportacao (CSV, XLSX, PDF)

Habilitada por padrao. Botao de download aparece na toolbar.

// Desabilitar
protected bool $exportable = false;
// Ou na tag: <mad-grid self no-export>

// Titulo customizado (header do PDF + nome do sheet do XLSX)
protected string $exportTitle = 'Relatorio de Pedidos';

// Nome-base do arquivo exportado (sem extensao). Vazio usa $exportTitle.
// Ex: 'relatorio-pedidos' → relatorio-pedidos.csv / .xlsx / .pdf
protected string $exportFilename = 'relatorio-pedidos';

// Header PDF customizado (placeholders: {TITLE}, {DATE}, {TOTAL})
protected function exportPdfHeader(): string
{
    return '<div style="display:flex;justify-content:space-between;">'
         . '  <div style="font-size:16px;font-weight:bold;">Empresa XYZ</div>'
         . '  <div style="font-size:8px;color:#888;">Gerado em {DATE} | {TOTAL} registros</div>'
         . '</div>';
}

// Footer PDF customizado (placeholders: {TITLE}, {DATE})
// A paginacao (1/20) e adicionada automaticamente a direita
protected function exportPdfFooter(): string
{
    return 'Confidencial | {DATE}';
}

Os atributos export-title, export-subtitle e export-filename fazem o mesmo direto na tag, e vencem as propriedades PHP (o $exportTitle protegido nao sobrevive ao AJAX de exportacao). Os tres aceitam {PERIOD}:

<mad-grid self per-page="0" exportable
          export-title="Analitico Orcamentario — {PERIOD}"
          export-subtitle="Exercicio 2026"
          export-filename="analitico">

Placeholders das bandas do PDF

Alem de {TITLE}, {DATE}, {TOTAL_REGISTER}, {APP_NAME}, {LOGO}, {LOGO_SMALL}, {PAGE_NUM} e {PAGE_COUNT}, as bandas aceitam:

Token Conteudo
{PERIOD} Periodo escolhido nos filtros, ja formatado ("de 01/01/2026 a 31/01/2026", o mes/ano, ou o rotulo do preset com o intervalo)
{FILTERS} Filtros ativos como texto ("Vendedor: Joao · Situacao: Aberto")
{SUBTITLE} O export-subtitle da tag
{UNIT_NAME} Nome da unidade (filial) ativa do usuario logado. Vazio em app sem multi-unidade ou usuario sem unidade (fw 5.79)
{USER_NAME} Nome do usuario que exportou (fw 5.79)
{TENANT_NAME} Nome da empresa/tenant do usuario. Vazio em app sem tenancy (fw 5.79)

Placeholders proprios do app (fw 5.80)

Qualquer dado do app pode virar token nas bandas — CNPJ, endereco, nome fantasia. Tres camadas, da mais simples a mais flexivel:

  1. Valor fixo, sem codigo — no MadBuilder, Configuracoes do projeto → Exportacao de PDF → Campos proprios. Vai na chave "placeholders" do app/config/pdf-export.json ({"CNPJ": "12.345.678/0001-90"}) e vira chip Campos do projeto no editor de bandas.
  2. Valor calculado, todas as telas — classe App\Helpers\PdfExportPlaceholders (o MadBuilder cria o esqueleto pelo botao Criar classe de campos; e um mad_code do tipo Helper). App\Support\PdfExportPlaceholders, da 5.79, continua aceita.
// app/Helpers/PdfExportPlaceholders.php
namespace App\Helpers;

final class PdfExportPlaceholders
{
    public static function resolve(): array
    {
        return ['RESPONSAVEL' => (string) session('username')];
    }
}
  1. Valor de uma tela — metodo na pagina (fora dos blocos @mad-block). Roda na requisicao do export, entao ve o que a pessoa filtrou e quem esta logado. Cada campo do <mad-grid-filters> e uma propriedade PUBLICA com o mesmo name (name="contrato" → $this->contrato; combo guarda o id):
protected function exportPdfPlaceholders(): array
{
    $financiador = $this->idfinanciador !== ''
        ? \App\Models\Financiador::find($this->idfinanciador)?->nome
        : null;

    return [
        'CONTRATO'    => $this->contrato ?: 'todos',                    // texto da busca
        'FINANCIADOR' => $financiador ?? 'Todos',                       // combo → nome
        'PERIODO'     => $this->reportPeriodLabel() ?: 'todo o periodo', // mesmo texto do {PERIOD}
        'EMAIL'       => (string) session('usermail'),                  // sessao de quem exporta
    ];
}

Periodo: $this->dtIni/$this->dtFim ou $this->mes/$this->ano; currentFilters() devolve os preenchidos (junto com outras props publicas da grade, como sortDir e exportColConfigs — leia so as chaves que interessam). Sessao: username, usermail, login, userunitname, userunitid (nome/unidade/empresa ja sao tokens prontos). So propriedades publicas sobrevivem ao AJAX do export — recalcule o resto dentro do metodo. Chave com ou sem chaves.

No editor, campo calculado entra como elemento de texto com o token ({RESPONSAVEL}); o runtime substitui.

Regras:

  • Precedencia: pagina > classe > valores fixos do json > tokens de texto do framework. Sobrescrever {APP_NAME} ou {DATE} e permitido (razao social, outro formato).
  • {LOGO}, {LOGO_SMALL}, {PAGE_NUM} e {PAGE_COUNT} sao estruturais e nao podem ser sobrescritos — a chave e descartada.
  • Valores sao texto puro: escapados na banda, nunca viram HTML.
  • Chave precisa ser identificador ([A-Za-z_][A-Za-z0-9_]*); vazia, com espaco ou de lista (['CNPJ']) e ignorada.
  • O metodo roda so no export PDF, dentro do AJAX do export: apenas props publicas da pagina round-tripam — recalcule o que precisar.
  • Se a classe global lancar, o export segue sem ela; excecao no metodo da pagina sobe normalmente (e codigo seu).

{FILTERS} depende de um bloco novo emitido pelo compilador de <mad-*-filters>: em app ja publicado, rode php artisan view:clear (ou republique o projeto) para as views recompilarem. Sem isso o token sai vazio — nunca quebra.

As cores das faixas de quebra e de total do PDF saem de MadGridExporter::PDF_PALETTE e podem ser trocadas por projeto pela chave "palette" do app/config/pdf-export.json (so valores que parecam cor CSS sao aceitos).

Orientacao do papel e linha do cabecalho (fw 5.103)

O PDF sai em paisagem por padrao. A orientacao e a cor da linha abaixo do cabecalho padrao vem da pagina ou do projeto, nesta ordem (pagina > projeto > padrao):

// pagina — bloco gerado pelo MadBuilder (@mad-block:pdf-export-bands)
protected function exportPdfBands(): ?array
{
    return [
        'header' => ['mode' => 'inherit'],
        'footer' => ['mode' => 'inherit'],
        'orientation' => 'portrait',                 // 'portrait' | 'landscape'
        'palette' => ['headerRule' => '#e3e8ef'],    // cor CSS ou 'none'
    ];
}
// projeto — app/config/pdf-export.json
{ "version": 1, "orientation": "portrait", "palette": { "headerRule": "none" } }
Chave Valores Default Descricao
orientation portrait, landscape landscape Papel A4 em pe (194mm uteis) ou deitado (281mm uteis). Valor invalido e ignorado
palette.headerRule cor CSS ou none #2563eb Linha abaixo do cabecalho padrao. none/transparent tiram a linha. Nao afeta cabecalho custom

Em retrato, colunas que nao cabem nos 194mm ficam cortadas: use para relatorios com poucas colunas. A palette da pagina aceita as mesmas chaves da do projeto (quebras e totais tambem) e vale para os dois motores do PDF.

O clique no botao de exportacao nao baixa o arquivo direto — abre um dialog "Exportacao concluida" com nome/tamanho do arquivo e um botao Baixar (rota autenticada mad.grid.export). Evita bloqueio de popup e da ao usuario controle sobre quando iniciar o download.

Agrupamento

// Campo unico
protected string $groupBy   = 'ano';
protected string $groupMask = 'Ano {ano}';

// Multi-nivel — via propriedade PHP da subclasse
protected string|array $groupBy   = ['ano', 'mes'];
protected string|array $groupMask = ['{ano}', 'Mes {mes}'];
protected bool         $groupTotal = true;  // subtotais por grupo

Em <mad-grid self> (Blade), o equivalente multi-nivel usa string com virgula no group-by (NAO array — ver tabela de atributos do <mad-grid> acima):

<mad-grid self group-by="ano,mes" :group-mask="['{ano}', 'Mes {mes}']" group-total>

Os agrupamentos aparecem automaticamente na exportacao PDF/XLSX/CSV.

Quebra por dia/mes/ano — group-by="data_venda|day"

Num campo date/datetime, a quebra sem sufixo usa o valor EXATO como chave — duas vendas do mesmo dia em horarios diferentes viram DUAS bandas. O sufixo de granularidade diz como o valor cru vira a chave do grupo:

Sufixo Chave Rotulo default (pt-BR)
|day 2026-03-09 09/03/2026
|week 2026-W11 (semana ISO) Semana 11/2026
|month 2026-03 Marco/2026
|year 2026 2026
<mad-grid self model="Venda" per-page="0" exportable
          group-by="data_venda|day"
          order-by="data_venda asc"
          group-total>
  • Sem group-mask o rotulo ja sai pronto, no formato de exibicao do locale (mad.tempo.date_format — o mesmo do {PERIOD} das bandas do PDF). O mes usa o nome do mes do idioma ativo, igual ao filtro de mes/ano.
  • Com group-mask, o formatador de sempre continua valendo (group-mask="Dia: {data_venda|date}") e os tokens {campo|day}, {campo|month}, {campo|year} e {campo|week} repetem o rotulo default.
  • Combina com caminho de relacao e com multi-nivel:
<mad-grid self group-by="venda->data|day" group-mask="Dia: {venda->data|day}">
<mad-grid self group-by="data_venda|day,vendedor_id" group-total>
  • A chave granular vale nos SEIS caminhos: tela, sub-totais (group-total), saldo acumulado (running-reset="group" zera quando muda o DIA), PDF, XLSX e CSV. <mad-data-table group-by="data|month"> segue a mesma regra.
  • Declare a ordenacao pelo campo BASE (order-by="data_venda asc"). Nada e injetado automaticamente; sem ordenacao os grupos saem na ordem que o banco devolver e o mesmo dia pode aparecer em duas bandas (o runtime avisa no log). order-by="data_venda|day asc" tambem funciona — o sufixo e descartado e a ordenacao usa o campo base.
  • Campo vazio (data nula) cai num balde de chave vazia, com rotulo em branco — nao some da listagem.
  • Sufixo desconhecido (|quinzena) e IGNORADO com aviso no log e a quebra usa o campo sem granularidade, em vez de jogar tudo num grupo em branco.

Quebra por campo de outra tabela — group-by="relacao->campo"

O group-by aceita caminho de relacao (belongsTo), com quantos niveis forem necessarios. O group-mask pode citar o mesmo caminho:

<mad-grid self model="Lancamento" per-page="0" exportable
          group-by="rubrica->codigo"
          group-mask="Rubrica: {rubrica->codigo}"
          order-by="rubrica->codigo asc, data asc"
          group-total>

Multi-nivel funciona igual ao de campo proprio — string com virgula no Blade:

<mad-grid self group-by="cidade->estado->nome,cidade->nome"
          :group-mask="['UF: {cidade->estado->nome}', 'Cidade: {cidade->nome}']"
          group-total>
  • O valor da relacao e resolvido UMA vez no carregamento e vale na tela, nos subtotais, no saldo acumulado (running-reset="group"), no PDF, no XLSX e no CSV — os seis leem o mesmo dado.
  • As relacoes do caminho entram automaticamente no with() da consulta: 50 linhas custam uma consulta a mais, nao 50.
  • Declare a ordenacao (order-by="rubrica->codigo asc"). Sem ela os grupos saem na ordem que o banco devolver e a mesma rubrica pode aparecer em duas bandas. O runtime avisa no log quando isso acontece.
  • Caminho que o model nao tem como resolver (relacao inexistente) e IGNORADO com aviso no log — em vez de jogar todos os registros num grupo em branco.
  • Coluna tambem aceita o caminho sem chaves: <mad-col field="cidade->nome"> e equivalente a field="{cidade->nome}".

Ordenacao por campo de outra tabela — order-by="relacao->campo"

order-by (e o defaultSort da subclasse) aceitam caminho de relacao, isolado ou misturado com campos da propria tabela:

<mad-grid self model="Venda" order-by="cidade->estado->nome asc, data desc">
  • Vira subquery correlacionada na tabela relacionada (belongsTo, N niveis).
  • Caminho sem ORDER BY possivel — relacao que nao e belongsTo, ou tabela em OUTRA conexao (db-fk) — e ignorado com aviso no log: a tela abre sem essa ordenacao em vez de estourar.
  • O clique no cabecalho de uma coluna sortable com caminho de relacao segue a mesma regra.

Banda da quebra alinhada as colunas — group-band="cells"

No modo padrao (inline) a quebra e uma faixa de texto corrido com o rotulo e os subtotais em sequencia. Em relatorio tabular isso desalinha da grade:

<mad-grid self per-page="0" exportable
          group-by="rubrica" :group-mask="'{rubrica}'" group-total
          group-band="cells" group-total-label="Sub-Totais da {group}">
  • Cada subtotal fica SOB a sua coluna; o rotulo ocupa em colspan as colunas livres a esquerda, ate a primeira coluna totalizada.
  • Quando a PRIMEIRA coluna ja e totalizada nao sobra espaco para o colspan e a banda cai no modo inline — o valor nunca e engolido pelo rotulo.
  • O sticky da quebra e desligado neste modo (o proxy flutuante tem colspan fixo e desalinharia o cabecalho). Relatorio usa per-page="0" para imprimir.
  • group-total-label aceita {group} e a mesma sintaxe do group-mask, inclusive formatador: group-total-label="Total {group} ({valor|money})".

Linha descritiva por registro — row-detail

Segunda <tr> abaixo de cada linha, em largura total, para o texto que nao cabe numa coluna:

<mad-grid self per-page="0" row-detail="{descricao} conf. {documento}">
  • Mesma sintaxe de mascara do group-mask: {campo}, {relacao->campo} e formatador {valor|money}. Token desconhecido fica cru ({foo}) em vez de sumir, para o erro aparecer.
  • A mascara e resolvida UMA vez, no carregamento, e vale na tela e no PDF.
  • CSV e XLSX ignoram: uma linha a mais quebraria tabela dinamica e importacao. Cada registro continua com uma linha nesses formatos.

Saldo acumulado — running

Coluna que acumula linha a linha (razao contabil, extrato, saldo do periodo):

{{-- Acumula o proprio campo --}}
<mad-col field="valor" label="Acumulado" money="R$" right running />

{{-- Acumula o delta de uma expressao; `saldo` nao precisa existir na tabela --}}
<mad-col field="saldo" label="Saldo" money="R$" right
         running="{credito} - {debito}" running-reset="group" total="sum" />
  • running nua acumula o valor da propria coluna (depois do evaluate); com valor, acumula o resultado da expressao — mesma DSL e mesmo sanitizador do evaluate, inclusive {relacao->campo}.
  • running-reset="group" zera na quebra do nivel 0; group:1 zera no nivel 1; none (default) corre do inicio ao fim. running-start semeia o saldo inicial de cada escopo.
  • O valor e materializado na linha, entao tela, subtotais, total geral, CSV, XLSX e PDF mostram o mesmo numero.
  • Com group-by, as linhas sao reordenadas para a ordem de exibicao — sem isso o saldo seguiria a ordem da query e divergiria da tela.
  • total="sum" numa coluna running devolve o ULTIMO saldo, nao a soma dos saldos (que nao significa nada). Use total="last" em qualquer coluna para o mesmo efeito.
  • Com per-page maior que zero o saldo acumula dentro da PAGINA (fica um aviso no log). Relatorio usa per-page="0".
  • Declare a ordenacao (order-by="data asc"): sem ela o saldo segue a ordem que o banco devolver.
  • No GridBuilder: ->running('{credito} - {debito}', 'group').

Acoes no controller

// Acao simples (retorna toast)
public function onAprovar(int $id): mixed
{
    // logica...
    return MadToast::success("Aprovado #{$id}");
}

// Acao com confirmacao (definida no Blade via confirm="...")
public function onExcluir(int $id): mixed
{
    MeuModel::find($id)?->delete();
    $this->loadData();
    return MadToast::success("Excluido");
}

// Abrir formulario
public function onEditar(int $id): MadResponse
{
    return MadResponse::open('MeuForm', ['id' => $id]);
}

// Limpar filtros
public function onLimpar(): void
{
    $this->busca   = '';
    $this->filters = [];
    $this->page    = 1;
    $this->loadData();
}

Query customizada

Para queries complexas (joins, subqueries), sobrescreva query() — sem argumentos, monte e execute o Builder Eloquent voce mesmo e retorne ['items' => [...], 'total' => int]. Retornar array vazio sinaliza pro grid usar a auto-query builder-native a partir de $this->model:

protected function query(): array
{
    $q = MeuModel::query()
        ->join('cliente', 'cliente.id', '=', 'meu_model.cliente_id')
        ->where('meu_model.ativo', '=', true);

    $total = (clone $q)->count();
    $items = $q->orderBy('meu_model.id', 'desc')
        ->forPage($this->page, $this->perPage)
        ->get();

    return ['items' => $items->all(), 'total' => $total];
}

Eager loading de relacoes — $with

Colunas com {relacao->campo}, transform/evaluate que navegam relacao, ou acoes que leem $row['__record']->relacao disparam 1 SELECT por linha (N+1) se a relacao nao foi eager-loaded. Declare $with na auto-query (builder-native, usado apenas quando query() nao foi sobrescrito):

protected array $with = ['cliente', 'itens.produto'];

Se voce sobrescreveu query(), aplique o ->with(...) voce mesmo no Builder — $with so e consumido pelo caminho _autoQuery()/_runQuery() automatico.

Metric cards (indicadores)

<mad-db-metric-card
    model="PedidoVenda"
    field="valor_total"
    total="sum"
    :query="$this->baseQuery(\App\Models\PedidoVenda::class)"
    label="Valor total"
    icon="circle-dollar-sign"
    variant="success"
    format="money:R$" />

baseQuery() (de MadFiltersTrait, usado por MadDataGrid) devolve um Builder Eloquent ja com periodo/unit/auto-filters aplicados — reaproveita exatamente o mesmo filtro ativo na listagem para o card.

Filtros declarativos — <mad-grid-filters> (recomendado)

Prefira o componente declarativo para mais de 1-2 filtros. Renderers prontos: toolbar, chips, drawer, modal, form, sidebar. Auto-discovery de props publicas + chips de filtros ativos + clear-all/individual + persistencia de sessao out of the box.

<mad-grid-filters style="toolbar">
    <mad-input-field name="busca" label="Busca" placeholder="ID 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="Ate" />
</mad-grid-filters>

<mad-grid self per-page="15">...</mad-grid>
use Illuminate\Database\Eloquent\Builder;

class PedidoList extends MadDataGrid
{
    protected string $model           = 'Pedido';
    protected bool   $rememberFilters = true;
    protected string $periodType      = 'date-range';
    protected string $dateField       = 'dt_pedido';
    protected array  $skipAutoFilter  = ['busca'];

    public string $busca     = '';
    public string $status_id = '';   // nome = coluna → auto-filter

    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}%");
            }
        };
    }
}

Doc completa: <mad-grid-filters>.

Drawer de filtro custom (sem <mad-grid-filters>)

Para casos onde voce precisa de logica de form completamente custom:

<mad-btn variant="outline" icon="sliders-horizontal"
    open-drawer="filtros-avancados">Filtro Avancado</mad-btn>

<mad-drawer name="filtros-avancados" title="Filtro Avancado" size="md">
    <mad-form submit="onReload">
        <mad-form-grid :cols="1">
            <mad-date-field name="dtIni" label="Data inicial" />
            <mad-date-field name="dtFim" label="Data final" />
        </mad-form-grid>
        <mad-form-actions>
            <mad-btn type="submit" variant="primary" icon="search">Aplicar</mad-btn>
            <mad-btn variant="ghost" icon="x-circle" mad:click="onLimpar">Limpar</mad-btn>
        </mad-form-actions>
    </mad-form>
</mad-drawer>

Drawer dinamico via MadResponse — conteudo injetado por acao

Quando o conteudo do drawer depende de qual linha o usuario clicou (ex: ver detalhe, trace, JSON), nao use Blade para renderizar o conteudo inicial (estara vazio). Em vez disso:

  1. Declare o drawer no Blade com um div placeholder vazio
  2. No PHP, monte o HTML e use MadResponse::html() + openDrawer()

Blade — drawer com placeholder

<mad-drawer name="detalhe-viewer" title="Detalhes" size="lg">
    <div id="detalhe-viewer-content"></div>
</mad-drawer>

PHP — injetar conteudo e abrir

public function onVerDetalhe(int $id): MadResponse
{
    $log = SystemSqlLog::findOrFail($id);

    $html = '<pre>' . htmlspecialchars($log->log_trace) . '</pre>';

    return (new MadResponse())
        ->html('#detalhe-viewer-content', $html)   // injeta no DOM
        ->openDrawer('detalhe-viewer');              // depois abre
}

A ordem importa: html() ANTES de openDrawer(), para que o conteudo ja esteja no DOM quando o drawer animar.

Quando usar cada abordagem

Cenario Abordagem
Drawer com conteudo fixo (form de filtro, form de edicao) open-drawer="nome" no <mad-btn> + conteudo direto no Blade
Drawer com conteudo dinamico por linha (trace, JSON, detalhe) MadResponse::html('#id', $html)->openDrawer('nome')

NUNCA renderizar conteudo dinamico via Blade prop

{{-- ERRADO: $traceHtml esta vazio no render inicial, drawer abre vazio --}}
<mad-drawer name="trace" title="Trace" size="lg">
    {!! $__component->traceHtml !!}
</mad-drawer>

{{-- CERTO: placeholder + injecao via MadResponse --}}
<mad-drawer name="trace" title="Trace" size="lg">
    <div id="trace-content"></div>
</mad-drawer>

manageRow — atualizar/inserir linha sem reload

Quando um formulario (drawer/modal) salva um registro, use manageRow() no MadResponse para atualizar ou inserir a linha no grid sem recarregar a pagina inteira. A linha recebe highlight azul automatico.

No formulario (drawer/modal)

public function onSave(): MadResponse
{
    // ... salvar ...
    return (new MadResponse())
        ->toast('Salvo!', 'success')
        ->closeDrawer()
        ->manageRow($registro->id, MinhaListagem::class);
}

Como funciona

  1. manageRow($id, $gridClass) chama MadDataGrid::renderSingleRow() no PHP
  2. O servidor gera o HTML da <tr> com colunas, acoes e formatacao identicas ao grid
  3. O JS recebe a op manage_row e:
    • Se a row ja existe (data-row-id): substitui o HTML + highlight azul
    • Se nao existe (registro novo): insere no topo do tbody + highlight azul
  4. Lucide icons e Alpine.js sao reinicializados na row automaticamente

Requisitos

  • O grid precisa ter sido renderizado pelo menos uma vez na sessao (para cache da config inline)
  • O $gridClass deve ser a classe do MadDataGrid (ex: SystemUnitList::class)
  • O $id deve corresponder ao campo id do registro

removeRow vs manageRow

Operacao Metodo Quando usar
Remover linha ->removeRow($id, $gridClass) Apos excluir registro
Atualizar/inserir linha ->manageRow($id, $gridClass) Apos salvar em formulario

Exemplo completo (form + list)

// No FormController (drawer)
public function onSave(): MadResponse
{
    // ... validar e salvar ...
    return (new MadResponse())
        ->toast(__('admin.unit_saved'), 'success')
        ->closeDrawer()
        ->manageRow($unit->id, SystemUnitList::class);
}

// No ListController (grid) — removeRow ja existia
public function onDelete(int $id): MadResponse
{
    // ... excluir ...
    return (new MadResponse())
        ->removeRow($id, static::class)
        ->toast('Excluido', 'success');
}

Card View — visualizacao em cards

Habilita um toggle tabela/cards na toolbar. Os cards sao renderizados server-side junto com a tabela e alternados client-side via Alpine x-show (sem request extra). Preferencia salva em localStorage.

Habilitar

{{-- Simples — roles auto-detectados --}}
<mad-grid self per-page="12" card-view>

{{-- Cards como view padrao --}}
<mad-grid self per-page="12" card-view card-default>

{{-- Numero de colunas do grid de cards (default: 3, responsivo) --}}
<mad-grid self per-page="12" card-view card-cols="4">

Atributos do <mad-grid>

Atributo Descricao
card-view Habilita o toggle tabela/cards
card-default Cards como visualizacao padrao (ao inves de tabela)
card-cols="3" Colunas do grid de cards. Responsivo: 1 col <= 768px, 2 cols <= 1200px

Card roles — mapeamento coluna → posicao no card

Cada coluna pode ter um card-role que define onde ela aparece no layout do card:

Role Posicao no card Auto-detectado quando
title Titulo principal (topo esquerdo) Primeira coluna de texto (nao-id, nao-badge, nao-money, nao-date)
subtitle Texto secundario abaixo do titulo Primeira coluna com date
badge Badge no canto superior direito Primeira coluna com badge="..."
highlight Valor destacado grande (ex: preco) Primeira coluna com money="..."
image Imagem no topo do card Nenhum (somente explicito)
(vazio) Campo label:valor no corpo Todas as demais colunas

Auto-deteccao de roles

Se nenhuma coluna declarar card-role, o framework atribui automaticamente:

<mad-grid self card-view>
    <mad-columns>
        <mad-col field="id" label="Cod." width="70" center sort />     {{-- corpo (id ignorado) --}}
        <mad-col field="nome" label="Nome" sort filter />               {{-- → title (1o texto) --}}
        <mad-col field="email" label="Email" />                         {{-- corpo --}}
        <mad-col field="valor" label="Valor" money="R$" total="sum" />  {{-- → highlight (money) --}}
        <mad-col field="dt_criacao" label="Data" date="d/m/Y" />        {{-- → subtitle (date) --}}
        <mad-col field="status" label="Status"
            badge="A:success:Ativo|I:danger:Inativo" />                 {{-- → badge --}}
    </mad-columns>
</mad-grid>

Roles explicitos

<mad-col field="foto_url" label="Foto" card-role="image" hide />
<mad-col field="nome" label="Nome" card-role="title" />
<mad-col field="dt_pedido" label="Data" date="d/m/Y" card-role="subtitle" />
<mad-col field="status" label="Status" badge="..." card-role="badge" />
<mad-col field="valor_total" label="Valor" money="R$" card-role="highlight" />
<mad-col field="obs" label="Obs" />  {{-- sem role = campo no corpo --}}

Comportamento em card mode

Feature Comportamento
Paginacao Funciona igual (footer compartilhado)
Busca/Filtros Funcionam igual (toolbar compartilhada)
Sort Funciona (server-side)
Column chooser Esconde campos no card tambem
Edicao inline Desabilitada em cards (trocar para tabela para editar)
Agrupamento Nao renderizado em cards (lista plana)
Exportacao Sempre exporta dados da tabela
manageRow Atualiza tanto <tr> quanto card
removeRow Remove tanto <tr> quanto card

NUNCA fazer

{{-- ERRADO: montar cards manualmente com HTML --}}
<div class="row">
    @foreach($items as $item)
    <div class="col-4"><div class="card">...</div></div>
    @endforeach
</div>

{{-- CERTO: usar card-view do MadDataGrid --}}
<mad-grid self card-view>
    <mad-columns>...</mad-columns>
</mad-grid>

Modo zero-PHP — <mad-grid> sem subclasse

<mad-grid> sem o atributo self (ou seja, fora de uma subclasse de MadDataGrid) compila para MadGrid — uma listagem completa sem precisar escrever nenhuma classe PHP. MadGrid extends MadDataGrid, herda toda a engine (paginacao, sort, export, edicao inline), so muda como colunas/acoes sao declaradas. Use para listagens simples/temporarias; para telas com regras de negocio, prefira a subclasse documentada no resto desta pagina.

<mad-grid model="Pessoa" per-page="15" searchable action-side="left">
    <mad-columns>
        <mad-col field="id" label="Cod." width="70" center sort />
        <mad-col field="nome" label="Nome" sort filter />
        <mad-col field="status" label="Status" badge="A:success:Ativo|I:danger:Inativo" />
    </mad-columns>
    <mad-actions>
        <mad-nav icon="pencil" label="Editar" target="PessoaForm::onEdit({id})" />
        <mad-del confirm="Excluir este registro?" />
    </mad-actions>
</mad-grid>

Existe tambem um builder fluent PHP equivalente, MadGrid::of('Pessoa')->col(...)->nav(...)->del(...), util para montar a listagem 100% no controller/dashboard sem Blade. Acoes que nao sejam navegacao/exclusao exigem ->handler('MinhaClasse') apontando para uma classe com os metodos publicos correspondentes.

Acesso e permissoes

Qualquer subclasse de MadDataGrid ja e alcancavel via /app/{Classe}/{metodo} sem registro manual — nao existe mais um arquivo central listando toda tela (isso era do framework legado). O acesso e controlado por autenticacao + permissao (PermissionGate), nao por uma lista de classes.

config/mad.php → permission.public_classes e o oposto de um registro geral: e uma allowlist pequena de telas liberadas para acesso anonimo (sem login) — cadastro, reset de senha, etc. Nunca adicione uma listagem comum ali, ou ela fica acessivel sem autenticacao.

NUNCA fazer

  • NUNCA montar HTML de tabela manualmente — usar <mad-grid self>
  • NUNCA usar $_GET/$_POST para filtros — usar props publicas serializadas
  • NUNCA abrir transacao no view() — o loadData() ja gerencia
  • NUNCA usar echo ou print no controller — retornar MadResponse ou MadToast
  • NUNCA fazer paginacao manual — o grid gerencia via $perPage e $page
  • NUNCA usar :group-by="['ano','mes']" (array) em <mad-grid self> — o _renderInlineGrid() faz (string) cast no valor recebido e o array vira a string literal "Array". Use group-by="ano,mes" (string com virgula)
  • NUNCA usar <mad-del> numa subclasse de MadDataGrid sem implementar onMadGridDelete(int $id) — o metodo so existe pronto em MadGrid (modo zero-PHP); na subclasse, implemente-o ou use <mad-act method="onExcluir"> com seu proprio handler
  • NUNCA usar transform que retorna HTML sem o atributo html na coluna — o conteudo sera escapado e exibido como texto. SEMPRE adicionar html quando o transform retorna HTML:
{{-- ERRADO: HTML do transform sera escapado (exibe tags literais) --}}
<mad-col field="status" label="Status" transform="MinhaClasse::transformStatus" />

{{-- CERTO: atributo html permite renderizar o HTML do transform --}}
<mad-col field="status" label="Status" transform="MinhaClasse::transformStatus" html />

Nota: badge="..." NAO precisa de html — o badge e tratado internamente pelo GridColumn.