Componentes mad-doc-*
page, header-band, footer-band, heading, text, image, table, qrcode.
Referência completa de cada tag <mad-doc-*> disponível dentro de um
documento MadDoc — props, defaults e os gotchas reais de cada uma. Para o pipeline
geral (controller, MadDocPdf, unidades) ver
Visão geral do MadDoc;
para documentos completos do início ao fim ver
Exemplos práticos.
Página raiz — mad-doc-page
Todo documento começa aqui. Emite o <!DOCTYPE html> completo + CSS
@page; os demais blocos vivem dentro e herdam fonte/cor por cascata.
| Atributo | Default | Descrição |
|---|---|---|
size | A4 | A4, A5, Letter, Legal — qualquer outro valor cai pra A4 |
orientation | portrait | portrait | landscape |
margins | 20,15,20,15 | top,right,bottom,left em mm — ou um único valor (ex: 20) pra uniforme |
font | DejaVu Sans | Family sanitizada (letras/números/espaço/hífen, até 40 chars) — fonte com suporte a acentuação |
font-size | 11 | pt, clamp 6–36 |
custom-css | '' | CSS extra, injetado por último (sobrescreve qualquer regra anterior) |
watermark-text | '' | Marca d'água diagonal; vazio = sem marca |
watermark-opacity | 0.1 | Clamp 0.0–1.0 |
watermark-angle | -30 | Clamp -90 a 90 graus |
watermark-color | #94a3b8 | Hex; inválido cai pro default |
<mad-doc-page size="A4" orientation="portrait" margins="20,15,20,15"
font="DejaVu Sans" font-size="11"
watermark-text="RASCUNHO" watermark-opacity="0.12"
watermark-angle="-30" watermark-color="#94a3b8">
MAD__BLADE_COMMENT__1__
</mad-doc-page>
mad-doc-page
Ele emite <!DOCTYPE html>/<html>/<body>
completos. Uma segunda <mad-doc-page> dentro da primeira quebra o
documento — qualquer coisa fora da raiz é descartada pelo DOMPDF. Múltiplas páginas
de papel se fazem com <mad-doc-page-break /> dentro da mesma page,
nunca com pages aninhadas.
Texto e estrutura
mad-doc-heading
| Atributo | Default | Descrição |
|---|---|---|
level | 1 | 1–4. Qualquer valor fora desse intervalo cai pra 1 (não pra 4) — não existem H5/H6 aqui |
align | left | left, center, right, justify |
color | #111111 | Hex |
Tamanho por nível: 1→24pt, 2→18pt, 3→14pt, 4→12pt.
<mad-doc-heading level="1" align="center" color="#1e3a8a">Pedido de Venda</mad-doc-heading>
<mad-doc-heading level="2">Itens</mad-doc-heading>
mad-doc-text
| Atributo | Default | Descrição |
|---|---|---|
align | left | left, center, right, justify |
size | 11 | pt, clamp 6–36 |
color | #333333 | Hex |
<mad-doc-text align="justify" size="9" color="#334155">
Texto livre, aceita {{ $record->obs }}. Cabem tags inline: <strong>negrito</strong>,
<em>itálico</em>, <br> quebra de linha.
</mad-doc-text>
mad-doc-horizontal-line / mad-doc-spacer / mad-doc-page-break
| Componente | Atributos |
|---|---|
horizontal-line | thickness (px, 1–10, def. 1), color (hex, def. #d1d5db), margin-y (px, 0–100, def. 8) |
spacer | height-mm (1–200, def. 10) |
page-break | nenhum — quebra de página simples |
<mad-doc-horizontal-line thickness="1" color="#d1d5db" margin-y="8" />
<mad-doc-spacer height-mm="10" />
<mad-doc-page-break />
Campo único — mad-doc-variable-field
Valor único rotulado, formatado via applyFormat (ver § Formatos):
| Atributo | Default | Descrição |
|---|---|---|
:value | null | Valor cru (geralmente $record->campo) — sempre bound |
format | text | Ver § Formatos |
prefix / suffix | '' | Texto antes/depois do valor formatado |
align | left | left, center, right (sem justify aqui) |
size | 11 | pt, clamp 6–36 |
bold | false | Negrito |
<mad-doc-variable-field :value="$record->dt_pedido" format="date" prefix="Data: " align="left" bold="true" />
<mad-doc-variable-field :value="$record->cliente->nome" format="text" prefix="Cliente: " />
<mad-doc-variable-field :value="$totals['valor_total'] ?? ''" format="text"
prefix="TOTAL: " align="right" size="14" bold="true" />
$totals? Use format="text"
$totals['valor_total'] já sai formatado em pt-BR (string, ex:
"1.234,50"). Passar de novo por format="money" tenta
converter essa string pra float e geralmente vira "0,00". Sempre
format="text" ao reexibir um total.
Tabela de dados — mad-doc-data-table
O bloco mais rico. Linhas vêm de 3 fontes mutuamente exclusivas; colunas são
declaradas como filhos <mad-doc-data-table-column>.
Fonte das linhas
| Forma | Quando usar |
|---|---|
:rows="$expr" | Escape hatch — coleção já carregada |
model="X" :filter="$c" | Carregar via Eloquent com filtro (closure) |
model="X" (sozinho) | Todos os registros do model |
<mad-doc-data-table :rows="$record->itens" ...>
<mad-doc-data-table model="PedidoVendaItem" :filter="fn($q) => $q->where('pedido_id', $record->id)->limit(50)" ...>
<mad-doc-data-table model="PedidoVendaItem" ...> MAD__BLADE_COMMENT__2__
:filter é sempre uma closure Eloquent — recebe o query
builder do model e devolve Builder/Relation/Collection/array:
fn($q) => $q->where('x', 1)->orderBy('id')->limit(50). Erro de
DB/model nunca derruba o PDF — vira [] e loga um warning.
Atributos da tabela
| Atributo | Default | Descrição |
|---|---|---|
:totals | — | O ArrayObject compartilhado (recebe os total-name das colunas) |
group-by | null | Agrupa por campo (dot notation) com cabeçalho e subtotal por grupo |
group-header | true | Emite a linha de cabeçalho de cada grupo — bound (:group-header="false") pra desligar |
group-subtotals | true | Emite a linha de subtotal de cada grupo — bound pra desligar. Só faz efeito com group-by |
zebra | true | Linhas zebradas — bound (:zebra="...") |
borders | true | Bordas — bound |
compact | false | Padding reduzido (3px) — bound |
header-bg | #f3f4f6 | Cor do cabeçalho |
cell-padding | 6 | px (ignorado se compact) |
font-size | 10 | pt |
Coluna — mad-doc-data-table-column
| Atributo | Vira (spec) | Descrição |
|---|---|---|
label | label | Cabeçalho |
field | field | Campo — dot notation (cliente.nome). MadDocRuntime::pull normaliza -> para . antes de navegar (o emitter do MadBuilder escreve produto->nome), então as duas formas funcionam; escreva com ponto |
formula | formula | Aritmética com {campo} (ver § Fórmulas) |
field-name | field_name | Nome do resultado da fórmula — publica na linha pra colunas seguintes encadearem |
formatter | format | Formato (§ Formatos) — atributo chama-se formatter na tag-form, vira format no componente |
total | total | sum | count | avg | min | max |
total-name | total_name | Publica o total em $totals[nome] |
align | align | left, center, right |
width | width | ex: 40% ou 110px |
mask | mask | Template livre: "{produto.nome} - {qtd}" |
attrs | attrs (ou field se for 1 só) | CSV de campos: "cidade, uf" → concatena |
separator | separator | Separador do attrs (default espaço) |
MAD__BLADE_COMMENT__3__
<mad-doc-data-table-column label="Item" mask="{produto.nome} ({produto.codigo})" align="left" />
MAD__BLADE_COMMENT__4__
<mad-doc-data-table-column label="Endereço" attrs="cidade, uf" separator=" / " align="left" />
Exemplo completo (fórmula + totais + agrupamento)
<mad-doc-data-table
model="PedidoVendaItem"
:filter="fn($q) => $q->where('pedido_id', $record->id)->orderBy('id')"
:totals="$totals"
group-by="categoria"
zebra="true" borders="true" compact="false"
header-bg="#f3f4f6" cell-padding="6" font-size="10">
<mad-doc-data-table-column label="Produto" field="produto.nome" align="left" width="40%" />
<mad-doc-data-table-column label="Qtd" field="quantidade" align="right" formatter="integer" total="sum" />
<mad-doc-data-table-column label="Vlr Unit." field="valor_unit" align="right" formatter="money" />
<mad-doc-data-table-column label="Subtotal"
formula="{quantidade}*{valor_unit}" field-name="subtotal"
align="right" formatter="money"
total="sum" total-name="valor_total" />
</mad-doc-data-table>
Sem formatter, a célula usa format="text" (string crua). O total
de uma coluna (total="sum"), se ela não tiver formatter explícito,
usa money por padrão (ou integer pra total="count") —
a célula e o total da mesma coluna podem ter formatos diferentes por essa razão.
Forma alternativa — componente direto
Útil quando as colunas já estão montadas em PHP (ex: codegen). Componente é
<x-doc-data-table> e as chaves são snake_case
(field_name, total_name, format em vez de
formatter) — é exatamente o que o compilador da tag-form gera por baixo:
<x-doc-data-table
:rows="$itens"
:totals="$totals"
:columns="[
['label' => 'Qtd', 'field' => 'quantidade', 'align' => 'right', 'format' => 'integer', 'total' => 'sum'],
['label' => 'Subtotal', 'formula' => '{quantidade}*{valor_unit}', 'field_name' => 'subtotal',
'align' => 'right', 'format' => 'money', 'total' => 'sum', 'total_name' => 'valor_total'],
]" />
Dentro de <mad-doc-data-table> só
<mad-doc-data-table-column ... /> é reconhecido — qualquer outra
tag no corpo é descartada pelo compilador. :rows e model
são mutuamente exclusivos: não combine os dois.
Repetidor — mad-doc-repeater
Itera linhas renderizando um template livre por linha ($record vira a linha
atual dentro do loop), acumulando agregados. Use quando o layout por item
não é tabular — o corpo aceita qualquer Blade, diferente do data-table.
| Atributo | Descrição |
|---|---|
:rows / model / :filter | Mesmas regras do data-table (§ Fonte das linhas) |
:aggregates | Mapa nome => ['op' => …, 'field' => …, 'format' => …] — array PHP literal |
:empty | HTML/expr renderizado quando não há linhas |
@php $totals = $totals ?? new \ArrayObject(); @endphp
<mad-doc-repeater
model="PedidoVendaItem"
:filter="fn($q) => $q->where('pedido_id', $record->id)"
:aggregates="[
'total_qtd' => ['op' => 'sum', 'field' => 'quantidade', 'format' => 'integer'],
'total_vlr' => ['op' => 'sum', 'field' => 'produto->preco', 'format' => 'money'],
]"
:empty="'<p>Nenhum item.</p>'">
<mad-doc-text>{{ $record->produto->nome }} — {{ $record->quantidade }}x</mad-doc-text>
</mad-doc-repeater>
<mad-doc-variable-field :value="$totals['total_vlr'] ?? ''" format="text"
prefix="Total: " align="right" bold="true" />
Ops de agregado suportadas: sum, count, avg, min, max (field é opcional só pra count). Resultado vai pra $totals[nome] (formatado) e $totals[nome . '_raw'] (float cru). $record é salvo antes do loop e restaurado depois — fora do repeater ele volta a apontar pro registro mestre.
:aggregates exige array literal puro
Nada de $variavel, func() ou :: dentro do
literal — o compilador valida isso em tempo de compilação e silenciosamente ignora
agregados inválidos (sem total nenhum, sem erro visível). Resolva valores dinâmicos
antes, fora do array:
// ERRADO - :aggregates precisa ser um array literal puro, sem $var/func()/::
:aggregates="['total' => ['op' => 'sum', 'field' => $campoDinamico]]"
// CERTO - resolva o nome do campo ANTES, fora do array literal
field em :aggregates não aceita dot notation
Diferente de field em coluna de data-table (que aceita
cliente.nome), o field de um agregado do repeater vira
acesso de propriedade PHP direto ($row->campo) — use nome simples
(quantidade) ou encadeamento com seta
(produto->preco), nunca ponto.
Cabeçalho / rodapé fixos — header-band / footer-band
| Atributo | Default | Descrição |
|---|---|---|
height-mm | 20 (header) / 15 (footer) | Clamp 5–80 |
align | left (header) / center (footer) | left, center, right |
background | transparent | transparent ou hex |
padding | 4 | pt, clamp 0–40 |
<mad-doc-page margins="28,15,18,15" ...>
<mad-doc-header-band height-mm="20" align="left" background="#1e3a8a" padding="5">
<table style="width:100%;border-collapse:collapse;color:#fff;">
<tr>
<td>{{ $empresa['nome'] }}</td>
<td style="text-align:right;">Pedido #{{ $record->id }}</td>
</tr>
</table>
</mad-doc-header-band>
<mad-doc-footer-band height-mm="12" align="center" background="#f8fafc" padding="3">
<mad-doc-page-number format="Página {page}" align="center" size="8" />
</mad-doc-footer-band>
MAD__BLADE_COMMENT__5__
<mad-doc-spacer height-mm="20" />
MAD__BLADE_COMMENT__6__
</mad-doc-page>
position:fixed sobrepõe conteúdo sem spacer
As bandas usam position:fixed — repetem em toda página, mas não
empurram o fluxo normal do documento. margins.top/margins.bottom
(mm) da page precisam ser maiores que height-mm da banda; e
todo início de página (corpo + depois de cada
<mad-doc-page-break />) precisa de um
<mad-doc-spacer height-mm="…" /> com altura ≥ a da banda —
senão o conteúdo abre por baixo dela e fica ilegível.
Numeração de página — mad-doc-page-number
| Atributo | Default | Descrição |
|---|---|---|
format | Página {page} de {total} | Tokens {page}/{total} |
align | center | left, center, right |
size | 9 | pt, clamp 6–18 |
<mad-doc-page-number format="Página {page}" align="center" size="9" />
<mad-doc-page-number format="Página {page} de {total}" /> MAD__BLADE_COMMENT__7__
Os tokens viram <span>: {page} vira
<span class="mad-page">, resolvido pelo CSS counter
counter(page) definido em <mad-doc-page>;
{total} vira <span class="mad-pages"></span>.
{total} é exato — custa uma render extra
counter(pages) não funciona no Dompdf 3.x (sai vazio/0).
Por isso MadDocPdf::fromHtml() faz two-pass: se o HTML
contém o span mad-pages, ele renderiza uma vez só pra contar as páginas
(getCanvas()->get_page_count(), mínimo 1), substitui o span pelo número
literal e renderiza de novo. Ou seja: {total} é confiável, mas usá-lo
dobra o custo de render do documento — em relatório muito grande,
prefira só {page}. Vale também para o span emitido por banda de layout
livre; sem o span, nada muda (render única).
Imagens — mad-doc-image / mad-doc-dynamic-image
| Atributo | image | dynamic-image |
|---|---|---|
src | string estática | :src bound, geralmente $record->campo |
width-mm | 5–250, def. 40 | 1–500, def. 40 |
align | left, center, right | |
fit | contain | cover | |
alt | Texto alternativo | |
MAD__BLADE_COMMENT__8__
<mad-doc-image :src="$logoDataUri" width-mm="40" align="center" fit="contain" alt="Logo" />
MAD__BLADE_COMMENT__9__
<mad-doc-dynamic-image :src="$record->foto_url" width-mm="60" fit="cover" align="left" />
mad-doc-image
src com esquema http:///https:// (que não
seja data:image/) renderiza o placeholder
"[Imagem sem origem válida]" em vez da imagem. Use data URI
(data:image/png;base64,...) ou um path local dentro do
chroot do DOMPDF (storage_path('app') por padrão).
mad-doc-dynamic-image não bloqueia no componente, mas o DOMPDF em si só
busca URL remota com isRemoteEnabled=true no $opts:
// dynamic-image com URL remota PRECISA disso no $opts do fromView/fromHtml:
$pdf = MadDocPdf::fromView('docs.pedido-venda', $data, [
'isRemoteEnabled' => true,
]);
Código de barras e QR — mad-doc-barcode / mad-doc-qrcode
| Componente | Atributos |
|---|---|
barcode | data (obrig.), format C128/C39/EAN13/EAN8/UPCA (def. C128, desconhecido cai pra C128), width-mm 10–250 (def. 60), height-mm 5–60 (def. 15), show-text (def. true), align (def. left) |
qrcode | data (obrig.), size-mm 10–80 (def. 25), ec-level L/M/Q/H (def. M), align (def. left) |
<mad-doc-barcode :data="'PV' . str_pad((string) $record->id, 10, '0', STR_PAD_LEFT)"
format="C128" width-mm="80" height-mm="18" show-text="true" align="center" />
<mad-doc-qrcode :data="'https://app.exemplo.com.br/pedido/' . $record->id"
size-mm="28" ec-level="M" align="center" />
Ambos renderizam como data:image/...;base64,... embutido — DOMPDF não toca o
disco. data vazio → nada é renderizado (silencioso, sem erro). QR usa Imagick
quando disponível (PNG mais compacto); sem a extensão, cai pra SVG puro automaticamente.
Assinatura — mad-doc-signature
| Atributo | Default | Descrição |
|---|---|---|
width-mm | 80 | Clamp 30–250 |
align | center | left, center, right |
name | '' | Nome abaixo da linha |
role | '' | Cargo/papel abaixo do nome |
<mad-doc-signature width-mm="80" align="center"
:name="$record->cliente->nome" role="Cliente" />
Formatos (format / formatter)
Usados em format (variable-field, agregados de repeater) e
formatter (coluna de data-table — alias da mesma chave). Padrão pt-BR.
MadDocRuntime::applyFormat delega os built-ins pro resolver ÚNICO
\Mad\Support\ValueFormatter — o mesmo catálogo do
formatter-select do MadBuilder e das colunas de grid. Token é case-insensitive e
token desconhecido cai em text (nunca lança).
| Formato | Entrada 1234.5 → | Regra |
|---|---|---|
text (default) | 1234.5 | String crua; null→'', bool→Sim/Não, array→CSV |
money | 1.234,50 | 2 casas, vírgula decimal, ponto de milhar — sem símbolo (contrato original; use prefix="R$ ") |
currency-usd | US$ 1,234.50 | Símbolo + separadores en-US |
currency-eur | € 1.234,50 | Símbolo + separadores pt-BR |
currency-gbp | £ 1,234.50 | Símbolo + separadores en-GB |
currency-jpy | ¥ 1,235 | 0 casas decimais |
number | 1.234,50 | Idêntico a money (2 casas, pt-BR) |
number-en | 1,234.50 | 2 casas, ponto decimal, vírgula de milhar |
integer | 1.235 | 0 casas decimais, arredondado |
percent | 1.234,5% | 1 casa + % (o valor não é multiplicado por 100) |
decimal-4 | 1.234,5000 | 4 casas decimais, pt-BR |
date | 15/06/2026 | d/m/Y — aceita timestamp, string ou DateTimeInterface |
date-iso | 2026-06-15 | Y-m-d |
date-long | 15 de junho de 2026 | Mês por extenso em pt-BR |
date-month-year | Jun/2026 | Mês abreviado + ano |
date-weekday | segunda-feira | Só o dia da semana, em pt-BR |
datetime | 15/06/2026 14:30 | d/m/Y H:i |
datetime-seconds | 15/06/2026 14:30:07 | d/m/Y H:i:s |
time | 14:30 | H:i |
cpf | 123.456.789-01 | Exige 11 dígitos; fora disso devolve o valor cru |
cnpj | 11.222.333/0001-81 | Aceita o CNPJ alfanumérico (via MadCnpj); precisa de 14 posições |
cep | 01310-100 | Exige 8 dígitos |
phone-br | (11) 98765-4321 | 10 ou 11 dígitos; fora disso valor cru |
boolean | Sim/Não | Trata 1/t/true/sim/s/y/yes (case-insensitive) como verdadeiro |
Transformer custom — custom:<slug>
Além dos built-ins, format/formatter aceita o prefixo
custom:, resolvido por contexto (documento, não grid):
MAD__BLADE_COMMENT__12__
<mad-doc-data-table-column label="Situação" field="status" formatter="custom:status_color" />
MAD__BLADE_COMMENT__13__
<mad-doc-variable-field :value="$record->status" format="custom:status_color" />
MAD__BLADE_COMMENT__14__
custom:status_color → \App\Transformer\DocumentTransformer::statusColor($value)
(slug snake/kebab → camelCase). Fallback legado, pra projetos gerados antigos:
\App\Transformer\StatusColor::apply($value). Slug só aceita
[A-Za-z0-9_-]; classe/método ausente, retorno não-escalar ou exceção
degradam pro valor cru e logam warning — o documento nunca quebra.
Note que no grid o mesmo custom: resolve pra
GridTransformer: são classes diferentes por contexto.
Fórmulas
Atributo formula de coluna: aritmética com {campo} resolvido por
linha. Ex: {quantidade}*{valor_unit}.
- Operadores:
+ - * / ( ). Token não-numérico vira0. Divisão por zero retorna0. - Safe-eval (shunting-yard, sem
eval()sobre o input). Fórmula inválida → retorna0e loga um warning, nunca derruba o PDF. field-namepublica o resultado de volta na linha → colunas seguintes podem referenciá-lo no próprio{token}.
Bag de totais — $totals
new \ArrayObject()criado no controller (ou no topo da view), passado via:totals— por referência, mutações dentro do componente propagam pra fora.total-name(data-table) e:aggregates(repeater) publicam valores nele.- Leitura no Blade:
MAD__BLADE_COMMENT__10__
{{ $totals['valor_total'] ?? '' }}
MAD__BLADE_COMMENT__11__
{{ $totals['total_vlr_raw'] ?? 0 }}
_raw (valor float cru, sem formatação) só existe pra totais publicados pelo repeater — o data-table só publica a versão formatada.
:nome="true")
Atributo de componente sem : sempre vira string literal
— e em PHP qualquer string não-vazia é truthy, então
zebra="false" avalia como true de qualquer forma:
zebra="false" MAD__BLADE_COMMENT__15__
:zebra="false" MAD__BLADE_COMMENT__16__
Vale pra zebra, borders, compact, group-header, group-subtotals (data-table), bold (variable-field), show-text (barcode) e qualquer outro boolean.