Docs›Geração de PDF (MadDoc)›Componentes mad-doc-*
Geração de PDF (MadDoc)

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.

AtributoDefaultDescrição
sizeA4A4, A5, Letter, Legal — qualquer outro valor cai pra A4
orientationportraitportrait | landscape
margins20,15,20,15top,right,bottom,left em mm — ou um único valor (ex: 20) pra uniforme
fontDejaVu SansFamily sanitizada (letras/números/espaço/hífen, até 40 chars) — fonte com suporte a acentuação
font-size11pt, clamp 6–36
custom-css''CSS extra, injetado por último (sobrescreve qualquer regra anterior)
watermark-text''Marca d'água diagonal; vazio = sem marca
watermark-opacity0.1Clamp 0.0–1.0
watermark-angle-30Clamp -90 a 90 graus
watermark-color#94a3b8Hex; 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>
Nunca aninhe 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

AtributoDefaultDescrição
level11–4. Qualquer valor fora desse intervalo cai pra 1 (não pra 4) — não existem H5/H6 aqui
alignleftleft, center, right, justify
color#111111Hex

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

AtributoDefaultDescrição
alignleftleft, center, right, justify
size11pt, clamp 6–36
color#333333Hex
<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

ComponenteAtributos
horizontal-linethickness (px, 1–10, def. 1), color (hex, def. #d1d5db), margin-y (px, 0–100, def. 8)
spacerheight-mm (1–200, def. 10)
page-breaknenhum — 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):

AtributoDefaultDescrição
:valuenullValor cru (geralmente $record->campo) — sempre bound
formattextVer § Formatos
prefix / suffix''Texto antes/depois do valor formatado
alignleftleft, center, right (sem justify aqui)
size11pt, clamp 6–36
boldfalseNegrito
<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" />
Valor já formatado vindo de $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

FormaQuando 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

AtributoDefaultDescrição
:totals—O ArrayObject compartilhado (recebe os total-name das colunas)
group-bynullAgrupa por campo (dot notation) com cabeçalho e subtotal por grupo
group-headertrueEmite a linha de cabeçalho de cada grupo — bound (:group-header="false") pra desligar
group-subtotalstrueEmite a linha de subtotal de cada grupo — bound pra desligar. Só faz efeito com group-by
zebratrueLinhas zebradas — bound (:zebra="...")
borderstrueBordas — bound
compactfalsePadding reduzido (3px) — bound
header-bg#f3f4f6Cor do cabeçalho
cell-padding6px (ignorado se compact)
font-size10pt

Coluna — mad-doc-data-table-column

AtributoVira (spec)Descrição
labellabelCabeçalho
fieldfieldCampo — 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
formulaformulaAritmética com {campo} (ver § Fórmulas)
field-namefield_nameNome do resultado da fórmula — publica na linha pra colunas seguintes encadearem
formatterformatFormato (§ Formatos) — atributo chama-se formatter na tag-form, vira format no componente
totaltotalsum | count | avg | min | max
total-nametotal_namePublica o total em $totals[nome]
alignalignleft, center, right
widthwidthex: 40% ou 110px
maskmaskTemplate livre: "{produto.nome} - {qtd}"
attrsattrs (ou field se for 1 só)CSV de campos: "cidade, uf" → concatena
separatorseparatorSeparador 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'],
    ]" />
Corpo só aceita colunas

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.

AtributoDescrição
:rows / model / :filterMesmas regras do data-table (§ Fonte das linhas)
:aggregatesMapa nome => ['op' => …, 'field' => …, 'format' => …] — array PHP literal
:emptyHTML/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

AtributoDefaultDescrição
height-mm20 (header) / 15 (footer)Clamp 5–80
alignleft (header) / center (footer)left, center, right
backgroundtransparenttransparent ou hex
padding4pt, 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

AtributoDefaultDescrição
formatPágina {page} de {total}Tokens {page}/{total}
aligncenterleft, center, right
size9pt, 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

Atributoimagedynamic-image
srcstring estática:src bound, geralmente $record->campo
width-mm5–250, def. 401–500, def. 40
alignleft, center, right
fitcontain | cover
altTexto 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" />
HTTP(S) é rejeitado em 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

ComponenteAtributos
barcodedata (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)
qrcodedata (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

AtributoDefaultDescrição
width-mm80Clamp 30–250
aligncenterleft, 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).

FormatoEntrada 1234.5 →Regra
text (default)1234.5String crua; null→'', bool→Sim/Não, array→CSV
money1.234,502 casas, vírgula decimal, ponto de milhar — sem símbolo (contrato original; use prefix="R$ ")
currency-usdUS$ 1,234.50Símbolo + separadores en-US
currency-eur€ 1.234,50Símbolo + separadores pt-BR
currency-gbp£ 1,234.50Símbolo + separadores en-GB
currency-jpy¥ 1,2350 casas decimais
number1.234,50Idêntico a money (2 casas, pt-BR)
number-en1,234.502 casas, ponto decimal, vírgula de milhar
integer1.2350 casas decimais, arredondado
percent1.234,5%1 casa + % (o valor não é multiplicado por 100)
decimal-41.234,50004 casas decimais, pt-BR
date15/06/2026d/m/Y — aceita timestamp, string ou DateTimeInterface
date-iso2026-06-15Y-m-d
date-long15 de junho de 2026Mês por extenso em pt-BR
date-month-yearJun/2026Mês abreviado + ano
date-weekdaysegunda-feiraSó o dia da semana, em pt-BR
datetime15/06/2026 14:30d/m/Y H:i
datetime-seconds15/06/2026 14:30:07d/m/Y H:i:s
time14:30H:i
cpf123.456.789-01Exige 11 dígitos; fora disso devolve o valor cru
cnpj11.222.333/0001-81Aceita o CNPJ alfanumérico (via MadCnpj); precisa de 14 posições
cep01310-100Exige 8 dígitos
phone-br(11) 98765-432110 ou 11 dígitos; fora disso valor cru
booleanSim/NãoTrata 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__
Como o slug vira método

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 vira 0. Divisão por zero retorna 0.
  • Safe-eval (shunting-yard, sem eval() sobre o input). Fórmula inválida → retorna 0 e loga um warning, nunca derruba o PDF.
  • field-name publica 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.

Booleans sempre bound (: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.

Próximos