Detail-form (auto-save)
mad-detail-form com model + foreign-key, hooks onLoad/onSave.
<mad-detail-form> e <mad-field-list> declarados com
model + foreign-key no Blade são auto-carregados
no render e auto-salvos por $this->form->save($mestre). Os
hooks onLoadDetail e onSaveDetail permitem transformar/validar
cada linha sem perder o automatismo.
Como funciona
Load (no onEdit)
$this->form->fill($mestre)guarda o$mestrecomo o record de origem (getSourceRecord()).- No render do Blade,
<mad-detail-form>/<mad-field-list>commodel="Item"foreign-key="mestre_id"chamamMadForm::autoLoadDetailRows(). - O loader monta uma query Eloquent
WHERE mestre_id = $mestre->id, ordena poride carrega os registros. - Para cada objeto, faz
toArray()e — se houver hookonLoadDetail— mescla o array retornado pelo callback. FieldListColumn::normalizeRows()normaliza (injeta__idestável) e popula$this->fields[$name].
Save (no onSave)
$this->form->save($mestre)chamafillRecord+save()do mestre +_afterStore._afterStorechama_autoSaveDetails(), que itera todos os details registrados (declarados no Blade commodel+foreign-key).- Para cada detail, lê
getFieldList($name)(filtra rows totalmente vazias) e persiste via smart-sync. -
Smart-sync quando as rows têm coluna
id: para cada row, carrega a instância existente viafind($id)(ou cria nova), seta colunas (ignora chaves__*), seta a FK, chama o hookonSaveDetailse existir, e salva. Depois de processar todas: deleta as rows comfk = $parentId AND id NOT IN (savedIds)— remove os órfãos. - Delete + insert quando as rows não têm coluna
id: apaga tudo da FK e reinsere do zero.
Não existe filtro extra de escopo (tipo "carregar só onde tipo =
'entrada'") no load/save automático do detail — ele sempre usa
WHERE foreign_key = $mestre->id puro. Se precisar de um
subconjunto filtrado da tabela filha, monte o load manualmente com
MadFieldListTrait::loadDetailRows() (ver
Field-list)
em vez de declarar model/foreign-key no Blade.
Hook onLoadDetail — transformar rows ao carregar
Assinatura: fn($child, $master): array — retorna campos extras para mesclar
na row. Útil para trazer campos derivados de relacionamentos que não existem na tabela
do filho.
public function onEdit(int $id): void
{
$pedido = PedidoVenda::findOrFail($id);
$this->pedidoId = (int) $pedido->id;
// Hook ANTES do fill() — registra o transform que roda no load
$this->form->onLoadDetail('itens', fn ($item, $pedido) => [
'familia_produto_id' => $item->produto_id
? ($item->produto->familia_produto_id ?? '')
: '',
]);
$this->form->fill($pedido);
}
Hook onSaveDetail — transformar/validar linhas ao salvar
Assinatura: fn($child, $master, $row): void — modifica $child
(instância já com colunas setadas + FK), ou acumula no $master. Útil para
calcular totais por item, normalizar valores, acumular agregados no mestre.
public function onSave(): MadResponse
{
try {
$this->form->validate(PedidoVenda::rules($this->pedidoId));
$pedido = PedidoVenda::findOrNew($this->pedidoId);
$pedido->valor_total = 0;
// Hook ANTES do save() — calcula valor_total por item e acumula no mestre
$this->form->onSaveDetail('itens', function ($item, $pedido, $row): void {
$desconto = (float) ($item->desconto ?: 0);
$item->valor_total = max(0, ((float) $item->valor - $desconto) * (float) $item->quantidade);
$pedido->valor_total += $item->valor_total;
});
$this->form->save($pedido);
$this->pedidoId = (int) $pedido->id;
// O total acumulado pelo hook só existe DEPOIS do primeiro save() —
// precisa de um segundo save() explícito para persistir o agregado.
$pedido->save();
return (new MadResponse())
->toast('Pedido salvo!', 'success')
->closeDrawer();
} catch (MadValidationException $e) {
return $e->asModal();
} catch (\Throwable $e) {
return MadMessage::error('Erro', $e->getMessage());
}
}
O hook precisa ser registrado antes do form->save().
A ordem real dentro de cada linha é: setar colunas → setar FK → hook
→ save() da linha. Dentro do hook já existem
$item->produto_id, $item->pedido_id (FK), etc.
Como _autoSaveDetails só roda dentro do _afterStore (o
passo final do save() do mestre), um agregado tipo
$pedido->valor_total acumulado pelo hook só existe depois
do primeiro save() — para persistir esse total é necessário um
segundo $pedido->save() explícito ao final.
Quando usar hooks vs. manual
| Cenário | Abordagem |
|---|---|
| Detail simples (campos do filho mapeiam 1:1 com inputs) | Apenas declarar model + foreign-key no Blade |
| Trazer campos derivados na carga (relacionamento) | onLoadDetail() |
| Calcular/normalizar valores por linha antes de persistir | onSaveDetail() |
| Acumular totais no mestre | onSaveDetail() + segundo save() no mestre |
| Filtrar por tipo/escopo extra na query | MadFieldListTrait::loadDetailRows() manual — não dá pra fazer isso com model/foreign-key declarados direto no Blade |
Detail sem model/foreign-key no Blade | MadFieldListTrait + loadDetailRows() / saveDetailItems() manual |
Comportamento do smart-sync
- Rows com coluna
id(já existentes, carregadas via auto-load): update. - Rows sem
idouidvazio: insert novo. - Rows que sumiram do POST: delete (via
id NOT IN (savedIds)). - Rows totalmente vazias (todos os campos vazios, exceto chaves
__*): ignoradas. - Chaves começando com
__(ex:__id): nunca são setadas no model. - A FK é sempre forçada para
$mestre->{pk}— não dá pra mudar via input do form.
Layout do form de entrada (<mad-detail-fields>)
O conteúdo de <mad-detail-fields> é renderizado como Blade arbitrário
em runtime. Qualquer componente de layout do MAD pode ser usado livremente:
mad-form-grid, mad-form-stack, mad-form-section,
mad-tabs, mad-accordion, mad-card,
mad-separator, etc. Os inputs em qualquer profundidade (incluindo abas
ocultas via x-show) são coletados pelo framework via
querySelectorAll('[name]') dentro do container [data-df-fields]
— o layout não interfere na coleta.
Auto-wrap em <mad-form-grid> (default)
Quando você não declara nenhum container de layout, o compiler envolve automaticamente o
conteúdo num <mad-form-grid :cols="{form-cols}"> (padrão 2 colunas). A
detecção reconhece: mad-form-grid, mad-form-stack,
mad-form-section, mad-tabs, mad-tabs-list,
mad-accordion, mad-card — se qualquer um desses já está
presente, o auto-wrap não acontece.
Opt-out: form-layout="custom"
Para desligar o auto-wrap explicitamente (ex: layout começando com <div>
custom), use form-layout="custom" em <mad-detail-form>:
<mad-detail-form name="itens" form-layout="custom" model="PedidoItem" foreign-key="pedido_id" mode="drawer">
<mad-detail-fields>
<div class="minha-estrutura-custom">
<mad-input-field name="produto" label="Produto" />
<mad-number-field name="quantidade" label="Qtd" />
</div>
</mad-detail-fields>
<mad-columns>
<mad-col field="produto" label="Produto" />
<mad-col field="quantidade" label="Qtd" right />
</mad-columns>
</mad-detail-form>
Exemplo: tabs por grupo
<mad-detail-form name="itens" model="PedidoItem" foreign-key="pedido_id" mode="drawer" form-title="Item do pedido">
<mad-detail-fields>
<mad-tabs default="dados" variant="underline">
<mad-tabs-list>
<mad-tab name="dados" icon="package">Dados</mad-tab>
<mad-tab name="medidas" icon="ruler">Medidas</mad-tab>
<mad-tab name="obs" icon="message-square">Observações</mad-tab>
</mad-tabs-list>
<mad-tab-panel name="dados">
<mad-form-grid :cols="2">
<mad-dbcombo-field name="produto_id" label="Produto" model="Produto" display="nome" required />
<mad-number-field name="quantidade" label="Qtd" step="0.001" required />
</mad-form-grid>
</mad-tab-panel>
<mad-tab-panel name="medidas">
<mad-form-grid :cols="3">
<mad-numeric-field name="largura" label="Largura" suffix="m" />
<mad-numeric-field name="altura" label="Altura" suffix="m" />
<mad-numeric-field name="profundidade" label="Profundidade" suffix="m" />
</mad-form-grid>
</mad-tab-panel>
<mad-tab-panel name="obs">
<mad-textarea-field name="obs" label="Observações" :rows="4" />
</mad-tab-panel>
</mad-tabs>
</mad-detail-fields>
<mad-columns>
<mad-col field="produto" label="Produto" sort />
<mad-col field="quantidade" label="Qtd" right />
</mad-columns>
</mad-detail-form>
Exemplo: multi-section com separador
<mad-detail-form name="itens" model="PedidoItem" foreign-key="pedido_id" mode="drawer">
<mad-detail-fields>
<mad-form-section title="Produto" icon="package">
<mad-form-grid :cols="2">
<mad-dbcombo-field name="produto_id" label="Produto" model="Produto" display="nome" required />
<mad-number-field name="quantidade" label="Qtd" required />
</mad-form-grid>
</mad-form-section>
<mad-separator />
<mad-form-section title="Preço" icon="dollar-sign">
<mad-form-grid :cols="2">
<mad-money-field name="valor" label="Unitário" />
<mad-money-field name="desconto" label="Desconto" />
</mad-form-grid>
</mad-form-section>
</mad-detail-fields>
<!-- ... mad-columns ... -->
</mad-detail-form>
Exemplo: form-stack (campos empilhados, sem grid)
<mad-detail-form name="anotacoes" model="PedidoAnotacao" foreign-key="pedido_id" mode="modal">
<mad-detail-fields>
<mad-form-stack>
<mad-input-field name="titulo" label="Título" required />
<mad-textarea-field name="conteudo" label="Conteúdo" :rows="6" required />
</mad-form-stack>
</mad-detail-fields>
<!-- ... mad-columns ... -->
</mad-detail-form>
Regras práticas
- Inputs dentro de qualquer container são coletados automaticamente (basta ter
name). mad:modelé substituído pordata-df-bindna string inteira — funciona em qualquer profundidade.- Abas ocultas (
x-show="false") mantêm os inputs no DOM, então o add/edit preserva todos os valores. <mad-col>e<mad-act>continuam fora de<mad-detail-fields>— dentro, são ignorados (são configs da grid de exibição, não do form de entrada).
Atributos da tag <mad-detail-form>
| Prop | Default | Descrição |
|---|---|---|
name | 'detail' | Nome do detail (usado em form->getDetailForm(), getFieldList()) |
mode | 'inline' | inline, modal ou drawer |
form-title | '' | Título do modal/drawer de entrada |
form-cols | 2 | Colunas do auto-wrap quando não há layout explícito |
add-label | 'Adicionar' | Texto do botão de adicionar |
model | '' | Classe Eloquent do filho — habilita auto-load/auto-save |
foreign-key | '' | Coluna FK no model filho |
database | '' | Conexão do banco (default: conexão do model) |
before-add | '' | Método PHP para validação server-side ao adicionar |
before-delete | '' | Método PHP para validação server-side ao deletar |
per-page | 0 | Paginação client-side da listagem (0 = sem paginação) |
sticky | false | Header fixo na listagem |
width | '' | Largura do overlay em mode="modal"/"drawer" |
form-layout | 'grid' | "custom" (ou "none") desliga o auto-wrap em mad-form-grid |
Erro de validação numa linha do detail
Use MadResponse::dfFieldError($nome, $campo, $msg) (ou
MadValidationException::asDetailForm($nome)): o erro é escopado ao
container do detail. O canal do master exclui explicitamente os campos do
sub-form (:not([data-df-fields] *)), senão um campo homônimo dentro do detail
roubaria o erro do formulário mestre.
No mode="drawer" o sub-form vai para o body e sai de dentro
de [data-mad-df-name]. O JS resolve com um fallback global por
[data-df-fields][data-df-name="<nome>"] — o erro chega ao campo
mesmo teleportado. Detalhes em
Validação.
NUNCA fazer
// ERRADO: registrar hook DEPOIS do save/fill — não tem efeito
$this->form->save($pedido);
$this->form->onSaveDetail('itens', fn ($item, $pedido, $row) => null); // tarde demais
// CERTO: registrar ANTES
$this->form->onSaveDetail('itens', fn ($item, $pedido, $row) => null);
$this->form->save($pedido);
// ERRADO: chamar saveDetailItems() manualmente quando o Blade já tem model+foreign-key
// → roda 2x: uma pelo auto-save e outra pelo manual
$this->form->save($pedido);
$this->saveDetailItems('PedidoItem', 'pedido_id', $pedido->id, $this->form->getFieldList('itens'));
// CERTO: confiar no auto-save quando model+foreign-key estão no Blade
$this->form->save($pedido);
// ERRADO: alterar a FK dentro do hook onSaveDetail — é sobrescrita
$this->form->onSaveDetail('itens', function ($item, $pedido) {
$item->pedido_id = 999; // ignorado — a FK é setada ANTES do hook rodar
});
// ERRADO: assumir que o agregado calculado no hook já está persistido no mestre
// após form->save() — o store() do mestre já rodou; precisa de um segundo save()