Docs›Formulários›Detail-form (auto-save)
Formulários

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)

  1. $this->form->fill($mestre) guarda o $mestre como o record de origem (getSourceRecord()).
  2. No render do Blade, <mad-detail-form>/<mad-field-list> com model="Item" foreign-key="mestre_id" chamam MadForm::autoLoadDetailRows().
  3. O loader monta uma query Eloquent WHERE mestre_id = $mestre->id, ordena por id e carrega os registros.
  4. Para cada objeto, faz toArray() e — se houver hook onLoadDetail — mescla o array retornado pelo callback.
  5. FieldListColumn::normalizeRows() normaliza (injeta __id estável) e popula $this->fields[$name].

Save (no onSave)

  1. $this->form->save($mestre) chama fillRecord + save() do mestre + _afterStore.
  2. _afterStore chama _autoSaveDetails(), que itera todos os details registrados (declarados no Blade com model + foreign-key).
  3. Para cada detail, lê getFieldList($name) (filtra rows totalmente vazias) e persiste via smart-sync.
  4. Smart-sync quando as rows têm coluna id: para cada row, carrega a instância existente via find($id) (ou cria nova), seta colunas (ignora chaves __*), seta a FK, chama o hook onSaveDetail se existir, e salva. Depois de processar todas: deleta as rows com fk = $parentId AND id NOT IN (savedIds) — remove os órfãos.
  5. Delete + insert quando as rows não têm coluna id: apaga tudo da FK e reinsere do zero.
Detail é sempre escopado por FK

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());
    }
}
Ordem de execução importa

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árioAbordagem
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 persistironSaveDetail()
Acumular totais no mestreonSaveDetail() + segundo save() no mestre
Filtrar por tipo/escopo extra na queryMadFieldListTrait::loadDetailRows() manual — não dá pra fazer isso com model/foreign-key declarados direto no Blade
Detail sem model/foreign-key no BladeMadFieldListTrait + loadDetailRows() / saveDetailItems() manual

Comportamento do smart-sync

  • Rows com coluna id (já existentes, carregadas via auto-load): update.
  • Rows sem id ou id vazio: 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 por data-df-bind na 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>

PropDefaultDescriçã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-cols2Colunas 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-page0Paginação client-side da listagem (0 = sem paginação)
stickyfalseHeader 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.

mode="drawer" teleporta o sub-form

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()

Próximos