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>nemsearchabletem 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 declararpublic bool $autoLoad = false;para pular a query domount()— o atributo no Blade so e visto na renderizacao. Grids gerados pelo MadBuilder nao precisam: omount()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" />
Navegacao (abre outra tela)
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 gridsearch-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:
- Valor fixo, sem codigo — no MadBuilder, Configuracoes do projeto →
Exportacao de PDF → Campos proprios. Vai na chave
"placeholders"doapp/config/pdf-export.json({"CNPJ": "12.345.678/0001-90"}) e vira chip Campos do projeto no editor de bandas. - Valor calculado, todas as telas — classe
App\Helpers\PdfExportPlaceholders(o MadBuilder cria o esqueleto pelo botao Criar classe de campos; e ummad_codedo 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')];
}
}
- 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 mesmoname(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-masko 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 afield="{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
sortablecom 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-labelaceita{group}e a mesma sintaxe dogroup-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" />
runningnua acumula o valor da propria coluna (depois doevaluate); com valor, acumula o resultado da expressao — mesma DSL e mesmo sanitizador doevaluate, inclusive{relacao->campo}.running-reset="group"zera na quebra do nivel 0;group:1zera no nivel 1;none(default) corre do inicio ao fim.running-startsemeia 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 colunarunningdevolve o ULTIMO saldo, nao a soma dos saldos (que nao significa nada). Usetotal="last"em qualquer coluna para o mesmo efeito.- Com
per-pagemaior que zero o saldo acumula dentro da PAGINA (fica um aviso no log). Relatorio usaper-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:
- Declare o drawer no Blade com um
divplaceholder vazio - 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
manageRow($id, $gridClass)chamaMadDataGrid::renderSingleRow()no PHP- O servidor gera o HTML da
<tr>com colunas, acoes e formatacao identicas ao grid - O JS recebe a op
manage_rowe:- 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
- Se a row ja existe (
- 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
$gridClassdeve ser a classe do MadDataGrid (ex:SystemUnitList::class) - O
$iddeve corresponder ao campoiddo 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/$_POSTpara filtros — usar props publicas serializadas - NUNCA abrir transacao no
view()— oloadData()ja gerencia - NUNCA usar
echoouprintno controller — retornarMadResponseouMadToast - NUNCA fazer paginacao manual — o grid gerencia via
$perPagee$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". Usegroup-by="ano,mes"(string com virgula) - NUNCA usar
<mad-del>numa subclasse deMadDataGridsem implementaronMadGridDelete(int $id)— o metodo so existe pronto emMadGrid(modo zero-PHP); na subclasse, implemente-o ou use<mad-act method="onExcluir">com seu proprio handler - NUNCA usar
transformque retorna HTML sem o atributohtmlna coluna — o conteudo sera escapado e exibido como texto. SEMPRE adicionarhtmlquando 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.